diff options
Diffstat (limited to 'org.eclipse.jgit')
628 files changed, 18994 insertions, 6929 deletions
diff --git a/org.eclipse.jgit/.classpath b/org.eclipse.jgit/.classpath index 139a059adc..dde8e3fce5 100644 --- a/org.eclipse.jgit/.classpath +++ b/org.eclipse.jgit/.classpath @@ -2,7 +2,7 @@ <classpath> <classpathentry kind="src" path="src"/> <classpathentry kind="src" path="resources"/> - <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"> <attributes> <attribute name="module" value="true"/> </attributes> diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 1fbc01102b..eeed75f64e 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -1,212 +1,73 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <component id="org.eclipse.jgit" version="2"> - <resource path="src/org/eclipse/jgit/errors/PackMismatchException.java" type="org.eclipse.jgit.errors.PackMismatchException"> - <filter id="1142947843"> - <message_arguments> - <message_argument value="5.9.1"/> - <message_argument value="isPermanent()"/> - </message_arguments> - </filter> - <filter id="1142947843"> - <message_arguments> - <message_argument value="5.9.1"/> - <message_argument value="setPermanent(boolean)"/> - </message_arguments> - </filter> - </resource> - <resource path="src/org/eclipse/jgit/lib/BatchingProgressMonitor.java" type="org.eclipse.jgit.lib.BatchingProgressMonitor"> - <filter id="336695337"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.BatchingProgressMonitor"/> - <message_argument value="onEndTask(String, int, Duration)"/> - </message_arguments> - </filter> - <filter id="336695337"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.BatchingProgressMonitor"/> - <message_argument value="onEndTask(String, int, int, int, Duration)"/> - </message_arguments> - </filter> - <filter id="336695337"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.BatchingProgressMonitor"/> - <message_argument value="onUpdate(String, int, Duration)"/> - </message_arguments> - </filter> - <filter id="336695337"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.BatchingProgressMonitor"/> - <message_argument value="onUpdate(String, int, int, int, Duration)"/> - </message_arguments> - </filter> - <filter id="338792546"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.BatchingProgressMonitor"/> - <message_argument value="onEndTask(String, int)"/> - </message_arguments> - </filter> - <filter id="338792546"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.BatchingProgressMonitor"/> - <message_argument value="onEndTask(String, int, int, int)"/> - </message_arguments> - </filter> - <filter id="338792546"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.BatchingProgressMonitor"/> - <message_argument value="onUpdate(String, int)"/> - </message_arguments> - </filter> - <filter id="338792546"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.BatchingProgressMonitor"/> - <message_argument value="onUpdate(String, int, int, int)"/> - </message_arguments> - </filter> - </resource> - <resource path="src/org/eclipse/jgit/lib/BranchConfig.java" type="org.eclipse.jgit.lib.BranchConfig$BranchRebaseMode"> - <filter id="372293724"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.BranchConfig.BranchRebaseMode"/> - <message_argument value="PRESERVE"/> - </message_arguments> - </filter> - </resource> - <resource path="src/org/eclipse/jgit/lib/ConfigConstants.java" type="org.eclipse.jgit.lib.ConfigConstants"> - <filter id="1142947843"> - <message_arguments> - <message_argument value="5.13.2"/> - <message_argument value="CONFIG_KEY_BITMAP_EXCLUDED_REFS_PREFIXES"/> - </message_arguments> - </filter> - <filter id="1142947843"> + <resource path="META-INF/MANIFEST.MF"> + <filter id="923795461"> <message_arguments> - <message_argument value="5.13.2"/> - <message_argument value="CONFIG_KEY_PRESERVE_OLD_PACKS"/> + <message_argument value="7.2.2"/> + <message_argument value="7.1.0"/> </message_arguments> </filter> - <filter id="1142947843"> - <message_arguments> - <message_argument value="5.13.2"/> - <message_argument value="CONFIG_KEY_PRUNE_PRESERVED"/> - </message_arguments> - </filter> - <filter id="1142947843"> - <message_arguments> - <message_argument value="5.13.2"/> - <message_argument value="CONFIG_KEY_SKIPHASH"/> - </message_arguments> - </filter> - <filter id="1142947843"> - <message_arguments> - <message_argument value="6.1.1"/> - <message_argument value="CONFIG_KEY_TRUST_PACKED_REFS_STAT"/> - </message_arguments> - </filter> - </resource> - <resource path="src/org/eclipse/jgit/lib/CoreConfig.java" type="org.eclipse.jgit.lib.CoreConfig"> - <filter id="336658481"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.CoreConfig"/> - <message_argument value="DEFAULT_COMMIT_GRAPH_ENABLE"/> - </message_arguments> - </filter> - </resource> - <resource path="src/org/eclipse/jgit/lib/CoreConfig.java" type="org.eclipse.jgit.lib.CoreConfig$TrustPackedRefsStat"> - <filter id="1142947843"> + <filter id="934281281"> <message_arguments> - <message_argument value="6.1.1"/> - <message_argument value="TrustPackedRefsStat"/> - </message_arguments> - </filter> - </resource> - <resource path="src/org/eclipse/jgit/lib/ProgressMonitor.java" type="org.eclipse.jgit.lib.ProgressMonitor"> - <filter id="403804204"> - <message_arguments> - <message_argument value="org.eclipse.jgit.lib.ProgressMonitor"/> - <message_argument value="showDuration(boolean)"/> + <message_argument value="org.eclipse.jgit.lib"/> + <message_argument value="8.1.0"/> + <message_argument value="7.1.0"/> </message_arguments> </filter> </resource> <resource path="src/org/eclipse/jgit/lib/RefDatabase.java" type="org.eclipse.jgit.lib.RefDatabase"> - <filter id="336658481"> + <filter id="336695337"> <message_arguments> <message_argument value="org.eclipse.jgit.lib.RefDatabase"/> - <message_argument value="additionalRefsNames"/> - </message_arguments> - </filter> - </resource> - <resource path="src/org/eclipse/jgit/lib/Repository.java" type="org.eclipse.jgit.lib.Repository"> - <filter id="1142947843"> - <message_arguments> - <message_argument value="5.13.2"/> <message_argument value="getReflogReader(Ref)"/> </message_arguments> </filter> </resource> - <resource path="src/org/eclipse/jgit/lib/TextProgressMonitor.java" type="org.eclipse.jgit.lib.TextProgressMonitor"> - <filter id="338792546"> + <resource path="src/org/eclipse/jgit/lib/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter"> + <filter id="403804204"> <message_arguments> - <message_argument value="org.eclipse.jgit.lib.TextProgressMonitor"/> - <message_argument value="onEndTask(String, int)"/> + <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/> + <message_argument value="getBoolean(Config, String, String, String, Boolean)"/> </message_arguments> </filter> - <filter id="338792546"> + <filter id="403804204"> <message_arguments> - <message_argument value="org.eclipse.jgit.lib.TextProgressMonitor"/> - <message_argument value="onEndTask(String, int, int, int)"/> + <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/> + <message_argument value="getInt(Config, String, String, String, Integer)"/> </message_arguments> </filter> - <filter id="338792546"> + <filter id="403804204"> <message_arguments> - <message_argument value="org.eclipse.jgit.lib.TextProgressMonitor"/> - <message_argument value="onUpdate(String, int)"/> + <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/> + <message_argument value="getIntInRange(Config, String, String, String, int, int, Integer)"/> </message_arguments> </filter> - <filter id="338792546"> + <filter id="403804204"> <message_arguments> - <message_argument value="org.eclipse.jgit.lib.TextProgressMonitor"/> - <message_argument value="onUpdate(String, int, int, int)"/> + <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/> + <message_argument value="getLong(Config, String, String, String, Long)"/> </message_arguments> </filter> - </resource> - <resource path="src/org/eclipse/jgit/revwalk/RevCommit.java" type="org.eclipse.jgit.revwalk.RevCommit"> - <filter id="1193279491"> + <filter id="403804204"> <message_arguments> - <message_argument value="6.5.1"/> - <message_argument value="buffer"/> + <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/> + <message_argument value="getTimeUnit(Config, String, String, String, Long, TimeUnit)"/> </message_arguments> </filter> </resource> - <resource path="src/org/eclipse/jgit/storage/pack/PackConfig.java" type="org.eclipse.jgit.storage.pack.PackConfig"> - <filter id="336658481"> - <message_arguments> - <message_argument value="org.eclipse.jgit.storage.pack.PackConfig"/> - <message_argument value="DEFAULT_BITMAP_EXCLUDED_REFS_PREFIXES"/> - </message_arguments> - </filter> - <filter id="336658481"> - <message_arguments> - <message_argument value="org.eclipse.jgit.storage.pack.PackConfig"/> - <message_argument value="DEFAULT_MIN_BYTES_FOR_OBJ_SIZE_INDEX"/> - </message_arguments> - </filter> + <resource path="src/org/eclipse/jgit/revwalk/RevWalk.java" type="org.eclipse.jgit.revwalk.RevWalk"> <filter id="1142947843"> <message_arguments> - <message_argument value="5.13.2"/> - <message_argument value="DEFAULT_BITMAP_EXCLUDED_REFS_PREFIXES"/> + <message_argument value="6.10.1"/> + <message_argument value="isMergedIntoAnyCommit(RevCommit, Collection<RevCommit>)"/> </message_arguments> </filter> - <filter id="1142947843"> - <message_arguments> - <message_argument value="5.13.2"/> - <message_argument value="getBitmapExcludedRefsPrefixes()"/> - </message_arguments> - </filter> - <filter id="1142947843"> + </resource> + <resource path="src/org/eclipse/jgit/util/Iterators.java" type="org.eclipse.jgit.util.Iterators"> + <filter id="1109393411"> <message_arguments> - <message_argument value="5.13.2"/> - <message_argument value="setBitmapExcludedRefsPrefixes(String[])"/> + <message_argument value="6.10.2"/> + <message_argument value="org.eclipse.jgit.util.Iterators"/> </message_arguments> </filter> </resource> diff --git a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs index 2abe9529d3..ef3d8ecaba 100644 --- a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs +++ b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs @@ -7,9 +7,9 @@ org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jgit.annotations.N org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.compliance=17 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate @@ -40,7 +40,7 @@ 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.invalidJavadocTagsDeprecatedRef=disabled org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=private org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning @@ -52,7 +52,7 @@ org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=error 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.missingJavadocTagDescription=all_standard_tags org.eclipse.jdt.core.compiler.problem.missingJavadocTags=error org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled @@ -115,7 +115,7 @@ org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=error org.eclipse.jdt.core.compiler.release=enabled -org.eclipse.jdt.core.compiler.source=11 +org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false diff --git a/org.eclipse.jgit/BUILD b/org.eclipse.jgit/BUILD index e806e7d6d0..1e9b7708a0 100644 --- a/org.eclipse.jgit/BUILD +++ b/org.eclipse.jgit/BUILD @@ -20,6 +20,7 @@ java_library( resources = RESOURCES, deps = [ ":insecure_cipher_factory", + "//lib:commons-codec", "//lib:javaewah", "//lib:slf4j-api", ], diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 9cc316158a..79e0cc7715 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -3,12 +3,16 @@ Bundle-ManifestVersion: 2 Bundle-Name: %Bundle-Name Automatic-Module-Name: org.eclipse.jgit Bundle-SymbolicName: org.eclipse.jgit -Bundle-Version: 6.6.0.qualifier -Bundle-Localization: plugin +Bundle-Version: 7.4.0.qualifier +Bundle-Localization: OSGI-INF/l10n/plugin Bundle-Vendor: %Bundle-Vendor +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Bundle-SCM: url=https://github.com/eclipse-jgit/jgit, connection=scm:git:https://eclipse.gerrithub.io/eclipse-jgit/jgit.git, developerConnection=scm:git:https://eclipse.gerrithub.io/a/eclipse-jgit/jgit.git +Service-Component: OSGI-INF/org.eclipse.jgit.internal.util.CleanupService.xml Eclipse-ExtensibleAPI: true -Export-Package: org.eclipse.jgit.annotations;version="6.6.0", - org.eclipse.jgit.api;version="6.6.0"; +Export-Package: org.eclipse.jgit.annotations;version="7.4.0", + org.eclipse.jgit.api;version="7.4.0"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.notes, org.eclipse.jgit.dircache, @@ -23,72 +27,77 @@ Export-Package: org.eclipse.jgit.annotations;version="6.6.0", org.eclipse.jgit.revwalk.filter, org.eclipse.jgit.blame, org.eclipse.jgit.merge", - org.eclipse.jgit.api.errors;version="6.6.0"; + org.eclipse.jgit.api.errors;version="7.4.0"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.errors", - org.eclipse.jgit.attributes;version="6.6.0"; + org.eclipse.jgit.attributes;version="7.4.0"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.treewalk", - org.eclipse.jgit.blame;version="6.6.0"; + org.eclipse.jgit.blame;version="7.4.0"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, - org.eclipse.jgit.treewalk.filter, - org.eclipse.jgit.diff", - org.eclipse.jgit.diff;version="6.6.0"; + org.eclipse.jgit.blame.cache, + org.eclipse.jgit.diff, + org.eclipse.jgit.treewalk.filter", + org.eclipse.jgit.blame.cache;version="7.4.0"; + uses:="org.eclipse.jgit.lib", + org.eclipse.jgit.diff;version="7.4.0"; uses:="org.eclipse.jgit.lib, - org.eclipse.jgit.attributes, org.eclipse.jgit.revwalk, org.eclipse.jgit.patch, + org.eclipse.jgit.attributes, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.treewalk, org.eclipse.jgit.util", - org.eclipse.jgit.dircache;version="6.6.0"; + org.eclipse.jgit.dircache;version="7.4.0"; uses:="org.eclipse.jgit.events, org.eclipse.jgit.lib, org.eclipse.jgit.attributes, org.eclipse.jgit.treewalk, org.eclipse.jgit.util", - org.eclipse.jgit.errors;version="6.6.0"; + org.eclipse.jgit.errors;version="7.4.0"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.dircache, - org.eclipse.jgit.lib, - org.eclipse.jgit.internal.storage.pack", - org.eclipse.jgit.events;version="6.6.0"; + org.eclipse.jgit.lib", + org.eclipse.jgit.events;version="7.4.0"; uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.fnmatch;version="6.6.0", - org.eclipse.jgit.gitrepo;version="6.6.0"; + org.eclipse.jgit.fnmatch;version="7.4.0", + org.eclipse.jgit.gitrepo;version="7.4.0"; uses:="org.xml.sax.helpers, org.eclipse.jgit.api, + org.eclipse.jgit.api.errors, org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, org.xml.sax", - org.eclipse.jgit.gitrepo.internal;version="6.6.0";x-internal:=true, - org.eclipse.jgit.hooks;version="6.6.0";uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.ignore;version="6.6.0", - org.eclipse.jgit.ignore.internal;version="6.6.0"; + org.eclipse.jgit.gitrepo.internal;version="7.4.0";x-internal:=true, + org.eclipse.jgit.hooks;version="7.4.0"; + uses:="org.eclipse.jgit.lib, + org.eclipse.jgit.util", + org.eclipse.jgit.ignore;version="7.4.0", + org.eclipse.jgit.ignore.internal;version="7.4.0"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal;version="6.6.0"; + org.eclipse.jgit.internal;version="7.4.0"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.http.test", - org.eclipse.jgit.internal.diff;version="6.6.0"; + org.eclipse.jgit.internal.diff;version="7.4.0"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.diffmergetool;version="6.6.0"; + org.eclipse.jgit.internal.diffmergetool;version="7.4.0"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.pgm.test, org.eclipse.jgit.pgm, org.eclipse.egit.ui", - org.eclipse.jgit.internal.fsck;version="6.6.0"; + org.eclipse.jgit.internal.fsck;version="7.4.0"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.revwalk;version="6.6.0"; + org.eclipse.jgit.internal.revwalk;version="7.4.0"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.storage.commitgraph;version="6.6.0"; + org.eclipse.jgit.internal.storage.commitgraph;version="7.4.0"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.storage.dfs;version="6.6.0"; + org.eclipse.jgit.internal.storage.dfs;version="7.4.0"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.http.server, org.eclipse.jgit.http.test, org.eclipse.jgit.lfs.test", - org.eclipse.jgit.internal.storage.file;version="6.6.0"; + org.eclipse.jgit.internal.storage.file;version="7.4.0"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.junit, org.eclipse.jgit.junit.http, @@ -97,39 +106,43 @@ Export-Package: org.eclipse.jgit.annotations;version="6.6.0", org.eclipse.jgit.pgm, org.eclipse.jgit.pgm.test, org.eclipse.jgit.ssh.apache", - org.eclipse.jgit.internal.storage.io;version="6.6.0"; + org.eclipse.jgit.internal.storage.io;version="7.4.0"; x-friends:="org.eclipse.jgit.junit, org.eclipse.jgit.test, org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.memory;version="6.6.0"; + org.eclipse.jgit.internal.storage.memory;version="7.4.0"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.storage.pack;version="6.6.0"; + org.eclipse.jgit.internal.storage.midx;version="7.4.0";x-internal:=true, + org.eclipse.jgit.internal.storage.pack;version="7.4.0"; x-friends:="org.eclipse.jgit.junit, org.eclipse.jgit.test, org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.reftable;version="6.6.0"; + org.eclipse.jgit.internal.storage.reftable;version="7.4.0"; x-friends:="org.eclipse.jgit.http.test, org.eclipse.jgit.junit, org.eclipse.jgit.test, org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.submodule;version="6.6.0";x-internal:=true, - org.eclipse.jgit.internal.transport.connectivity;version="6.6.0"; + org.eclipse.jgit.internal.submodule;version="7.4.0";x-internal:=true, + org.eclipse.jgit.internal.transport.connectivity;version="7.4.0"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.transport.http;version="6.6.0"; + org.eclipse.jgit.internal.transport.http;version="7.4.0"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.transport.parser;version="6.6.0"; + org.eclipse.jgit.internal.transport.parser;version="7.4.0"; x-friends:="org.eclipse.jgit.http.server, org.eclipse.jgit.test", - org.eclipse.jgit.internal.transport.ssh;version="6.6.0"; + org.eclipse.jgit.internal.transport.ssh;version="7.4.0"; x-friends:="org.eclipse.jgit.ssh.apache, org.eclipse.jgit.ssh.jsch, org.eclipse.jgit.test", - org.eclipse.jgit.lib;version="6.6.0"; + org.eclipse.jgit.internal.util;version="7.4.0"; + x-friends:="org.eclipse.jgit.junit", + org.eclipse.jgit.lib;version="7.4.0"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.util.sha1, org.eclipse.jgit.dircache, org.eclipse.jgit.revwalk, org.eclipse.jgit.internal.storage.file, + org.eclipse.jgit.api, org.eclipse.jgit.attributes, org.eclipse.jgit.events, com.googlecode.javaewah, @@ -138,12 +151,12 @@ Export-Package: org.eclipse.jgit.annotations;version="6.6.0", org.eclipse.jgit.util, org.eclipse.jgit.submodule, org.eclipse.jgit.util.time", - org.eclipse.jgit.lib.internal;version="6.6.0"; + org.eclipse.jgit.lib.internal;version="7.4.0"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.pgm, org.eclipse.egit.ui", - org.eclipse.jgit.logging;version="6.6.0", - org.eclipse.jgit.merge;version="6.6.0"; + org.eclipse.jgit.logging;version="7.4.0", + org.eclipse.jgit.merge;version="7.4.0"; uses:="org.eclipse.jgit.dircache, org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, @@ -152,67 +165,69 @@ Export-Package: org.eclipse.jgit.annotations;version="6.6.0", org.eclipse.jgit.util, org.eclipse.jgit.api, org.eclipse.jgit.attributes", - org.eclipse.jgit.nls;version="6.6.0", - org.eclipse.jgit.notes;version="6.6.0"; + org.eclipse.jgit.nls;version="7.4.0", + org.eclipse.jgit.notes;version="7.4.0"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk, org.eclipse.jgit.merge", - org.eclipse.jgit.patch;version="6.6.0"; + org.eclipse.jgit.patch;version="7.4.0"; uses:="org.eclipse.jgit.lib, + org.eclipse.jgit.revwalk, org.eclipse.jgit.diff", - org.eclipse.jgit.revplot;version="6.6.0"; - uses:="org.eclipse.jgit.lib, - org.eclipse.jgit.revwalk", - org.eclipse.jgit.revwalk;version="6.6.0"; + org.eclipse.jgit.revplot;version="7.4.0"; + uses:="org.eclipse.jgit.revwalk, + org.eclipse.jgit.lib", + org.eclipse.jgit.revwalk;version="7.4.0"; uses:="org.eclipse.jgit.lib, + org.eclipse.jgit.revwalk.filter, org.eclipse.jgit.diff, org.eclipse.jgit.treewalk.filter, - org.eclipse.jgit.revwalk.filter, - org.eclipse.jgit.treewalk", - org.eclipse.jgit.revwalk.filter;version="6.6.0"; + org.eclipse.jgit.treewalk, + org.eclipse.jgit.internal.storage.commitgraph", + org.eclipse.jgit.revwalk.filter;version="7.4.0"; uses:="org.eclipse.jgit.revwalk, org.eclipse.jgit.lib, org.eclipse.jgit.util", - org.eclipse.jgit.storage.file;version="6.6.0"; + org.eclipse.jgit.storage.file;version="7.4.0"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.util", - org.eclipse.jgit.storage.pack;version="6.6.0"; + org.eclipse.jgit.storage.pack;version="7.4.0"; uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.submodule;version="6.6.0"; + org.eclipse.jgit.submodule;version="7.4.0"; uses:="org.eclipse.jgit.lib, - org.eclipse.jgit.diff, org.eclipse.jgit.treewalk.filter, + org.eclipse.jgit.diff, org.eclipse.jgit.treewalk, org.eclipse.jgit.util", - org.eclipse.jgit.transport;version="6.6.0"; + org.eclipse.jgit.transport;version="7.4.0"; uses:="javax.crypto, + org.eclipse.jgit.hooks, org.eclipse.jgit.util.io, org.eclipse.jgit.lib, - org.eclipse.jgit.revwalk, org.eclipse.jgit.transport.http, - org.eclipse.jgit.internal.storage.file, + org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk, org.eclipse.jgit.util, org.eclipse.jgit.internal.storage.pack, org.eclipse.jgit.transport.resolver, org.eclipse.jgit.storage.pack, org.eclipse.jgit.errors", - org.eclipse.jgit.transport.http;version="6.6.0"; + org.eclipse.jgit.transport.http;version="7.4.0"; uses:="javax.net.ssl", - org.eclipse.jgit.transport.resolver;version="6.6.0"; + org.eclipse.jgit.transport.resolver;version="7.4.0"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.lib", - org.eclipse.jgit.treewalk;version="6.6.0"; + org.eclipse.jgit.treewalk;version="7.4.0"; uses:="org.eclipse.jgit.dircache, org.eclipse.jgit.lib, org.eclipse.jgit.attributes, org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.util", - org.eclipse.jgit.treewalk.filter;version="6.6.0"; + org.eclipse.jgit.treewalk.filter;version="7.4.0"; uses:="org.eclipse.jgit.treewalk", - org.eclipse.jgit.util;version="6.6.0"; + org.eclipse.jgit.util;version="7.4.0"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.hooks, org.eclipse.jgit.revwalk, @@ -225,17 +240,17 @@ Export-Package: org.eclipse.jgit.annotations;version="6.6.0", org.eclipse.jgit.treewalk, javax.net.ssl, org.eclipse.jgit.util.time", - org.eclipse.jgit.util.io;version="6.6.0"; + org.eclipse.jgit.util.io;version="7.4.0"; uses:="org.eclipse.jgit.attributes, org.eclipse.jgit.lib, org.eclipse.jgit.treewalk", - org.eclipse.jgit.util.sha1;version="6.6.0", - org.eclipse.jgit.util.time;version="6.6.0" -Bundle-RequiredExecutionEnvironment: JavaSE-11 + org.eclipse.jgit.util.sha1;version="7.4.0", + org.eclipse.jgit.util.time;version="7.4.0" Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", javax.crypto, javax.management, javax.net.ssl, - org.slf4j;version="[1.7.0,2.0.0)", + org.apache.commons.codec.digest;version="[1.15.0,2.0.0)", + org.slf4j;version="[1.7.0,3.0.0)", org.xml.sax, org.xml.sax.helpers diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF index 407d45c748..780059761d 100644 --- a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF @@ -3,5 +3,6 @@ Bundle-ManifestVersion: 2 Bundle-Name: org.eclipse.jgit - Sources Bundle-SymbolicName: org.eclipse.jgit.source Bundle-Vendor: Eclipse.org - JGit -Bundle-Version: 6.6.0.qualifier -Eclipse-SourceBundle: org.eclipse.jgit;version="6.6.0.qualifier";roots="." +Bundle-Version: 7.4.0.qualifier +Bundle-SCM: url=https://github.com/eclipse-jgit/jgit, connection=scm:git:https://eclipse.gerrithub.io/eclipse-jgit/jgit.git, developerConnection=scm:git:https://eclipse.gerrithub.io/a/eclipse-jgit/jgit.git +Eclipse-SourceBundle: org.eclipse.jgit;version="7.4.0.qualifier";roots="." diff --git a/org.eclipse.jgit/plugin.properties b/org.eclipse.jgit/OSGI-INF/l10n/plugin.properties index 3e132b01ca..3e132b01ca 100644 --- a/org.eclipse.jgit/plugin.properties +++ b/org.eclipse.jgit/OSGI-INF/l10n/plugin.properties diff --git a/org.eclipse.jgit/OSGI-INF/org.eclipse.jgit.internal.util.CleanupService.xml b/org.eclipse.jgit/OSGI-INF/org.eclipse.jgit.internal.util.CleanupService.xml new file mode 100644 index 0000000000..8d97374c66 --- /dev/null +++ b/org.eclipse.jgit/OSGI-INF/org.eclipse.jgit.internal.util.CleanupService.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="start" deactivate="shutDown" name="org.eclipse.jgit.internal.util.CleanupService"> + <implementation class="org.eclipse.jgit.internal.util.CleanupService"/> +</scr:component>
\ No newline at end of file diff --git a/org.eclipse.jgit/build.properties b/org.eclipse.jgit/build.properties index 0b8f825bf0..e29b88faa0 100644 --- a/org.eclipse.jgit/build.properties +++ b/org.eclipse.jgit/build.properties @@ -2,6 +2,6 @@ source.. = src/,\ resources/ output.. = bin/ bin.includes = META-INF/,\ + OSGI-INF/,\ .,\ - about.html,\ - plugin.properties
\ No newline at end of file + about.html diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml index 100a2ff0ca..ec3f314bef 100644 --- a/org.eclipse.jgit/pom.xml +++ b/org.eclipse.jgit/pom.xml @@ -20,7 +20,7 @@ <parent> <groupId>org.eclipse.jgit</groupId> <artifactId>org.eclipse.jgit-parent</artifactId> - <version>6.6.0-SNAPSHOT</version> + <version>7.4.0-SNAPSHOT</version> </parent> <artifactId>org.eclipse.jgit</artifactId> @@ -46,6 +46,11 @@ <artifactId>slf4j-api</artifactId> </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + </dependency> + </dependencies> <build> @@ -77,7 +82,7 @@ <target> <copy file="META-INF/SOURCE-MANIFEST.MF" tofile="${source-bundle-manifest}" overwrite="true"/> <replace file="${source-bundle-manifest}"> - <replacefilter token=".qualifier" value=".${maven.build.timestamp}"/> + <replacefilter token=".qualifier" value=".${commit.time.version}"/> </replace> </target> </configuration> diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index bc8144f1c9..e24cba6ac4 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -20,6 +20,8 @@ applyBinaryPatchTypeNotSupported=Couldn't apply binary patch of type {0} applyTextPatchCannotApplyHunk=Hunk cannot be applied applyTextPatchSingleClearingHunk=Expected a single hunk for clearing all content applyBinaryResultOidWrong=Result of binary patch for file {0} has wrong OID +applyPatchDestInvalid=Destination path in patch is invalid +applyPatchSourceInvalid==Source path in patch is invalid applyPatchWithoutSourceOnAlreadyExistingSource=Cannot perform {0} action on an existing file applyPatchWithCreationOverAlreadyExistingDestination=Cannot perform {0} action which overrides an existing file applyPatchWithSourceOnNonExistentSource=Cannot perform {0} action on a non-existent file @@ -64,6 +66,7 @@ binaryHunkLineTooShort=Binary hunk, line {0}: input ended prematurely binaryHunkMissingNewline=Binary hunk, line {0}: input line not terminated by newline bitmapMissingObject=Bitmap at {0} is missing {1}. bitmapsMustBePrepared=Bitmaps must be prepared before they may be written. +bitmapUseNoopNoListener=Use NOOP instance for no listener blameNotCommittedYet=Not Committed Yet blockLimitNotMultipleOfBlockSize=blockLimit {0} must be a multiple of blockSize {1} blockLimitNotPositive=blockLimit must be positive: {0} @@ -73,6 +76,7 @@ branchNameInvalid=Branch name {0} is not allowed buildingBitmaps=Building bitmaps cachedPacksPreventsIndexCreation=Using cached packs prevents index creation cachedPacksPreventsListingObjects=Using cached packs prevents listing objects +cacheRegionAllOrNoneNull=expected all null or none: {0}, {1} cannotAccessLastModifiedForSafeDeletion=Unable to access lastModifiedTime of file {0}, skip deletion since we cannot safely avoid race condition cannotBeCombined=Cannot be combined. cannotBeRecursiveWhenTreesAreIncluded=TreeWalk shouldn't be recursive when tree objects are included. @@ -156,6 +160,7 @@ commitGraphChunkNeeded=commit-graph 0x{0} chunk has not been loaded commitGraphChunkRepeated=commit-graph chunk id 0x{0} appears multiple times commitGraphChunkUnknown=unknown commit-graph chunk: 0x{0} commitGraphFileIsTooLargeForJgit=commit-graph file is too large for jgit +commitGraphUnexpectedSize=Commit-graph: expected {0} bytes but out has {1} bytes commitGraphWritingCancelled=commit-graph writing was canceled commitMessageNotSpecified=commit message not specified commitOnRepoWithoutHEADCurrentlyNotSupported=Commit on repo without HEAD currently not supported @@ -163,6 +168,7 @@ commitAmendOnInitialNotPossible=Amending is not possible on initial commit. commitsHaveAlreadyBeenMarkedAsStart=Commits have already been marked as walk starts. compressingObjects=Compressing objects computingCommitGeneration=Computing commit-graph generation numbers +computingPathBloomFilters=Computing commit-graph path bloom filters configSubsectionContainsNewline=config subsection name contains newline configSubsectionContainsNullByte=config subsection name contains byte 0x00 configValueContainsNullByte=config value contains byte 0x00 @@ -223,6 +229,7 @@ corruptObjectTruncatedInMode=truncated in mode corruptObjectTruncatedInName=truncated in name corruptObjectTruncatedInObjectId=truncated in object id corruptObjectZeroId=entry points to null SHA-1 +corruptReverseIndexChecksumIncorrect=Reverse index checksum incorrect: written as {0} but digest was {1} corruptUseCnt=close() called when useCnt is already zero for {0} couldNotGetAdvertisedRef=Remote {0} did not advertise Ref for branch {1}. This Ref may not exist in the remote or may be hidden by permission settings. couldNotGetRepoStatistics=Could not get repository statistics @@ -255,9 +262,11 @@ deleteFileFailed=Could not delete file {0} deletedOrphanInPackDir=Deleted orphaned file {} deleteRequiresZeroNewId=Delete requires new ID to be zero deleteTagUnexpectedResult=Delete tag returned unexpected result {0} +deletingBranches=Deleting branches... deletingNotSupported=Deleting {0} not supported. depthMustBeAt1=Depth must be >= 1 depthWithUnshallow=Depth and unshallow can\'t be used together +deprecatedTrustFolderStat=Option core.trustFolderStat is deprecated, replace it by core.trustStat. destinationIsNotAWildcard=Destination is not a wildcard. detachedHeadDetected=HEAD is detached diffToolNotGivenError=No diff tool provided and no defaults configured. @@ -276,6 +285,9 @@ DIRCUnrecognizedExtendedFlags=Unrecognized extended flags: {0} downloadCancelled=Download cancelled downloadCancelledDuringIndexing=Download cancelled during indexing duplicateAdvertisementsOf=duplicate advertisements of {0} +duplicateCacheTablesGiven=Duplicate cache tables given +duplicatePackExtensionsForCacheTables=Duplicate pack extension {0} in cache tables +duplicatePackExtensionsSet=Attempting to configure duplicate pack extensions: {0}.{1}.{2} contains {3} duplicateRef=Duplicate ref: {0} duplicateRefAttribute=Duplicate ref attribute: {0} duplicateRemoteRefUpdateIsIllegal=Duplicate remote ref update is illegal. Affected remote name: {0} @@ -385,6 +397,7 @@ initFailedGitDirIsNoDirectory=Cannot set git-dir to ''{0}'' which is not a direc initFailedNonBareRepoSameDirs=When initializing a non-bare repo with directory {0} and separate git-dir {1} specified both folders should not point to the same location inMemoryBufferLimitExceeded=In-memory buffer limit exceeded inputDidntMatchLength=Input did not match supplied length. {0} bytes are missing. +inputStreamClosed=InputStream was closed inputStreamMustSupportMark=InputStream must support mark() integerValueNotInRange=Integer value {0}.{1} = {2} not in range {3}..{4} integerValueNotInRangeSubSection=Integer value {0}.{1}.{2} = {3} not in range {4}..{5} @@ -451,6 +464,7 @@ invalidTimestamp=Invalid timestamp in {0} invalidTimeUnitValue2=Invalid time unit value: {0}.{1}={2} invalidTimeUnitValue3=Invalid time unit value: {0}.{1}.{2}={3} invalidTreeZeroLengthName=Cannot append a tree entry with zero-length name +invalidTrustStat=core.trustStat must not be set to TrustStat.INHERIT, falling back to TrustStat.ALWAYS. invalidURL=Invalid URL {0} invalidWildcards=Invalid wildcards {0} invalidRefSpec=Invalid refspec {0} @@ -482,6 +496,7 @@ logInvalidDefaultCharset=System property "native.encoding" specifies unknown cha logLargerFiletimeDiff={}: inconsistent duration from file timestamps on {}, {}: diff = {} > {} (last good value). Aborting measurement. logSmallerFiletime={}: got smaller file timestamp on {}, {}: {} < {}. Aborting measurement at resolution {}. logXDGConfigHomeInvalid=Environment variable XDG_CONFIG_HOME contains an invalid path {} +logXDGCacheHomeInvalid=Environment variable XDG_CACHE_HOME contains an invalid path {} looseObjectHandleIsStale=loose-object {0} file handle is stale. retry {1} of {2} maxCountMustBeNonNegative=max count must be >= 0 mergeConflictOnNonNoteEntries=Merge conflict on non-note entries: base = {0}, ours = {1}, theirs = {2} @@ -489,11 +504,14 @@ mergeConflictOnNotes=Merge conflict on note {0}. base = {1}, ours = {2}, theirs mergeStrategyAlreadyExistsAsDefault=Merge strategy "{0}" already exists as a default strategy mergeStrategyDoesNotSupportHeads=merge strategy {0} does not support {1} heads to be merged into HEAD mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4} -mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1} +mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1}\nFailing paths: {2} mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n count {3}" mergeToolNotGivenError=No merge tool provided and no defaults configured. mergeToolNullError=Parameter for merge tool cannot be null. messageAndTaggerNotAllowedInUnannotatedTags = Unannotated tags cannot have a message or tagger +midxChunkNeeded=midx 0x{0} chunk has not been loaded +midxChunkRepeated=midx chunk id 0x{0} appears multiple times +midxChunkUnknown=unknown midx chunk: 0x{0} minutesAgo={0} minutes ago mismatchOffset=mismatch offset for object {0} mismatchCRC=mismatch CRC for object {0} @@ -514,6 +532,10 @@ mkDirsFailed=Creating directories for {0} failed month=month months=months monthsAgo={0} months ago +multiPackIndexFileIsTooLargeForJgit=Multipack index file is too large for jgit +multiPackIndexPackCountMismatch=Multipack index: header mentions {0} packs but packfile names chunk has {1} +multiPackIndexUnexpectedSize=MultiPack index: expected {0} bytes but out has {1} bytes +multiPackIndexWritingCancelled=Multipack index writing was canceled multipleMergeBasesFor=Multiple merge bases for:\n {0}\n {1} found:\n {2}\n {3} nameMustNotBeNullOrEmpty=Ref name must not be null or empty. need2Arguments=Need 2 arguments @@ -529,6 +551,8 @@ noMergeBase=No merge base could be determined. Reason={0}. {1} noMergeHeadSpecified=No merge head specified nonBareLinkFilesNotSupported=Link files are not supported with nonbare repos nonCommitToHeads=Cannot point a branch to a non-commit object +noPackExtConfigurationGiven=No PackExt configuration given +noPackExtGivenForConfiguration=No PackExt given for configuration noPathAttributesFound=No Attributes found for {0}. noSuchRef=no such ref noSuchRefKnown=no such ref: {0} @@ -539,6 +563,7 @@ notACommitGraph=not a commit-graph notADIRCFile=Not a DIRC file. notAGitDirectory=not a git directory notAPACKFile=Not a PACK file. +notAMIDX=not a multi-pack-index notARef=Not a ref: {0}: {1} notASCIIString=Not ASCII string: {0} notAuthorized=not authorized @@ -549,6 +574,7 @@ nothingToPush=Nothing to push. notMergedExceptionMessage=Branch was not deleted as it has not been merged yet; use the force option to delete it anyway notShallowedUnshallow=The server sent a unshallow for a commit that wasn''t marked as shallow: {0} noXMLParserAvailable=No XML parser available. +nullRevCommit=RevCommit is null numberDoesntFit=Number doesn't fit in a single byte objectAtHasBadZlibStream=Object at {0} in {1} has bad zlib stream objectIsCorrupt=Object {0} is corrupt: {1} @@ -560,13 +586,12 @@ obtainingCommitsForCherryPick=Obtaining commits that need to be cherry-picked oldIdMustNotBeNull=Expected old ID must not be null onlyOneFetchSupported=Only one fetch supported onlyOneOperationCallPerConnectionIsSupported=Only one operation call per connection is supported. -onlyOpenPgpSupportedForSigning=OpenPGP is the only supported signing option with JGit at this time (gpg.format must be set to openpgp). openFilesMustBeAtLeast1=Open files must be >= 1 openingConnection=Opening connection operationCanceled=Operation {0} was canceled outputHasAlreadyBeenStarted=Output has already been started. overflowedReftableBlock=Overflowed reftable block -packChecksumMismatch=Pack checksum mismatch detected for pack file {0}: .pack has {1} whilst .idx has {2} +packChecksumMismatch=Pack checksum mismatch detected for pack file {0}: {1} has {2} whilst {3} has {4} packCorruptedWhileWritingToFilesystem=Pack corrupted while writing to filesystem packedRefsHandleIsStale=packed-refs handle is stale, {0}. retry packetSizeMustBeAtLeast=packet size {0} must be >= {1} @@ -582,6 +607,8 @@ packInaccessible=Failed to access pack file {0}, caught {1} consecutive errors w packingCancelledDuringObjectsWriting=Packing cancelled during objects writing packObjectCountMismatch=Pack object count mismatch: pack {0} index {1}: {2} packRefs=Pack refs +packRefsFailed=Packing refs failed +packRefsSuccessful=Packed refs successfully packSizeNotSetYet=Pack size not yet set since it has not yet been received packTooLargeForIndexVersion1=Pack too large for index version 1 packWasDeleted=Pack file {0} was deleted, removing it from pack list @@ -598,6 +625,7 @@ peerDidNotSupplyACompleteObjectGraph=peer did not supply a complete object graph personIdentEmailNonNull=E-mail address of PersonIdent must not be null. personIdentNameNonNull=Name of PersonIdent must not be null. postCommitHookFailed=Execution of post-commit hook failed: {0}. +precedenceTrustConfig=Both core.trustFolderStat and core.trustStat are set, ignoring trustFolderStat since trustStat takes precedence. Remove core.trustFolderStat from your configuration. prefixRemote=remote: problemWithResolvingPushRefSpecsLocally=Problem with resolving push ref specs locally: {0} progressMonUploading=Uploading {0} @@ -625,8 +653,6 @@ readFileStoreAttributesFailed=Reading FileStore attributes from user config fail readerIsRequired=Reader is required readingObjectsFromLocalRepositoryFailed=reading objects from local repository failed: {0} readLastModifiedFailed=Reading lastModified of {0} failed -readPipeIsNotAllowed=FS.readPipe() isn't allowed for command ''{0}''. Working directory: ''{1}''. -readPipeIsNotAllowedRequiredPermission=FS.readPipe() isn't allowed for command ''{0}''. Working directory: ''{1}''. Required permission: {2}. readTimedOut=Read timed out after {0} ms receivePackObjectTooLarge1=Object too large, rejecting the pack. Max object size limit is {0} bytes. receivePackObjectTooLarge2=Object too large ({0} bytes), rejecting the pack. Max object size limit is {1} bytes. @@ -689,6 +715,7 @@ searchForReachableBranches=Finding reachable branches saveFileStoreAttributesFailed=Saving measured FileStore attributes to user config failed searchForReuse=Finding sources searchForReuseTimeout=Search for reuse timed out after {0} seconds +unsupportedObjectIdVersion=Object id version {0} is not supported searchForSizes=Getting sizes secondsAgo={0} seconds ago selectingCommits=Selecting commits @@ -703,6 +730,11 @@ shortCompressedStreamAt=Short compressed stream at {0} shortReadOfBlock=Short read of block. shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section. shortSkipOfBlock=Short skip of block. +shutdownCleanup=Cleanup {} during JVM shutdown +shutdownCleanupFailed=Cleanup during JVM shutdown failed +shutdownCleanupListenerFailed=Cleanup of {0} during JVM shutdown failed +signatureServiceConflict={0} conflict for type {1}. Already registered is {2}; additional factory {3} is ignored. +signatureTypeUnknown=No signer for {0} signatures. Use another signature type for git config gpg.format, or do not sign. signatureVerificationError=Signature verification failed signatureVerificationUnavailable=No signature verifier registered signedTagMessageNoLf=A non-empty message of a signed tag must end in LF. @@ -788,6 +820,7 @@ truncatedHunkOldLinesMissing=Truncated hunk, at least {0} old lines is missing tSizeMustBeGreaterOrEqual1=tSize must be >= 1 unableToCheckConnectivity=Unable to check connectivity. unableToCreateNewObject=Unable to create new object: {0} +unableToReadFullArray=Unable to read an array with {0} elements from the stream unableToReadFullInt=Unable to read a full int from the stream unableToReadPackfile=Unable to read packfile {0} unableToRemovePath=Unable to remove path ''{0}'' @@ -814,7 +847,8 @@ unknownObject=unknown object unknownObjectInIndex=unknown object {0} found in index but not in pack file unknownObjectType=Unknown object type {0}. unknownObjectType2=unknown -unknownPositionEncoding=Unknown position encoding %s +unknownPackExtension=Unknown pack extension: {0}.{1}.{2}={3} +unknownPositionEncoding=Unknown position encoding {0} unknownRefStorageFormat=Unknown ref storage format "{0}" unknownRepositoryFormat=Unknown repository format unknownRepositoryFormat2=Unknown repository format "{0}"; expected "0". @@ -825,6 +859,8 @@ unmergedPath=Unmerged path: {0} unmergedPaths=Repository contains unmerged paths unpackException=Exception while parsing pack stream unreadableCommitGraph=Unreadable commit-graph: {0} +unreadableMIDX=Unreadable multi-pack-index: {0} +unreadableObjectSizeIndex=Unreadable object size index. First {0} bytes are ''{1}'' unreadablePackIndex=Unreadable pack index: {0} unrecognizedPackExtension=Unrecognized pack extension: {0} unrecognizedRef=Unrecognized ref: {0} @@ -837,6 +873,8 @@ unsupportedEncryptionAlgorithm=Unsupported encryption algorithm: {0} unsupportedEncryptionVersion=Unsupported encryption version: {0} unsupportedGC=Unsupported garbage collector for repository type: {0} unsupportedMark=Mark not supported +unsupportedMIDXVersion=Unsupported MIDX version {0} +unsupportedObjectSizeIndexVersion=Unsupported object size index version {0} unsupportedOperationNotAddAtEnd=Not add-at-end: {0} unsupportedPackIndexVersion=Unsupported pack index version {0} unsupportedPackReverseIndexVersion=Unsupported pack reverse index version {0} @@ -845,6 +883,7 @@ unsupportedReftableVersion=Unsupported reftable version {0}. unsupportedRepositoryDescription=Repository description not supported unsupportedSizesObjSizeIndex=Unsupported sizes in object-size-index updateRequiresOldIdAndNewId=Update requires both old ID and new ID to be nonzero +updatingConfig=Updating git config updatingHeadFailed=Updating HEAD failed updatingReferences=Updating references updatingRefFailed=Updating the ref {0} to {1} failed. ReturnCode from RefUpdate.update() was {2} @@ -873,7 +912,7 @@ writerAlreadyInitialized=Writer already initialized writeTimedOut=Write timed out after {0} ms writingNotPermitted=Writing not permitted writingNotSupported=Writing {0} not supported. -writingOutCommitGraph=Writing out commit-graph in {0} passes +writingOutCommitGraph=Writing out commit-graph writingObjects=Writing objects wrongDecompressedLength=wrong decompressed length wrongRepositoryState=Wrong Repository State: {0} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java index cb32324043..b4d1cab513 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> - * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com> and others + * Copyright (C) 2010, 2025 Stefan Lay <stefan.lay@sap.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -17,9 +17,10 @@ import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; import java.io.IOException; import java.io.InputStream; +import java.text.MessageFormat; import java.time.Instant; -import java.util.Collection; -import java.util.LinkedList; +import java.util.ArrayList; +import java.util.List; import org.eclipse.jgit.api.errors.FilterFailedException; import org.eclipse.jgit.api.errors.GitAPIException; @@ -55,12 +56,19 @@ import org.eclipse.jgit.treewalk.filter.TreeFilter; */ public class AddCommand extends GitCommand<DirCache> { - private Collection<String> filepatterns; + private List<String> filepatterns; private WorkingTreeIterator workingTreeIterator; + // Update only known index entries, don't add new ones. If there's no file + // for an index entry, remove it: stage deletions. private boolean update = false; + // If TRUE, also stage deletions, otherwise only update and add index + // entries. + // If not set explicitly + private Boolean all; + // This defaults to true because it's what JGit has been doing // traditionally. The C git default would be false. private boolean renormalize = true; @@ -73,7 +81,7 @@ public class AddCommand extends GitCommand<DirCache> { */ public AddCommand(Repository repo) { super(repo); - filepatterns = new LinkedList<>(); + filepatterns = new ArrayList<>(); } /** @@ -82,6 +90,17 @@ public class AddCommand extends GitCommand<DirCache> { * A directory name (e.g. <code>dir</code> to add <code>dir/file1</code> and * <code>dir/file2</code>) can also be given to add all files in the * directory, recursively. Fileglobs (e.g. *.c) are not yet supported. + * </p> + * <p> + * If a pattern {@code "."} is added, all changes in the git repository's + * working tree will be added. + * </p> + * <p> + * File patterns are required unless {@code isUpdate() == true} or + * {@link #setAll(boolean)} is called. If so and no file patterns are given, + * all changes will be added (i.e., a file pattern of {@code "."} is + * implied). + * </p> * * @param filepattern * repository-relative path of file/directory to add (with @@ -113,15 +132,41 @@ public class AddCommand extends GitCommand<DirCache> { * Executes the {@code Add} command. Each instance of this class should only * be used for one invocation of the command. Don't call this method twice * on an instance. + * </p> + * + * @throws JGitInternalException + * on errors, but also if {@code isUpdate() == true} _and_ + * {@link #setAll(boolean)} had been called + * @throws NoFilepatternException + * if no file patterns are given if {@code isUpdate() == false} + * and {@link #setAll(boolean)} was not called */ @Override public DirCache call() throws GitAPIException, NoFilepatternException { - - if (filepatterns.isEmpty()) - throw new NoFilepatternException(JGitText.get().atLeastOnePatternIsRequired); checkCallable(); + + if (update && all != null) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().illegalCombinationOfArguments, + "--update", "--all/--no-all")); //$NON-NLS-1$ //$NON-NLS-2$ + } + boolean addAll; + if (filepatterns.isEmpty()) { + if (update || all != null) { + addAll = true; + } else { + throw new NoFilepatternException( + JGitText.get().atLeastOnePatternIsRequired); + } + } else { + addAll = filepatterns.contains("."); //$NON-NLS-1$ + if (all == null && !update) { + all = Boolean.TRUE; + } + } + boolean stageDeletions = update || (all != null && all.booleanValue()); + DirCache dc = null; - boolean addAll = filepatterns.contains("."); //$NON-NLS-1$ try (ObjectInserter inserter = repo.newObjectInserter(); NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) { @@ -181,7 +226,8 @@ public class AddCommand extends GitCommand<DirCache> { if (f == null) { // working tree file does not exist if (entry != null - && (!update || GITLINK == entry.getFileMode())) { + && (!stageDeletions + || GITLINK == entry.getFileMode())) { builder.add(entry); } continue; @@ -252,7 +298,8 @@ public class AddCommand extends GitCommand<DirCache> { } /** - * Set whether to only match against already tracked files + * Set whether to only match against already tracked files. If + * {@code update == true}, re-sets a previous {@link #setAll(boolean)}. * * @param update * If set to true, the command only matches {@code filepattern} @@ -314,4 +361,32 @@ public class AddCommand extends GitCommand<DirCache> { public boolean isRenormalize() { return renormalize; } + + /** + * Defines whether the command will use '--all' mode: update existing index + * entries, add new entries, and remove index entries for which there is no + * file. (In other words: also stage deletions.) + * <p> + * The setting is independent of {@link #setUpdate(boolean)}. + * </p> + * + * @param all + * whether to enable '--all' mode + * @return {@code this} + * @since 7.2 + */ + public AddCommand setAll(boolean all) { + this.all = Boolean.valueOf(all); + return this; + } + + /** + * Tells whether '--all' has been set for this command. + * + * @return {@code true} if it was set; {@code false} otherwise + * @since 7.2 + */ + public boolean isAll() { + return all != null && all.booleanValue(); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddNoteCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddNoteCommand.java index ceb17fbe25..8805ea2353 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddNoteCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddNoteCommand.java @@ -51,7 +51,6 @@ public class AddNoteCommand extends GitCommand<Note> { super(repo); } - /** {@inheritDoc} */ @Override public Note call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java index e612924771..df0f616256 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java @@ -37,11 +37,12 @@ public class ApplyCommand extends GitCommand<ApplyResult> { /** * Constructs the command. * - * @param local + * @param repo + * the repository this command will be used on */ - ApplyCommand(Repository local) { - super(local); - if (local == null) { + ApplyCommand(Repository repo) { + super(repo); + if (repo == null) { throw new NullPointerException(JGitText.get().repositoryIsRequired); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java index fdf8b80cd4..81b3611774 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java @@ -177,6 +177,8 @@ public class ArchiveCommand extends GitCommand<OutputStream> { } /** + * Get the problematic format name + * * @return the problematic format name */ public String getFormat() { @@ -389,7 +391,6 @@ public class ArchiveCommand extends GitCommand<OutputStream> { } } - /** {@inheritDoc} */ @Override public OutputStream call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java index 7319ff4b2f..32c242f271 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> - * Copyright (C) 2011, 2020 Matthias Sohn <matthias.sohn@sap.com> and others + * Copyright (C) 2011, 2023 Matthias Sohn <matthias.sohn@sap.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -17,7 +17,6 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -28,6 +27,7 @@ import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.RefAlreadyExistsException; import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.dircache.Checkout; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; @@ -55,7 +55,6 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; /** @@ -164,10 +163,9 @@ public class CheckoutCommand extends GitCommand<Ref> { */ protected CheckoutCommand(Repository repo) { super(repo); - this.paths = new LinkedList<>(); + this.paths = new ArrayList<>(); } - /** {@inheritDoc} */ @Override public Ref call() throws GitAPIException, RefAlreadyExistsException, RefNotFoundException, InvalidRefNameException, @@ -326,6 +324,8 @@ public class CheckoutCommand extends GitCommand<Ref> { } /** + * Set progress monitor + * * @param monitor * a progress monitor * @return this instance @@ -407,13 +407,14 @@ public class CheckoutCommand extends GitCommand<Ref> { * * @return this instance * @throws java.io.IOException + * if an IO error occurred * @throws org.eclipse.jgit.api.errors.RefNotFoundException + * if {@code Ref} couldn't be resolved */ protected CheckoutCommand checkoutPaths() throws IOException, RefNotFoundException { actuallyModifiedPaths = new HashSet<>(); - WorkingTreeOptions options = repo.getConfig() - .get(WorkingTreeOptions.KEY); + Checkout checkout = new Checkout(repo).setRecursiveDeletion(true); DirCache dc = repo.lockDirCache(); try (RevWalk revWalk = new RevWalk(repo); TreeWalk treeWalk = new TreeWalk(repo, @@ -422,10 +423,10 @@ public class CheckoutCommand extends GitCommand<Ref> { if (!checkoutAllPaths) treeWalk.setFilter(PathFilterGroup.createFromStrings(paths)); if (isCheckoutIndex()) - checkoutPathsFromIndex(treeWalk, dc, options); + checkoutPathsFromIndex(treeWalk, dc, checkout); else { RevCommit commit = revWalk.parseCommit(getStartPointObjectId()); - checkoutPathsFromCommit(treeWalk, dc, commit, options); + checkoutPathsFromCommit(treeWalk, dc, commit, checkout); } } finally { try { @@ -443,7 +444,7 @@ public class CheckoutCommand extends GitCommand<Ref> { } private void checkoutPathsFromIndex(TreeWalk treeWalk, DirCache dc, - WorkingTreeOptions options) + Checkout checkout) throws IOException { DirCacheIterator dci = new DirCacheIterator(dc); treeWalk.addTree(dci); @@ -469,7 +470,7 @@ public class CheckoutCommand extends GitCommand<Ref> { if (stage > DirCacheEntry.STAGE_0) { if (checkoutStage != null) { if (stage == checkoutStage.number) { - checkoutPath(ent, r, options, + checkoutPath(ent, r, checkout, path, new CheckoutMetadata(eolStreamType, filterCommand)); actuallyModifiedPaths.add(path); @@ -480,7 +481,7 @@ public class CheckoutCommand extends GitCommand<Ref> { throw new JGitInternalException(e.getMessage(), e); } } else { - checkoutPath(ent, r, options, + checkoutPath(ent, r, checkout, path, new CheckoutMetadata(eolStreamType, filterCommand)); actuallyModifiedPaths.add(path); @@ -494,7 +495,7 @@ public class CheckoutCommand extends GitCommand<Ref> { } private void checkoutPathsFromCommit(TreeWalk treeWalk, DirCache dc, - RevCommit commit, WorkingTreeOptions options) throws IOException { + RevCommit commit, Checkout checkout) throws IOException { treeWalk.addTree(commit.getTree()); final ObjectReader r = treeWalk.getObjectReader(); DirCacheEditor editor = dc.editor(); @@ -516,7 +517,7 @@ public class CheckoutCommand extends GitCommand<Ref> { } ent.setObjectId(blobId); ent.setFileMode(mode); - checkoutPath(ent, r, options, + checkoutPath(ent, r, checkout, path, new CheckoutMetadata(eolStreamType, filterCommand)); actuallyModifiedPaths.add(path); } @@ -526,10 +527,9 @@ public class CheckoutCommand extends GitCommand<Ref> { } private void checkoutPath(DirCacheEntry entry, ObjectReader reader, - WorkingTreeOptions options, CheckoutMetadata checkoutMetadata) { + Checkout checkout, String path, CheckoutMetadata checkoutMetadata) { try { - DirCacheCheckout.checkoutEntry(repo, entry, reader, true, - checkoutMetadata, options); + checkout.checkout(entry, checkoutMetadata, reader, path); } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().checkoutConflictWithFile, @@ -644,24 +644,6 @@ public class CheckoutCommand extends GitCommand<Ref> { /** * Specify to force the ref update in case of a branch switch. * - * @param force - * if <code>true</code> and the branch with the given name - * already exists, the start-point of an existing branch will be - * set to a new start-point; if false, the existing branch will - * not be changed - * @return this instance - * @deprecated this method was badly named comparing its semantics to native - * git's checkout --force option, use - * {@link #setForceRefUpdate(boolean)} instead - */ - @Deprecated - public CheckoutCommand setForce(boolean force) { - return setForceRefUpdate(force); - } - - /** - * Specify to force the ref update in case of a branch switch. - * * In releases prior to 5.2 this method was called setForce() but this name * was misunderstood to implement native git's --force option, which is not * true. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java index 5f8c2b728a..a353d1a135 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java @@ -9,11 +9,12 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.api.CherryPickCommitMessageProvider.ORIGINAL; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; import java.io.IOException; import java.text.MessageFormat; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -62,10 +63,12 @@ import org.eclipse.jgit.treewalk.FileTreeIterator; public class CherryPickCommand extends GitCommand<CherryPickResult> { private String reflogPrefix = "cherry-pick:"; //$NON-NLS-1$ - private List<Ref> commits = new LinkedList<>(); + private List<Ref> commits = new ArrayList<>(); private String ourCommitName = null; + private CherryPickCommitMessageProvider messageProvider = ORIGINAL; + private MergeStrategy strategy = MergeStrategy.RECURSIVE; private ContentMergeStrategy contentStrategy; @@ -99,7 +102,7 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { UnmergedPathsException, ConcurrentRefUpdateException, WrongRepositoryStateException, NoHeadException { RevCommit newHead = null; - List<Ref> cherryPickedRefs = new LinkedList<>(); + List<Ref> cherryPickedRefs = new ArrayList<>(); checkCallable(); try (RevWalk revWalk = new RevWalk(repo)) { @@ -168,8 +171,10 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { dco.checkout(); if (!noCommit) { try (Git git = new Git(getRepository())) { + String commitMessage = messageProvider + .getCherryPickedCommitMessage(srcCommit); newHead = git.commit() - .setMessage(srcCommit.getFullMessage()) + .setMessage(commitMessage) .setReflogComment(reflogPrefix + " " //$NON-NLS-1$ + srcCommit.getShortMessage()) .setAuthor(srcCommit.getAuthorIdent()) @@ -297,6 +302,22 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { } /** + * Set a message provider for a target cherry-picked commit<br> + * By default original commit message is used (see + * {@link CherryPickCommitMessageProvider#ORIGINAL}) + * + * @param messageProvider + * the commit message provider + * @return {@code this} + * @since 6.9 + */ + public CherryPickCommand setCherryPickCommitMessageProvider( + CherryPickCommitMessageProvider messageProvider) { + this.messageProvider = messageProvider; + return this; + } + + /** * Set the prefix to use in the reflog. * <p> * This is primarily needed for implementing rebase in terms of @@ -399,7 +420,6 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { return headName; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommitMessageProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommitMessageProvider.java new file mode 100644 index 0000000000..50d65b6fa8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommitMessageProvider.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 Dmitrii Naumenko <dmitrii.naumenko@jetbrains.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is aailable at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.api; + +import java.util.List; + +import org.eclipse.jgit.revwalk.FooterLine; +import org.eclipse.jgit.revwalk.RevCommit; + +/** + * The interface is used to construct a cherry-picked commit message based on + * the original commit + * + * @see #ORIGINAL + * @see #ORIGINAL_WITH_REFERENCE + * @since 6.9 + */ +public interface CherryPickCommitMessageProvider { + + /** + * This provider returns the original commit message + */ + static final CherryPickCommitMessageProvider ORIGINAL = RevCommit::getFullMessage; + + /** + * This provider returns the original commit message with original commit + * hash in SHA-1 form.<br> + * Example: + * + * <pre> + * <code>my original commit message + * + * (cherry picked from commit 75355897dc28e9975afed028c1a6d8c6b97b2a3c)</code> + * </pre> + * + * This is similar to <code>-x</code> flag in git-scm (see <a href= + * "https://git-scm.com/docs/git-cherry-pick#_options">https://git-scm.com/docs/git-cherry-pick#_options</a>) + */ + static final CherryPickCommitMessageProvider ORIGINAL_WITH_REFERENCE = srcCommit -> { + String fullMessage = srcCommit.getFullMessage(); + + // Don't add extra new line after footer (aka trailer) + // https://stackoverflow.com/questions/70007405/git-log-exclude-cherry-pick-messages-for-trailers + // https://lore.kernel.org/git/7vmx136cdc.fsf@alter.siamese.dyndns.org + String separator = messageEndsWithFooter(srcCommit) ? "\n" : "\n\n"; //$NON-NLS-1$//$NON-NLS-2$ + String revisionString = srcCommit.getName(); + return String.format("%s%s(cherry picked from commit %s)", //$NON-NLS-1$ + fullMessage, separator, revisionString); + }; + + /** + * @param srcCommit + * original cherry-picked commit + * @return target cherry-picked commit message + */ + String getCherryPickedCommitMessage(RevCommit srcCommit); + + private static boolean messageEndsWithFooter(RevCommit srcCommit) { + byte[] rawBuffer = srcCommit.getRawBuffer(); + List<FooterLine> footers = srcCommit.getFooterLines(); + int maxFooterEnd = footers.stream().mapToInt(FooterLine::getEndOffset) + .max().orElse(-1); + return rawBuffer.length == maxFooterEnd; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickResult.java index 55d86b7402..995f890389 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickResult.java @@ -25,21 +25,21 @@ public class CherryPickResult { * The cherry-pick status */ public enum CherryPickStatus { - /** */ + /** Cherry-pick succeeded */ OK { @Override public String toString() { return "Ok"; //$NON-NLS-1$ } }, - /** */ + /** Cherry-pick failed */ FAILED { @Override public String toString() { return "Failed"; //$NON-NLS-1$ } }, - /** */ + /** Cherry-pick found conflicts to be resolved */ CONFLICTING { @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java index 36ca97d694..a4a0c49f45 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java @@ -113,24 +113,25 @@ public class CleanCommand extends GitCommand<Set<String>> { } /** - * When dryRun is false, deletes the specified path from disk. If dryRun - * is true, no paths are actually deleted. In both cases, the paths that - * would have been deleted are added to inFiles and returned. + * When dryRun is false, deletes the specified path from disk. If dryRun is + * true, no paths are actually deleted. In both cases, the paths that would + * have been deleted are added to inFiles and returned. * * Paths that are directories are recursively deleted when - * {@link #directories} is true. - * Paths that are git repositories are recursively deleted when - * {@link #directories} and {@link #force} are both true. + * {@link #directories} is true. Paths that are git repositories are + * recursively deleted when {@link #directories} and {@link #force} are both + * true. * * @param path - * The path to be cleaned + * The path to be cleaned * @param inFiles - * A set of strings representing the files that have been cleaned - * already, the path to be cleaned will be added to this set - * before being returned. + * A set of strings representing the files that have been cleaned + * already, the path to be cleaned will be added to this set + * before being returned. * * @return a set of strings with the cleaned path added to it * @throws IOException + * if an IO error occurred */ private Set<String> cleanPath(String path, Set<String> inFiles) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java index 107b00e274..4a536b9534 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java @@ -29,6 +29,7 @@ import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.util.ShutdownHook; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.BranchConfig.BranchRebaseMode; import org.eclipse.jgit.lib.ConfigConstants; @@ -66,6 +67,8 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { private boolean bare; + private boolean relativePaths; + private FS fs; private String remote = Constants.DEFAULT_REMOTE_NAME; @@ -100,6 +103,8 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { private List<String> shallowExcludes = new ArrayList<>(); + private ShutdownHook.Listener shutdownListener = this::cleanup; + private enum FETCH_TYPE { MULTIPLE_BRANCHES, ALL_BRANCHES, MIRROR } @@ -181,12 +186,7 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { @SuppressWarnings("resource") // Closed by caller Repository repository = init(); FetchResult fetchResult = null; - Thread cleanupHook = new Thread(() -> cleanup()); - try { - Runtime.getRuntime().addShutdownHook(cleanupHook); - } catch (IllegalStateException e) { - // ignore - the VM is already shutting down - } + ShutdownHook.INSTANCE.register(shutdownListener); try { fetchResult = fetch(repository, u); } catch (IOException ioe) { @@ -210,11 +210,7 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { cleanup(); throw e; } finally { - try { - Runtime.getRuntime().removeShutdownHook(cleanupHook); - } catch (IllegalStateException e) { - // ignore - the VM is already shutting down - } + ShutdownHook.INSTANCE.unregister(shutdownListener); } try { checkout(repository, fetchResult); @@ -270,6 +266,7 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { private Repository init() throws GitAPIException { InitCommand command = Git.init(); command.setBare(bare); + command.setRelativeDirs(relativePaths); if (fs != null) { command.setFs(fs); } @@ -561,6 +558,20 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { } /** + * Set whether the cloned repository shall use relative paths for GIT_DIR + * and GIT_WORK_TREE + * + * @param relativePaths + * if true, use relative paths for GIT_DIR and GIT_WORK_TREE + * @return this instance + * @since 7.2 + */ + public CloneCommand setRelativePaths(boolean relativePaths) { + this.relativePaths = relativePaths; + return this; + } + + /** * Set the file system abstraction to be used for repositories created by * this command. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index 3b3baf5a12..a7d409c3f5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -16,7 +16,6 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import org.eclipse.jgit.annotations.NonNull; @@ -52,9 +51,6 @@ import org.eclipse.jgit.lib.CommitConfig.CleanupMode; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.GpgConfig; -import org.eclipse.jgit.lib.GpgConfig.GpgFormat; -import org.eclipse.jgit.lib.GpgObjectSigner; -import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; @@ -63,6 +59,8 @@ import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryState; +import org.eclipse.jgit.lib.Signer; +import org.eclipse.jgit.lib.Signers; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevTag; @@ -109,7 +107,7 @@ public class CommitCommand extends GitCommand<RevCommit> { * parents this commit should have. The current HEAD will be in this list * and also all commits mentioned in .git/MERGE_HEAD */ - private List<ObjectId> parents = new LinkedList<>(); + private List<ObjectId> parents = new ArrayList<>(); private String reflogComment; @@ -130,7 +128,7 @@ public class CommitCommand extends GitCommand<RevCommit> { private String signingKey; - private GpgSigner gpgSigner; + private Signer signer; private GpgConfig gpgConfig; @@ -320,30 +318,22 @@ public class CommitCommand extends GitCommand<RevCommit> { } } - private void sign(CommitBuilder commit) throws ServiceUnavailableException, - CanceledException, UnsupportedSigningFormatException { - if (gpgSigner == null) { - gpgSigner = GpgSigner.getDefault(); - if (gpgSigner == null) { - throw new ServiceUnavailableException( - JGitText.get().signingServiceUnavailable); + private void sign(CommitBuilder commit) + throws CanceledException, IOException, + UnsupportedSigningFormatException { + if (signer == null) { + signer = Signers.get(gpgConfig.getKeyFormat()); + if (signer == null) { + throw new UnsupportedSigningFormatException(MessageFormat + .format(JGitText.get().signatureTypeUnknown, + gpgConfig.getKeyFormat().toConfigValue())); } } if (signingKey == null) { signingKey = gpgConfig.getSigningKey(); } - if (gpgSigner instanceof GpgObjectSigner) { - ((GpgObjectSigner) gpgSigner).signObject(commit, - signingKey, committer, credentialsProvider, - gpgConfig); - } else { - if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) { - throw new UnsupportedSigningFormatException(JGitText - .get().onlyOpenPgpSupportedForSigning); - } - gpgSigner.sign(commit, signingKey, committer, - credentialsProvider); - } + signer.signObject(repo, gpgConfig, commit, committer, signingKey, + credentialsProvider); } private void updateRef(RepositoryState state, ObjectId headId, @@ -614,7 +604,7 @@ public class CommitCommand extends GitCommand<RevCommit> { // specified the commit should not be empty. This behaviour differs // from native git but can only be adapted in the next release. // TODO(ch) align the defaults with native git - allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE; + allowEmpty = only.isEmpty() ? Boolean.TRUE : Boolean.FALSE; // when doing a merge commit parse MERGE_HEAD and MERGE_MSG files if (state == RepositoryState.MERGING_RESOLVED @@ -1098,22 +1088,22 @@ public class CommitCommand extends GitCommand<RevCommit> { } /** - * Sets the {@link GpgSigner} to use if the commit is to be signed. + * Sets the {@link Signer} to use if the commit is to be signed. * * @param signer * to use; if {@code null}, the default signer will be used * @return {@code this} - * @since 5.11 + * @since 7.0 */ - public CommitCommand setGpgSigner(GpgSigner signer) { + public CommitCommand setSigner(Signer signer) { checkCallable(); - this.gpgSigner = signer; + this.signer = signer; return this; } /** * Sets an external {@link GpgConfig} to use. Whether it will be used is at - * the discretion of the {@link #setGpgSigner(GpgSigner)}. + * the discretion of the {@link #setSigner(Signer)}. * * @param config * to set; if {@code null}, the config will be loaded from the diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CreateBranchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CreateBranchCommand.java index 9e77dd7348..013e0ff131 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CreateBranchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CreateBranchCommand.java @@ -82,16 +82,13 @@ public class CreateBranchCommand extends GitCommand<Ref> { super(repo); } - /** {@inheritDoc} */ @Override public Ref call() throws GitAPIException, RefAlreadyExistsException, RefNotFoundException, InvalidRefNameException { checkCallable(); processOptions(); try (RevWalk revWalk = new RevWalk(repo)) { - Ref refToCheck = repo.findRef(name); - boolean exists = refToCheck != null - && refToCheck.getName().startsWith(R_HEADS); + boolean exists = repo.findRef(R_HEADS + name) != null; if (!force && exists) throw new RefAlreadyExistsException(MessageFormat.format( JGitText.get().refAlreadyExists1, name)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DeleteBranchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DeleteBranchCommand.java index 3d3ee63568..a5c535f772 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DeleteBranchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DeleteBranchCommand.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com> - * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> and others + * Copyright (C) 2010, 2023 Chris Aniszczyk <caniszczyk@gmail.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -14,6 +14,7 @@ import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -25,6 +26,8 @@ import org.eclipse.jgit.api.errors.NotMergedException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ConfigConstants; 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.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; @@ -47,8 +50,11 @@ import org.eclipse.jgit.revwalk.RevWalk; * >Git documentation about Branch</a> */ public class DeleteBranchCommand extends GitCommand<List<String>> { + private final Set<String> branchNames = new HashSet<>(); + private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; + private boolean force; /** @@ -61,14 +67,34 @@ public class DeleteBranchCommand extends GitCommand<List<String>> { super(repo); } - /** {@inheritDoc} */ @Override public List<String> call() throws GitAPIException, NotMergedException, CannotDeleteCurrentBranchException { checkCallable(); List<String> result = new ArrayList<>(); - if (branchNames.isEmpty()) + Set<String> shortNames = new HashSet<>(); + if (branchNames.isEmpty()) { return result; + } + Exception error = null; + try { + deleteBranches(result, shortNames); + } catch (Exception e) { + error = e; + } + monitor.beginTask(JGitText.get().updatingConfig, 1); + try { + updateConfig(shortNames, error); + } finally { + monitor.update(1); + monitor.endTask(); + } + return result; + } + + private void deleteBranches(List<String> result, + Set<String> shortNames) throws GitAPIException, NotMergedException, + CannotDeleteCurrentBranchException { try { String currentBranch = repo.getFullBranch(); if (!force) { @@ -78,12 +104,13 @@ public class DeleteBranchCommand extends GitCommand<List<String>> { RevCommit tip = walk .parseCommit(repo.resolve(Constants.HEAD)); for (String branchName : branchNames) { - if (branchName == null) + if (branchName == null) { continue; + } Ref currentRef = repo.findRef(branchName); - if (currentRef == null) + if (currentRef == null) { continue; - + } RevCommit base = walk .parseCommit(repo.resolve(branchName)); if (!walk.isMergedInto(base, tip)) { @@ -93,58 +120,105 @@ public class DeleteBranchCommand extends GitCommand<List<String>> { } } setCallable(false); - for (String branchName : branchNames) { - if (branchName == null) - continue; - Ref currentRef = repo.findRef(branchName); - if (currentRef == null) - continue; - String fullName = currentRef.getName(); - if (fullName.equals(currentBranch)) - throw new CannotDeleteCurrentBranchException( - MessageFormat - .format( - JGitText.get().cannotDeleteCheckedOutBranch, - branchName)); - RefUpdate update = repo.updateRef(fullName); - update.setRefLogMessage("branch deleted", false); //$NON-NLS-1$ - update.setForceUpdate(true); - Result deleteResult = update.delete(); - - boolean ok = true; - switch (deleteResult) { - case IO_FAILURE: - case LOCK_FAILURE: - case REJECTED: - ok = false; - break; - default: - break; - } + monitor.start(2); + monitor.beginTask(JGitText.get().deletingBranches, + branchNames.size()); + try { + for (String branchName : branchNames) { + if (branchName == null) { + monitor.update(1); + continue; + } + Ref currentRef = repo.findRef(branchName); + if (currentRef == null) { + monitor.update(1); + continue; + } + String fullName = currentRef.getName(); + if (fullName.equals(currentBranch)) { + throw new CannotDeleteCurrentBranchException( + MessageFormat.format(JGitText + .get().cannotDeleteCheckedOutBranch, + branchName)); + } + RefUpdate update = repo.updateRef(fullName); + update.setRefLogMessage("branch deleted", false); //$NON-NLS-1$ + update.setForceUpdate(true); + Result deleteResult = update.delete(); - if (ok) { - result.add(fullName); - if (fullName.startsWith(Constants.R_HEADS)) { - String shortenedName = fullName - .substring(Constants.R_HEADS.length()); - // remove upstream configuration if any - final StoredConfig cfg = repo.getConfig(); - cfg.unsetSection( - ConfigConstants.CONFIG_BRANCH_SECTION, - shortenedName); - cfg.save(); + switch (deleteResult) { + case IO_FAILURE: + case LOCK_FAILURE: + case REJECTED: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().deleteBranchUnexpectedResult, + deleteResult.name())); + default: + result.add(fullName); + if (fullName.startsWith(Constants.R_HEADS)) { + shortNames.add(fullName + .substring(Constants.R_HEADS.length())); + } + break; + } + monitor.update(1); + if (monitor.isCancelled()) { + break; } - } else - throw new JGitInternalException(MessageFormat.format( - JGitText.get().deleteBranchUnexpectedResult, - deleteResult.name())); + } + } finally { + monitor.endTask(); } - return result; } catch (IOException ioe) { throw new JGitInternalException(ioe.getMessage(), ioe); } } + private void updateConfig(Set<String> shortNames, Exception error) + throws GitAPIException { + IOException configError = null; + if (!shortNames.isEmpty()) { + try { + // Remove upstream configurations if any + StoredConfig cfg = repo.getConfig(); + boolean changed = false; + for (String branchName : shortNames) { + changed |= cfg.removeSection( + ConfigConstants.CONFIG_BRANCH_SECTION, + branchName); + } + if (changed) { + cfg.save(); + } + } catch (IOException e) { + configError = e; + } + } + if (error == null) { + if (configError != null) { + throw new JGitInternalException(configError.getMessage(), + configError); + } + } else if (error instanceof GitAPIException) { + if (configError != null) { + error.addSuppressed(configError); + } + throw (GitAPIException) error; + } else if (error instanceof RuntimeException) { + if (configError != null) { + error.addSuppressed(configError); + } + throw (RuntimeException) error; + } else { + JGitInternalException internal = new JGitInternalException( + error.getMessage(), error); + if (configError != null) { + internal.addSuppressed(configError); + } + throw internal; + } + } + /** * Set the names of the branches to delete * @@ -161,6 +235,22 @@ public class DeleteBranchCommand extends GitCommand<List<String>> { } /** + * Sets the names of the branches to delete + * + * @param branchNames + * the names of the branches to delete; if not set, this will do + * nothing; invalid branch names will simply be ignored + * @return {@code this} + * @since 6.8 + */ + public DeleteBranchCommand setBranchNames(Collection<String> branchNames) { + checkCallable(); + this.branchNames.clear(); + this.branchNames.addAll(branchNames); + return this; + } + + /** * Set whether to forcefully delete branches * * @param force @@ -176,4 +266,34 @@ public class DeleteBranchCommand extends GitCommand<List<String>> { this.force = force; return this; } + + /** + * Retrieves the progress monitor. + * + * @return the {@link ProgressMonitor} for the delete operation + * @since 6.8 + */ + public ProgressMonitor getProgressMonitor() { + return monitor; + } + + /** + * Sets the progress monitor associated with the delete operation. By + * default, this is set to <code>NullProgressMonitor</code> + * + * @see NullProgressMonitor + * @param monitor + * a {@link ProgressMonitor} + * @return {@code this} + * @since 6.8 + */ + public DeleteBranchCommand setProgressMonitor(ProgressMonitor monitor) { + checkCallable(); + if (monitor == null) { + monitor = NullProgressMonitor.INSTANCE; + } + this.monitor = monitor; + return this; + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DeleteTagCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DeleteTagCommand.java index 64d0d94171..92d88aad7d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DeleteTagCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DeleteTagCommand.java @@ -48,7 +48,6 @@ public class DeleteTagCommand extends GitCommand<List<String>> { super(repo); } - /** {@inheritDoc} */ @Override public List<String> call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java index 805a886392..d2526287f9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java @@ -15,11 +15,11 @@ import static org.eclipse.jgit.lib.TypedConfigGetter.UNSET_INT; import java.io.IOException; import java.text.MessageFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.Date; import java.util.List; import java.util.Map; import java.util.Optional; @@ -76,6 +76,11 @@ public class DescribeCommand extends GitCommand<String> { private List<FileNameMatcher> matchers = new ArrayList<>(); /** + * Pattern matchers to be applied to tags for exclusion. + */ + private List<FileNameMatcher> excludeMatchers = new ArrayList<>(); + + /** * Whether to use all refs in the refs/ namespace */ private boolean useAll; @@ -263,6 +268,27 @@ public class DescribeCommand extends GitCommand<String> { return this; } + /** + * Sets one or more {@code glob(7)} patterns that tags must not match to be + * considered. If multiple patterns are provided, they will all be applied. + * + * @param patterns + * the {@code glob(7)} pattern or patterns + * @return {@code this} + * @throws org.eclipse.jgit.errors.InvalidPatternException + * if the pattern passed in was invalid. + * @see <a href= + * "https://www.kernel.org/pub/software/scm/git/docs/git-describe.html" + * >Git documentation about describe</a> + * @since 7.2 + */ + public DescribeCommand setExclude(String... patterns) throws InvalidPatternException { + for (String p : patterns) { + excludeMatchers.add(new FileNameMatcher(p, null)); + } + return this; + } + private final Comparator<Ref> TAG_TIE_BREAKER = new Comparator<>() { @Override @@ -274,25 +300,28 @@ public class DescribeCommand extends GitCommand<String> { } } - private Date tagDate(Ref tag) throws IOException { + private Instant tagDate(Ref tag) throws IOException { RevTag t = w.parseTag(tag.getObjectId()); w.parseBody(t); - return t.getTaggerIdent().getWhen(); + return t.getTaggerIdent().getWhenAsInstant(); } }; private Optional<Ref> getBestMatch(List<Ref> tags) { if (tags == null || tags.isEmpty()) { return Optional.empty(); - } else if (matchers.isEmpty()) { + } else if (matchers.isEmpty() && excludeMatchers.isEmpty()) { Collections.sort(tags, TAG_TIE_BREAKER); return Optional.of(tags.get(0)); - } else { + } + + Stream<Ref> matchingTags; + if (!matchers.isEmpty()) { // Find the first tag that matches in the stream of all tags // filtered by matchers ordered by tie break order - Stream<Ref> matchingTags = Stream.empty(); + matchingTags = Stream.empty(); for (FileNameMatcher matcher : matchers) { - Stream<Ref> m = tags.stream().filter( + Stream<Ref> m = tags.stream().filter( // tag -> { matcher.append(formatRefName(tag.getName())); boolean result = matcher.isMatch(); @@ -301,8 +330,22 @@ public class DescribeCommand extends GitCommand<String> { }); matchingTags = Stream.of(matchingTags, m).flatMap(i -> i); } - return matchingTags.sorted(TAG_TIE_BREAKER).findFirst(); + } else { + // If there are no matchers, there are only excluders + // Assume all tags match for now before applying excluders + matchingTags = tags.stream(); + } + + for (FileNameMatcher matcher : excludeMatchers) { + matchingTags = matchingTags.filter( // + tag -> { + matcher.append(formatRefName(tag.getName())); + boolean result = matcher.isMatch(); + matcher.reset(); + return !result; + }); } + return matchingTags.sorted(TAG_TIE_BREAKER).findFirst(); } private ObjectId getObjectIdFromRef(Ref r) throws JGitInternalException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java index 3c772c2765..f24127bd51 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java @@ -124,7 +124,7 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { FetchRecurseSubmodulesMode mode = repo.getConfig().getEnum( FetchRecurseSubmodulesMode.values(), ConfigConstants.CONFIG_SUBMODULE_SECTION, path, - ConfigConstants.CONFIG_KEY_FETCH_RECURSE_SUBMODULES, null); + ConfigConstants.CONFIG_KEY_FETCH_RECURSE_SUBMODULES); if (mode != null) { return mode; } @@ -132,7 +132,7 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { // Fall back to fetch.recurseSubmodules, if set mode = repo.getConfig().getEnum(FetchRecurseSubmodulesMode.values(), ConfigConstants.CONFIG_FETCH_SECTION, null, - ConfigConstants.CONFIG_KEY_RECURSE_SUBMODULES, null); + ConfigConstants.CONFIG_KEY_RECURSE_SUBMODULES); if (mode != null) { return mode; } @@ -491,13 +491,13 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { * * Default setting is Transport.DEFAULT_FETCH_THIN * - * @param thin + * @param thinPack * the thin-pack preference * @return {@code this} */ - public FetchCommand setThin(boolean thin) { + public FetchCommand setThin(boolean thinPack) { checkCallable(); - this.thin = thin; + this.thin = thinPack; return this; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java index 584d2bc394..f6935e1c67 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java @@ -12,6 +12,7 @@ package org.eclipse.jgit.api; import java.io.IOException; import java.text.MessageFormat; import java.text.ParseException; +import java.time.Instant; import java.util.Date; import java.util.Properties; import java.util.concurrent.ExecutionException; @@ -59,10 +60,12 @@ public class GarbageCollectCommand extends GitCommand<Properties> { private ProgressMonitor monitor; - private Date expire; + private Instant expire; private PackConfig pconfig; + private Boolean packKeptObjects; + /** * Constructor for GarbageCollectCommand. * @@ -96,8 +99,29 @@ public class GarbageCollectCommand extends GitCommand<Properties> { * @param expire * minimal age of objects to be pruned. * @return this instance + * @deprecated use {@link #setExpire(Instant)} instead */ + @Deprecated(since = "7.2") public GarbageCollectCommand setExpire(Date expire) { + if (expire != null) { + this.expire = expire.toInstant(); + } + return this; + } + + /** + * During gc() or prune() each unreferenced, loose object which has been + * created or modified after <code>expire</code> will not be pruned. Only + * older objects may be pruned. If set to null then every object is a + * candidate for pruning. Use {@link org.eclipse.jgit.util.GitTimeParser} to + * parse time formats used by git gc. + * + * @param expire + * minimal age of objects to be pruned. + * @return this instance + * @since 7.2 + */ + public GarbageCollectCommand setExpire(Instant expire) { this.expire = expire; return this; } @@ -106,8 +130,8 @@ public class GarbageCollectCommand extends GitCommand<Properties> { * Whether to use aggressive mode or not. If set to true JGit behaves more * similar to native git's "git gc --aggressive". If set to * <code>true</code> compressed objects found in old packs are not reused - * but every object is compressed again. Configuration variables - * pack.window and pack.depth are set to 250 for this GC. + * but every object is compressed again. Configuration variables pack.window + * and pack.depth are set to 250 for this GC. * * @since 3.6 * @param aggressive @@ -132,6 +156,19 @@ public class GarbageCollectCommand extends GitCommand<Properties> { } /** + * Whether to include objects in `.keep` packs when repacking. + * + * @param packKeptObjects + * whether to include objects in `.keep` files when repacking. + * @return this instance + * @since 5.13.3 + */ + public GarbageCollectCommand setPackKeptObjects(boolean packKeptObjects) { + this.packKeptObjects = Boolean.valueOf(packKeptObjects); + return this; + } + + /** * Whether to preserve old pack files instead of deleting them. * * @since 4.7 @@ -163,7 +200,6 @@ public class GarbageCollectCommand extends GitCommand<Properties> { return this; } - /** {@inheritDoc} */ @Override public Properties call() throws GitAPIException { checkCallable(); @@ -175,7 +211,9 @@ public class GarbageCollectCommand extends GitCommand<Properties> { gc.setProgressMonitor(monitor); if (this.expire != null) gc.setExpire(expire); - + if (this.packKeptObjects != null) { + gc.setPackKeptObjects(packKeptObjects.booleanValue()); + } try { gc.gc().get(); return toProperties(gc.getStatistics()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java index 3b3e10e7b2..a8b866d25b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java @@ -68,6 +68,7 @@ public class Git implements AutoCloseable { * @return a {@link org.eclipse.jgit.api.Git} object for the existing git * repository * @throws java.io.IOException + * if an IO error occurred */ public static Git open(File dir) throws IOException { return open(dir, FS.DETECTED); @@ -84,6 +85,7 @@ public class Git implements AutoCloseable { * @return a {@link org.eclipse.jgit.api.Git} object for the existing git * repository. Closing this instance will close the repo. * @throws java.io.IOException + * if an IO error occurred */ public static Git open(File dir, FS fs) throws IOException { RepositoryCache.FileKey key; @@ -199,7 +201,24 @@ public class Git implements AutoCloseable { this(repo, false); } - Git(Repository repo, boolean closeRepo) { + /** + * Construct a new {@link org.eclipse.jgit.api.Git} object which can + * interact with the specified git repository. + * <p> + * All command classes returned by methods of this class will always + * interact with this git repository. + * <p> + * If {@code closeRepo = false} the caller is responsible for closing the + * repository. + * + * @param repo + * the git repository this class is interacting with; + * {@code null} is not allowed. + * @param closeRepo + * whether to close the repository when this instance is closed + * @since 7.4 + */ + public Git(Repository repo, boolean closeRepo) { this.repo = requireNonNull(repo); this.closeRepo = closeRepo; } @@ -712,6 +731,16 @@ public class Git implements AutoCloseable { } /** + * Return a command object to execute a {@code PackRefs} command + * + * @return a {@link org.eclipse.jgit.api.PackRefsCommand} + * @since 7.1 + */ + public PackRefsCommand packRefs() { + return new PackRefsCommand(repo); + } + + /** * Return a command object to find human-readable names of revisions. * * @return a {@link org.eclipse.jgit.api.NameRevCommand}. @@ -792,7 +821,6 @@ public class Git implements AutoCloseable { return repo; } - /** {@inheritDoc} */ @Override public String toString() { return "Git[" + repo + "]"; //$NON-NLS-1$//$NON-NLS-2$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java index 240290f4f9..1da71aa6eb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java @@ -19,6 +19,7 @@ import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.file.FileRepository; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; @@ -44,6 +45,8 @@ public class InitCommand implements Callable<Git> { private String initialBranch; + private boolean relativePaths; + /** * {@inheritDoc} * <p> @@ -100,7 +103,11 @@ public class InitCommand implements Callable<Git> { : initialBranch); Repository repository = builder.build(); if (!repository.getObjectDatabase().exists()) - repository.create(bare); + if (repository instanceof FileRepository) { + ((FileRepository) repository).create(bare, relativePaths); + } else { + repository.create(bare); + } return new Git(repository, true); } catch (IOException | ConfigInvalidException e) { throw new JGitInternalException(e.getMessage(), e); @@ -214,4 +221,18 @@ public class InitCommand implements Callable<Git> { this.initialBranch = branch; return this; } + + /** + * * Set whether the repository shall use relative paths for GIT_DIR and + * GIT_WORK_TREE + * + * @param relativePaths + * if true, use relative paths for GIT_DIR and GIT_WORK_TREE + * @return {@code this} + * @since 7.2 + */ + public InitCommand setRelativeDirs(boolean relativePaths) { + this.relativePaths = relativePaths; + return this; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ListBranchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ListBranchCommand.java index 4b7445fc95..e3c3c89bcb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ListBranchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ListBranchCommand.java @@ -73,7 +73,6 @@ public class ListBranchCommand extends GitCommand<List<Ref>> { super(repo); } - /** {@inheritDoc} */ @Override public List<Ref> call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ListNotesCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ListNotesCommand.java index 34955e86f9..9eb52866dc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ListNotesCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ListNotesCommand.java @@ -44,7 +44,6 @@ public class ListNotesCommand extends GitCommand<List<Note>> { super(repo); } - /** {@inheritDoc} */ @Override public List<Note> call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ListTagCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ListTagCommand.java index 27a5288429..9a4a822b59 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ListTagCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ListTagCommand.java @@ -54,8 +54,11 @@ public class ListTagCommand extends GitCommand<List<Ref>> { * the specified commit * @return this command * @throws IOException + * if an IO error occurred * @throws IncorrectObjectTypeException + * if commit has an incorrect object type * @throws MissingObjectException + * if the commit is missing * * @since 6.6 */ @@ -67,7 +70,6 @@ public class ListTagCommand extends GitCommand<List<Ref>> { return this; } - /** {@inheritDoc} */ @Override public List<Ref> call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java index fa40d93e52..2a8d34ed68 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java @@ -53,13 +53,11 @@ import org.eclipse.jgit.treewalk.filter.TreeFilter; * </pre> * <p> * - * <p> * Get commits only for a specific file: * * <pre> * git.log().add(head).addPath("dir/filename.txt").call(); * </pre> - * <p> * * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-log.html" * >Git documentation about Log</a> @@ -112,10 +110,10 @@ public class LogCommand extends GitCommand<Iterable<RevCommit>> { } if (!filters.isEmpty()) { if (filters.size() == 1) { - filters.add(TreeFilter.ANY_DIFF); + walk.setTreeFilter(filters.get(0)); + } else { + walk.setTreeFilter(AndTreeFilter.create(filters)); } - walk.setTreeFilter(AndTreeFilter.create(filters)); - } if (skip > -1 && maxCount > -1) walk.setRevFilter(AndRevFilter.create(SkipRevFilter.create(skip), diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java index ed4a5342b3..7064f5a57a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java @@ -13,9 +13,9 @@ package org.eclipse.jgit.api; import java.io.IOException; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -75,7 +75,7 @@ public class MergeCommand extends GitCommand<MergeResult> { private ContentMergeStrategy contentStrategy; - private List<Ref> commits = new LinkedList<>(); + private List<Ref> commits = new ArrayList<>(); private Boolean squash; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java index 7347f63889..0fa11f2cbb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeResult.java @@ -31,7 +31,9 @@ public class MergeResult { * The status the merge resulted in. */ public enum MergeStatus { - /** */ + /** + * Merge is a fast-forward + */ FAST_FORWARD { @Override public String toString() { @@ -44,6 +46,8 @@ public class MergeResult { } }, /** + * Merge is a fast-forward, squashed + * * @since 2.0 */ FAST_FORWARD_SQUASHED { @@ -57,7 +61,9 @@ public class MergeResult { return true; } }, - /** */ + /** + * Already up to date, merge was a no-op + */ ALREADY_UP_TO_DATE { @Override public String toString() { @@ -69,7 +75,9 @@ public class MergeResult { return true; } }, - /** */ + /** + * Merge failed + */ FAILED { @Override public String toString() { @@ -81,7 +89,9 @@ public class MergeResult { return false; } }, - /** */ + /** + * Merged + */ MERGED { @Override public String toString() { @@ -94,6 +104,8 @@ public class MergeResult { } }, /** + * Merged, squashed, not updating HEAD + * * @since 2.0 */ MERGED_SQUASHED { @@ -108,6 +120,8 @@ public class MergeResult { } }, /** + * Merged, squashed, not committed + * * @since 3.0 */ MERGED_SQUASHED_NOT_COMMITTED { @@ -121,7 +135,9 @@ public class MergeResult { return true; } }, - /** */ + /** + * Merge raised conflicts to be resolved + */ CONFLICTING { @Override public String toString() { @@ -134,6 +150,8 @@ public class MergeResult { } }, /** + * Merge was aborted + * * @since 2.2 */ ABORTED { @@ -148,6 +166,8 @@ public class MergeResult { } }, /** + * Merged, not committed + * * @since 3.0 **/ MERGED_NOT_COMMITTED { @@ -161,7 +181,7 @@ public class MergeResult { return true; } }, - /** */ + /** Not yet supported */ NOT_SUPPORTED { @Override public String toString() { @@ -191,6 +211,8 @@ public class MergeResult { }; /** + * Whether the merge was successful + * * @return whether the status indicates a successful result */ public abstract boolean isSuccessful(); @@ -364,7 +386,6 @@ public class MergeResult { return base; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/NameRevCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/NameRevCommand.java index e6ab1e6588..1ff6e98b12 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/NameRevCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/NameRevCommand.java @@ -102,7 +102,6 @@ public class NameRevCommand extends GitCommand<Map<ObjectId, String>> { }; } - /** {@inheritDoc} */ @Override public Map<ObjectId, String> call() throws GitAPIException { try { @@ -349,7 +348,7 @@ public class NameRevCommand extends GitCommand<Map<ObjectId, String>> { } // Don't tiebreak if prefixes are the same, in order to prefer first-parent // paths. - return li - ri; + return (long) li - ri; } private static String simplify(String refName) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java new file mode 100644 index 0000000000..29a69c5ac4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Qualcomm Innovation Center, 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 v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.api; + +import java.io.IOException; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Repository; + +/** + * Optimize storage of references. + * + * @since 7.1 + */ +public class PackRefsCommand extends GitCommand<String> { + private ProgressMonitor monitor; + + private boolean all; + + /** + * Creates a new {@link PackRefsCommand} instance with default values. + * + * @param repo + * the repository this command will be used on + */ + public PackRefsCommand(Repository repo) { + super(repo); + this.monitor = NullProgressMonitor.INSTANCE; + } + + /** + * Set progress monitor + * + * @param monitor + * a progress monitor + * @return this instance + */ + public PackRefsCommand setProgressMonitor(ProgressMonitor monitor) { + this.monitor = monitor; + return this; + } + + /** + * Specify whether to pack all the references. + * + * @param all + * if <code>true</code> all the loose refs will be packed + * @return this instance + */ + public PackRefsCommand setAll(boolean all) { + this.all = all; + return this; + } + + /** + * Whether to pack all the references + * + * @return whether to pack all the references + */ + public boolean isAll() { + return all; + } + + @Override + public String call() throws GitAPIException { + checkCallable(); + try { + repo.getRefDatabase().packRefs(monitor, this); + return JGitText.get().packRefsSuccessful; + } catch (IOException e) { + throw new JGitInternalException(JGitText.get().packRefsFailed, e); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java index 83ae0fc9d4..4b2cee45c2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java @@ -533,9 +533,9 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { Config config) { BranchRebaseMode mode = config.getEnum(BranchRebaseMode.values(), ConfigConstants.CONFIG_BRANCH_SECTION, - branchName, ConfigConstants.CONFIG_KEY_REBASE, null); + branchName, ConfigConstants.CONFIG_KEY_REBASE); if (mode == null) { - mode = config.getEnum(BranchRebaseMode.values(), + mode = config.getEnum( ConfigConstants.CONFIG_PULL_SECTION, null, ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE); } @@ -549,7 +549,7 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { Config config = repo.getConfig(); Merge ffMode = config.getEnum(Merge.values(), ConfigConstants.CONFIG_PULL_SECTION, null, - ConfigConstants.CONFIG_KEY_FF, null); + ConfigConstants.CONFIG_KEY_FF); return ffMode != null ? FastForwardMode.valueOf(ffMode) : null; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullResult.java index cbb9cc2f78..fdc7f3f333 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullResult.java @@ -89,7 +89,6 @@ public class PullResult { return true; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java index 2ed1c52fd7..e9d1a3213b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java @@ -614,13 +614,13 @@ public class PushCommand extends * * Default setting is Transport.DEFAULT_PUSH_THIN * - * @param thin + * @param thinPack * the thin-pack preference value * @return {@code this} */ - public PushCommand setThin(boolean thin) { + public PushCommand setThin(boolean thinPack) { checkCallable(); - this.thin = thin; + this.thin = thinPack; return this; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index 1e5523f275..3ae7a6c81e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -18,12 +18,13 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.text.MessageFormat; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; @@ -86,7 +87,6 @@ import org.eclipse.jgit.util.RawParseUtils; * supported options and arguments of this command and a {@link #call()} method * to finally execute the command. Each instance of this class should only be * used for one invocation of the command (means: one call to {@link #call()}) - * <p> * * @see <a * href="http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html" @@ -290,13 +290,17 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } } RebaseResult res = initFilesAndRewind(); - if (stopAfterInitialization) + if (stopAfterInitialization) { return RebaseResult.INTERACTIVE_PREPARED_RESULT; + } if (res != null) { - autoStashApply(); - if (rebaseState.getDir().exists()) + if (!autoStashApply()) { + res = RebaseResult.STASH_APPLY_CONFLICTS_RESULT; + } + if (rebaseState.getDir().exists()) { FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); + } return res; } } @@ -382,7 +386,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } private boolean autoStashApply() throws IOException, GitAPIException { - boolean conflicts = false; + boolean success = true; if (rebaseState.getFile(AUTOSTASH).exists()) { String stash = rebaseState.readFile(AUTOSTASH); try (Git git = Git.wrap(repo)) { @@ -390,7 +394,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { .ignoreRepositoryState(true).setStrategy(strategy) .call(); } catch (StashApplyFailureException e) { - conflicts = true; + success = false; try (RevWalk rw = new RevWalk(repo)) { ObjectId stashId = repo.resolve(stash); RevCommit commit = rw.parseCommit(stashId); @@ -399,7 +403,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } } } - return conflicts; + return success; } private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent, @@ -724,13 +728,15 @@ public class RebaseCommand extends GitCommand<RebaseResult> { boolean lastStepIsForward) throws IOException, GitAPIException { String headName = rebaseState.readFile(HEAD_NAME); updateHead(headName, finalHead, upstreamCommit); - boolean stashConflicts = autoStashApply(); + boolean unstashSuccessful = autoStashApply(); getRepository().autoGC(monitor); FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); - if (stashConflicts) + if (!unstashSuccessful) { return RebaseResult.STASH_APPLY_CONFLICTS_RESULT; - if (lastStepIsForward || finalHead == null) + } + if (lastStepIsForward || finalHead == null) { return RebaseResult.FAST_FORWARD_RESULT; + } return RebaseResult.OK_RESULT; } @@ -1000,7 +1006,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> { /** * @return the commit if we had to do a commit, otherwise null * @throws GitAPIException + * if JGit API failed * @throws IOException + * if an IO error occurred */ private RevCommit continueRebase() throws GitAPIException, IOException { // if there are still conflicts, we throw a specific Exception @@ -1057,12 +1065,16 @@ public class RebaseCommand extends GitCommand<RebaseResult> { String authorScript = toAuthorScript(author); rebaseState.createFile(AUTHOR_SCRIPT, authorScript); rebaseState.createFile(MESSAGE, commitToPick.getFullMessage()); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try (DiffFormatter df = new DiffFormatter(bos)) { - df.setRepository(repo); - df.format(commitToPick.getParent(0), commitToPick); + if (commitToPick.getParentCount() > 0) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (DiffFormatter df = new DiffFormatter(bos)) { + df.setRepository(repo); + df.format(commitToPick.getParent(0), commitToPick); + } + rebaseState.createFile(PATCH, new String(bos.toByteArray(), UTF_8)); + } else { + rebaseState.createFile(PATCH, ""); //$NON-NLS-1$ } - rebaseState.createFile(PATCH, new String(bos.toByteArray(), UTF_8)); rebaseState.createFile(STOPPED_SHA, repo.newObjectReader() .abbreviate( @@ -1102,13 +1114,15 @@ public class RebaseCommand extends GitCommand<RebaseResult> { * that can not be parsed as steps * * @param numSteps + * number of steps to remove * @throws IOException + * if an IO error occurred */ private void popSteps(int numSteps) throws IOException { if (numSteps == 0) return; - List<RebaseTodoLine> todoLines = new LinkedList<>(); - List<RebaseTodoLine> poppedLines = new LinkedList<>(); + List<RebaseTodoLine> todoLines = new ArrayList<>(); + List<RebaseTodoLine> poppedLines = new ArrayList<>(); for (RebaseTodoLine line : repo.readRebaseTodo( rebaseState.getPath(GIT_REBASE_TODO), true)) { @@ -1146,7 +1160,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { if (!isInteractive() && walk.isMergedInto(upstream, headCommit)) return RebaseResult.UP_TO_DATE_RESULT; else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) { - // head is already merged into upstream, fast-foward + // head is already merged into upstream, fast-forward monitor.beginTask(MessageFormat.format( JGitText.get().resettingHead, upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN); @@ -1217,7 +1231,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { Iterator<RevCommit> commitsToUse = r.iterator(); while (commitsToUse.hasNext()) { RevCommit commit = commitsToUse.next(); - if (preserveMerges || commit.getParentCount() == 1) { + if (preserveMerges || commit.getParentCount() <= 1) { cherryPickList.add(commit); } } @@ -1234,23 +1248,31 @@ public class RebaseCommand extends GitCommand<RebaseResult> { walk.markStart(upstreamCommit); walk.markStart(headCommit); RevCommit base; - while ((base = walk.next()) != null) + while ((base = walk.next()) != null) { RebaseState.createFile(rewrittenDir, base.getName(), upstreamCommit.getName()); - + } Iterator<RevCommit> iterator = cherryPickList.iterator(); pickLoop: while(iterator.hasNext()){ RevCommit commit = iterator.next(); - for (int i = 0; i < commit.getParentCount(); i++) { - boolean parentRewritten = new File(rewrittenDir, commit - .getParent(i).getName()).exists(); - if (parentRewritten) { - new File(rewrittenDir, commit.getName()).createNewFile(); - continue pickLoop; + int nOfParents = commit.getParentCount(); + if (nOfParents == 0) { + // Must be the very first commit in the cherryPickList. We + // have independent branches. + new File(rewrittenDir, commit.getName()).createNewFile(); + } else { + for (int i = 0; i < nOfParents; i++) { + boolean parentRewritten = new File(rewrittenDir, + commit.getParent(i).getName()).exists(); + if (parentRewritten) { + new File(rewrittenDir, commit.getName()) + .createNewFile(); + continue pickLoop; + } } + // commit is only merged in, needs not be rewritten + iterator.remove(); } - // commit is only merged in, needs not be rewritten - iterator.remove(); } } return cherryPickList; @@ -1289,7 +1311,9 @@ public class RebaseCommand extends GitCommand<RebaseResult> { * if we can fast-forward to. * @return the new head, or null * @throws java.io.IOException + * if an IO error occurred * @throws org.eclipse.jgit.api.errors.GitAPIException + * if a JGit API exception occurred */ public RevCommit tryFastForward(RevCommit newCommit) throws IOException, GitAPIException { @@ -1434,13 +1458,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> { throw new JGitInternalException( JGitText.get().abortingRebaseFailed); } - boolean stashConflicts = autoStashApply(); + boolean unstashSuccessful = autoStashApply(); // cleanup the files FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); repo.writeCherryPickHead(null); repo.writeMergeHeads(null); - if (stashConflicts) + if (!unstashSuccessful) { return RebaseResult.STASH_APPLY_CONFLICTS_RESULT; + } return result; } finally { @@ -1540,6 +1565,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { * the name of the upstream branch * @return {@code this} * @throws org.eclipse.jgit.api.errors.RefNotFoundException + * if {@code upstream} Ref couldn't be resolved */ public RebaseCommand setUpstream(String upstream) throws RefNotFoundException { @@ -1811,23 +1837,26 @@ public class RebaseCommand extends GitCommand<RebaseResult> { // the time is saved as <seconds since 1970> <timezone offset> int timeStart = 0; - if (time.startsWith("@")) //$NON-NLS-1$ + if (time.startsWith("@")) { //$NON-NLS-1$ timeStart = 1; - else + } else { timeStart = 0; - long when = Long - .parseLong(time.substring(timeStart, time.indexOf(' '))) * 1000; + } + Instant when = Instant.ofEpochSecond( + Long.parseLong(time.substring(timeStart, time.indexOf(' ')))); String tzOffsetString = time.substring(time.indexOf(' ') + 1); int multiplier = -1; - if (tzOffsetString.charAt(0) == '+') + if (tzOffsetString.charAt(0) == '+') { multiplier = 1; + } int hours = Integer.parseInt(tzOffsetString.substring(1, 3)); int minutes = Integer.parseInt(tzOffsetString.substring(3, 5)); // this is in format (+/-)HHMM (hours and minutes) - // we need to convert into minutes - int tz = (hours * 60 + minutes) * multiplier; - if (name != null && email != null) + ZoneOffset tz = ZoneOffset.ofHoursMinutes(hours * multiplier, + minutes * multiplier); + if (name != null && email != null) { return new PersonIdent(name, email, when, tz); + } return null; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java index 826ea518e4..2aa64df46f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseResult.java @@ -146,6 +146,8 @@ public class RebaseResult { }; /** + * Whether the rebase was successful + * * @return whether the status indicates a successful result */ public abstract boolean isSuccessful(); @@ -194,6 +196,7 @@ public class RebaseResult { * Create <code>RebaseResult</code> * * @param status + * the overall rebase status * @param commit * current commit * @return the RebaseResult diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java index dead2749b7..a149649004 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java @@ -68,7 +68,7 @@ public class ReflogCommand extends GitCommand<Collection<ReflogEntry>> { checkCallable(); try { - ReflogReader reader = repo.getReflogReader(ref); + ReflogReader reader = repo.getRefDatabase().getReflogReader(ref); if (reader == null) throw new RefNotFoundException(MessageFormat.format( JGitText.get().refNotResolved, ref)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteRemoveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteRemoveCommand.java index 553fc2e7a0..ad553f07a9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteRemoveCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteRemoveCommand.java @@ -49,18 +49,6 @@ public class RemoteRemoveCommand extends GitCommand<RemoteConfig> { /** * The name of the remote to remove. * - * @param name - * a remote name - * @deprecated use {@link #setRemoteName} instead - */ - @Deprecated - public void setName(String name) { - this.remoteName = name; - } - - /** - * The name of the remote to remove. - * * @param remoteName * a remote name * @return {@code this} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteSetUrlCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteSetUrlCommand.java index e3d01861fc..68ddce361f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteSetUrlCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteSetUrlCommand.java @@ -71,18 +71,6 @@ public class RemoteSetUrlCommand extends GitCommand<RemoteConfig> { /** * The name of the remote to change the URL for. * - * @param name - * a remote name - * @deprecated use {@link #setRemoteName} instead - */ - @Deprecated - public void setName(String name) { - this.remoteName = name; - } - - /** - * The name of the remote to change the URL for. - * * @param remoteName * a remote remoteName * @return {@code this} @@ -96,18 +84,6 @@ public class RemoteSetUrlCommand extends GitCommand<RemoteConfig> { /** * The new URL for the remote. * - * @param uri - * an URL for the remote - * @deprecated use {@link #setRemoteUri} instead - */ - @Deprecated - public void setUri(URIish uri) { - this.remoteUri = uri; - } - - /** - * The new URL for the remote. - * * @param remoteUri * an URL for the remote * @return {@code this} @@ -121,23 +97,6 @@ public class RemoteSetUrlCommand extends GitCommand<RemoteConfig> { /** * Whether to change the push URL of the remote instead of the fetch URL. * - * @param push - * <code>true</code> to set the push url, <code>false</code> to - * set the fetch url - * @deprecated use {@link #setUriType} instead - */ - @Deprecated - public void setPush(boolean push) { - if (push) { - setUriType(UriType.PUSH); - } else { - setUriType(UriType.FETCH); - } - } - - /** - * Whether to change the push URL of the remote instead of the fetch URL. - * * @param type * the <code>UriType</code> value to set * @return {@code this} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoveNoteCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoveNoteCommand.java index f4b60adedb..c65cbf160f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoveNoteCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoveNoteCommand.java @@ -47,7 +47,6 @@ public class RemoveNoteCommand extends GitCommand<Note> { super(repo); } - /** {@inheritDoc} */ @Override public Note call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RenameBranchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RenameBranchCommand.java index 922cb531de..029f9d02d6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RenameBranchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RenameBranchCommand.java @@ -55,7 +55,6 @@ public class RenameBranchCommand extends GitCommand<Ref> { super(repo); } - /** {@inheritDoc} */ @Override public Ref call() throws GitAPIException, RefNotFoundException, InvalidRefNameException, RefAlreadyExistsException, DetachedHeadException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java index 87b4acb14c..47145a0563 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java @@ -11,8 +11,8 @@ package org.eclipse.jgit.api; import java.io.IOException; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import org.eclipse.jgit.api.errors.CheckoutConflictException; import org.eclipse.jgit.api.errors.GitAPIException; @@ -90,7 +90,7 @@ public class ResetCommand extends GitCommand<Ref> { private ResetType mode; - private Collection<String> filepaths = new LinkedList<>(); + private Collection<String> filepaths = new ArrayList<>(); private boolean isReflogDisabled; @@ -436,7 +436,6 @@ public class ResetCommand extends GitCommand<Ref> { repo.writeMergeCommitMsg(null); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java index 513f579b67..6643c83662 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others + * Copyright (C) 2010, 2024 Christian Halstrick <christian.halstrick@sap.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -13,7 +13,7 @@ import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; import java.io.IOException; import java.text.MessageFormat; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -58,11 +58,13 @@ import org.eclipse.jgit.treewalk.FileTreeIterator; * >Git documentation about revert</a> */ public class RevertCommand extends GitCommand<RevCommit> { - private List<Ref> commits = new LinkedList<>(); + private List<Ref> commits = new ArrayList<>(); private String ourCommitName = null; - private List<Ref> revertedRefs = new LinkedList<>(); + private boolean insertChangeId; + + private List<Ref> revertedRefs = new ArrayList<>(); private MergeResult failingResult; @@ -132,7 +134,7 @@ public class RevertCommand extends GitCommand<RevCommit> { String ourName = calculateOurName(headRef); String revertName = srcCommit.getId() - .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name() + " " //$NON-NLS-1$ + .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name() + ' ' + srcCommit.getShortMessage(); ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo); @@ -141,8 +143,8 @@ public class RevertCommand extends GitCommand<RevCommit> { merger.setCommitNames(new String[] { "BASE", ourName, revertName }); //$NON-NLS-1$ - String shortMessage = "Revert \"" + srcCommit.getShortMessage() //$NON-NLS-1$ - + "\""; //$NON-NLS-1$ + String shortMessage = "Revert \"" //$NON-NLS-1$ + + srcCommit.getFirstMessageLine() + '"'; String newMessage = shortMessage + "\n\n" //$NON-NLS-1$ + "This reverts commit " + srcCommit.getId().getName() //$NON-NLS-1$ + ".\n"; //$NON-NLS-1$ @@ -162,6 +164,7 @@ public class RevertCommand extends GitCommand<RevCommit> { dco.checkout(); try (Git git = new Git(getRepository())) { newHead = git.commit().setMessage(newMessage) + .setInsertChangeId(insertChangeId) .setReflogComment("revert: " + shortMessage) //$NON-NLS-1$ .call(); } @@ -327,4 +330,18 @@ public class RevertCommand extends GitCommand<RevCommit> { this.monitor = monitor; return this; } + + /** + * Defines whether to add a Gerrit change ID to each revert commit message. + * + * @param insertChangeId + * whether to insert a change ID + * @return {@code this} + * @since 6.8 + */ + public RevertCommand setInsertChangeId(boolean insertChangeId) { + this.insertChangeId = insertChangeId; + return this; + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RmCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RmCommand.java index 656f36a81a..7459e7298f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RmCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RmCommand.java @@ -13,7 +13,6 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import org.eclipse.jgit.api.errors.GitAPIException; @@ -71,7 +70,7 @@ public class RmCommand extends GitCommand<DirCache> { */ public RmCommand(Repository repo) { super(repo); - filepatterns = new LinkedList<>(); + filepatterns = new ArrayList<>(); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ShowNoteCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ShowNoteCommand.java index f64cb6b831..7bb9de0ef4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ShowNoteCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ShowNoteCommand.java @@ -44,7 +44,6 @@ public class ShowNoteCommand extends GitCommand<Note> { super(repo); } - /** {@inheritDoc} */ @Override public Note call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java index 17036a9cd3..b0b715e06b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012, 2021 GitHub Inc. and others + * Copyright (C) 2012, 2023 GitHub Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -23,6 +23,7 @@ import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.StashApplyFailureException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.dircache.Checkout; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheCheckout; @@ -48,7 +49,6 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.treewalk.WorkingTreeOptions; /** * Command class to apply a stashed commit. @@ -263,18 +263,6 @@ public class StashApplyCommand extends GitCommand<ObjectId> { /** * Whether to restore the index state * - * @param applyIndex - * true (default) if the command should restore the index state - * @deprecated use {@link #setRestoreIndex} instead - */ - @Deprecated - public void setApplyIndex(boolean applyIndex) { - this.restoreIndex = applyIndex; - } - - /** - * Whether to restore the index state - * * @param restoreIndex * true (default) if the command should restore the index state * @return {@code this} @@ -319,19 +307,6 @@ public class StashApplyCommand extends GitCommand<ObjectId> { /** * Whether the command should restore untracked files * - * @param applyUntracked - * true (default) if the command should restore untracked files - * @since 3.4 - * @deprecated use {@link #setRestoreUntracked} instead - */ - @Deprecated - public void setApplyUntracked(boolean applyUntracked) { - this.restoreUntracked = applyUntracked; - } - - /** - * Whether the command should restore untracked files - * * @param restoreUntracked * true (default) if the command should restore untracked files * @return {@code this} @@ -383,8 +358,7 @@ public class StashApplyCommand extends GitCommand<ObjectId> { private void resetUntracked(RevTree tree) throws CheckoutConflictException, IOException { Set<String> actuallyModifiedPaths = new HashSet<>(); - WorkingTreeOptions options = repo.getConfig() - .get(WorkingTreeOptions.KEY); + Checkout checkout = new Checkout(repo).setRecursiveDeletion(true); // TODO maybe NameConflictTreeWalk ? try (TreeWalk walk = new TreeWalk(repo)) { walk.addTree(tree); @@ -408,17 +382,17 @@ public class StashApplyCommand extends GitCommand<ObjectId> { FileTreeIterator fIter = walk .getTree(1, FileTreeIterator.class); + String gitPath = entry.getPathString(); if (fIter != null) { if (fIter.isModified(entry, true, reader)) { // file exists and is dirty - throw new CheckoutConflictException( - entry.getPathString()); + throw new CheckoutConflictException(gitPath); } } - checkoutPath(entry, reader, options, + checkoutPath(entry, gitPath, reader, checkout, new CheckoutMetadata(eolStreamType, null)); - actuallyModifiedPaths.add(entry.getPathString()); + actuallyModifiedPaths.add(gitPath); } } finally { if (!actuallyModifiedPaths.isEmpty()) { @@ -428,11 +402,11 @@ public class StashApplyCommand extends GitCommand<ObjectId> { } } - private void checkoutPath(DirCacheEntry entry, ObjectReader reader, - WorkingTreeOptions options, CheckoutMetadata checkoutMetadata) { + private void checkoutPath(DirCacheEntry entry, String gitPath, + ObjectReader reader, + Checkout checkout, CheckoutMetadata checkoutMetadata) { try { - DirCacheCheckout.checkoutEntry(repo, entry, reader, true, - checkoutMetadata, options); + checkout.checkout(entry, checkoutMetadata, reader, gitPath); } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().checkoutConflictWithFile, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java index 23fbe0197f..2dba0ef0f2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java @@ -165,7 +165,8 @@ public class StashDropCommand extends GitCommand<ObjectId> { List<ReflogEntry> entries; try { - ReflogReader reader = repo.getReflogReader(R_STASH); + ReflogReader reader = repo.getRefDatabase() + .getReflogReader(R_STASH); if (reader == null) { throw new RefNotFoundException(MessageFormat .format(JGitText.get().refNotResolved, stashRef)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashListCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashListCommand.java index 8171c4c3ae..828a3020f8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashListCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashListCommand.java @@ -43,7 +43,6 @@ public class StashListCommand extends GitCommand<Collection<RevCommit>> { super(repo); } - /** {@inheritDoc} */ @Override public Collection<RevCommit> call() throws GitAPIException, InvalidRefNameException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StatusCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StatusCommand.java index eab389460a..cdd078ea25 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StatusCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StatusCommand.java @@ -10,7 +10,7 @@ package org.eclipse.jgit.api; import java.io.IOException; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import org.eclipse.jgit.api.errors.GitAPIException; @@ -83,7 +83,7 @@ public class StatusCommand extends GitCommand<Status> { */ public StatusCommand addPath(String path) { if (paths == null) - paths = new LinkedList<>(); + paths = new ArrayList<>(); paths.add(path); return this; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java index 606d567393..5105dfc2e5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java @@ -66,6 +66,7 @@ public class SubmoduleAddCommand extends * Set the submodule name * * @param name + * name of the submodule * @return this command * @since 5.1 */ @@ -117,6 +118,7 @@ public class SubmoduleAddCommand extends * * @return true if submodule exists in index, false otherwise * @throws java.io.IOException + * if an IO error occurred */ protected boolean submoduleExists() throws IOException { TreeFilter filter = PathFilter.create(path); @@ -174,8 +176,9 @@ public class SubmoduleAddCommand extends CloneCommand clone = Git.cloneRepository(); configure(clone); clone.setDirectory(moduleDirectory); - clone.setGitDir(new File(new File(repo.getDirectory(), - Constants.MODULES), path)); + clone.setGitDir(new File( + new File(repo.getCommonDirectory(), Constants.MODULES), path)); + clone.setRelativePaths(true); clone.setURI(resolvedUri); if (monitor != null) clone.setProgressMonitor(monitor); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java index f4b8ac2e07..0aa1515334 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java @@ -61,6 +61,7 @@ public class SubmoduleDeinitCommand * Constructor of SubmoduleDeinitCommand * * @param repo + * repository this command works on */ public SubmoduleDeinitCommand(Repository repo) { super(repo); @@ -69,7 +70,6 @@ public class SubmoduleDeinitCommand /** * {@inheritDoc} - * <p> * * @return the set of repositories successfully deinitialized. * @throws NoSuchSubmoduleException @@ -135,6 +135,7 @@ public class SubmoduleDeinitCommand * @param path * the path to clean * @throws IOException + * if an IO error occurred */ private void deinit(String path) throws IOException { File dir = new File(repo.getWorkTree(), path); @@ -157,10 +158,14 @@ public class SubmoduleDeinitCommand * the parent repo's index or HEAD. * * @param revWalk + * used to walk commit graph * @param path + * path of the submodule * @return status of the command * @throws GitAPIException + * if JGit API failed * @throws IOException + * if an IO error occurred */ private SubmoduleDeinitStatus checkDirty(RevWalk revWalk, String path) throws GitAPIException, IOException { @@ -216,6 +221,7 @@ public class SubmoduleDeinitCommand * @return {@code true} if path exists and is a submodule in index, * {@code false} otherwise * @throws IOException + * if an IO error occurred */ private boolean submoduleExists(String path) throws IOException { TreeFilter filter = PathFilter.create(path); @@ -241,6 +247,7 @@ public class SubmoduleDeinitCommand * else it will refuse to do so. * * @param force + * execute the command forcefully if there are local modifications * @return {@code this} */ public SubmoduleDeinitCommand setForce(boolean force) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitResult.java index 8129be4a71..b7d7b66333 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitResult.java @@ -25,6 +25,7 @@ public class SubmoduleDeinitResult { * @param path * path of the submodule * @param status + * effect of a SubmoduleDeinitCommand's execution */ public SubmoduleDeinitResult(String path, SubmoduleDeinitCommand.SubmoduleDeinitStatus status) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleInitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleInitCommand.java index bdceabad37..03b4a000ad 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleInitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleInitCommand.java @@ -61,7 +61,6 @@ public class SubmoduleInitCommand extends GitCommand<Collection<String>> { return this; } - /** {@inheritDoc} */ @Override public Collection<String> call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleStatusCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleStatusCommand.java index 196ef7b2a9..d5bc0dda98 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleStatusCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleStatusCommand.java @@ -61,7 +61,6 @@ public class SubmoduleStatusCommand extends return this; } - /** {@inheritDoc} */ @Override public Map<String, SubmoduleStatus> call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java index b319a1b478..4f3e8512c3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java @@ -70,6 +70,7 @@ public class SubmoduleSyncCommand extends GitCommand<Map<String, String>> { * a {@link org.eclipse.jgit.lib.Repository} object. * @return shortened branch name, null on failures * @throws java.io.IOException + * if an IO error occurred */ protected String getHeadBranch(Repository subRepo) throws IOException { Ref head = subRepo.exactRef(Constants.HEAD); @@ -79,7 +80,6 @@ public class SubmoduleSyncCommand extends GitCommand<Map<String, String>> { return null; } - /** {@inheritDoc} */ @Override public Map<String, String> call() throws GitAPIException { checkCallable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java index df73164161..5e4b2ee0b7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java @@ -28,6 +28,7 @@ import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.internal.storage.file.LockFile; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; @@ -39,6 +40,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.submodule.SubmoduleWalk; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; +import org.eclipse.jgit.util.FileUtils; /** * A class used to execute a submodule update command. @@ -62,6 +64,8 @@ public class SubmoduleUpdateCommand extends private boolean fetch = false; + private boolean clonedRestored; + /** * <p> * Constructor for SubmoduleUpdateCommand. @@ -116,25 +120,77 @@ public class SubmoduleUpdateCommand extends return this; } + private static boolean submoduleExists(File gitDir) { + if (gitDir != null && gitDir.isDirectory()) { + File[] files = gitDir.listFiles(); + return files != null && files.length != 0; + } + return false; + } + + private static void restoreSubmodule(File gitDir, File workingTree) + throws IOException { + LockFile dotGitLock = new LockFile( + new File(workingTree, Constants.DOT_GIT)); + if (dotGitLock.lock()) { + String content = Constants.GITDIR + + getRelativePath(gitDir, workingTree); + dotGitLock.write(Constants.encode(content)); + dotGitLock.commit(); + } + } + + private static String getRelativePath(File gitDir, File workingTree) { + File relPath; + try { + relPath = workingTree.toPath().relativize(gitDir.toPath()) + .toFile(); + } catch (IllegalArgumentException e) { + relPath = gitDir; + } + return FileUtils.pathToString(relPath); + } + + private String determineUpdateMode(String mode) { + if (clonedRestored) { + return ConfigConstants.CONFIG_KEY_CHECKOUT; + } + return mode; + } + private Repository getOrCloneSubmodule(SubmoduleWalk generator, String url) throws IOException, GitAPIException { Repository repository = generator.getRepository(); + boolean restored = false; + boolean cloned = false; if (repository == null) { - if (callback != null) { - callback.cloningSubmodule(generator.getPath()); - } - CloneCommand clone = Git.cloneRepository(); - configure(clone); - clone.setURI(url); - clone.setDirectory(generator.getDirectory()); - clone.setGitDir( - new File(new File(repo.getDirectory(), Constants.MODULES), - generator.getPath())); - if (monitor != null) { - clone.setProgressMonitor(monitor); + File gitDir = new File( + new File(repo.getCommonDirectory(), Constants.MODULES), + generator.getPath()); + if (submoduleExists(gitDir)) { + restoreSubmodule(gitDir, generator.getDirectory()); + restored = true; + clonedRestored = true; + repository = generator.getRepository(); + } else { + if (callback != null) { + callback.cloningSubmodule(generator.getPath()); + } + CloneCommand clone = Git.cloneRepository(); + configure(clone); + clone.setURI(url); + clone.setDirectory(generator.getDirectory()); + clone.setGitDir(gitDir); + clone.setRelativePaths(true); + if (monitor != null) { + clone.setProgressMonitor(monitor); + } + repository = clone.call().getRepository(); + cloned = true; + clonedRestored = true; } - repository = clone.call().getRepository(); - } else if (this.fetch) { + } + if ((this.fetch || restored) && !cloned) { if (fetchCallback != null) { fetchCallback.fetchingSubmodule(generator.getPath()); } @@ -171,15 +227,17 @@ public class SubmoduleUpdateCommand extends continue; // Skip submodules not registered in parent repository's config String url = generator.getConfigUrl(); - if (url == null) + if (url == null) { continue; - + } + clonedRestored = false; try (Repository submoduleRepo = getOrCloneSubmodule(generator, url); RevWalk walk = new RevWalk(submoduleRepo)) { RevCommit commit = walk .parseCommit(generator.getObjectId()); - String update = generator.getConfigUpdate(); + String update = determineUpdateMode( + generator.getConfigUpdate()); if (ConfigConstants.CONFIG_KEY_MERGE.equals(update)) { MergeCommand merge = new MergeCommand(submoduleRepo); merge.include(commit); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java index 58c18b38d1..cc8589fa1c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java @@ -18,14 +18,11 @@ import org.eclipse.jgit.api.errors.InvalidTagNameException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.RefAlreadyExistsException; -import org.eclipse.jgit.api.errors.ServiceUnavailableException; import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.GpgConfig; import org.eclipse.jgit.lib.GpgConfig.GpgFormat; -import org.eclipse.jgit.lib.GpgObjectSigner; -import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; @@ -33,7 +30,8 @@ import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.lib.RepositoryState; +import org.eclipse.jgit.lib.Signer; +import org.eclipse.jgit.lib.Signers; import org.eclipse.jgit.lib.TagBuilder; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; @@ -51,13 +49,11 @@ import org.eclipse.jgit.transport.CredentialsProvider; * </pre> * <p> * - * <p> * Create a new unannotated tag for the current commit: * * <pre> * git.tag().setName("v1.0").setAnnotated(false).call(); * </pre> - * <p> * * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-tag.html" * >Git documentation about Tag</a> @@ -82,7 +78,7 @@ public class TagCommand extends GitCommand<Ref> { private GpgConfig gpgConfig; - private GpgObjectSigner gpgSigner; + private Signer signer; private CredentialsProvider credentialsProvider; @@ -110,9 +106,7 @@ public class TagCommand extends GitCommand<Ref> { public Ref call() throws GitAPIException, ConcurrentRefUpdateException, InvalidTagNameException, NoHeadException { checkCallable(); - - RepositoryState state = repo.getRepositoryState(); - processOptions(state); + processOptions(); try (RevWalk revWalk = new RevWalk(repo)) { // if no id is set, we should attempt to use HEAD @@ -138,9 +132,9 @@ public class TagCommand extends GitCommand<Ref> { newTag.setTagger(tagger); newTag.setObjectId(id); - if (gpgSigner != null) { - gpgSigner.signObject(newTag, signingKey, tagger, - credentialsProvider, gpgConfig); + if (signer != null) { + signer.signObject(repo, gpgConfig, newTag, tagger, signingKey, + credentialsProvider); } // write the tag object @@ -199,20 +193,14 @@ public class TagCommand extends GitCommand<Ref> { * Sets default values for not explicitly specified options. Then validates * that all required data has been provided. * - * @param state - * the state of the repository we are working on - * * @throws InvalidTagNameException * if the tag name is null or invalid - * @throws ServiceUnavailableException - * if the tag should be signed but no signer can be found * @throws UnsupportedSigningFormatException * if the tag should be signed but {@code gpg.format} is not * {@link GpgFormat#OPENPGP} */ - private void processOptions(RepositoryState state) - throws InvalidTagNameException, ServiceUnavailableException, - UnsupportedSigningFormatException { + private void processOptions() + throws InvalidTagNameException, UnsupportedSigningFormatException { if (name == null || !Repository.isValidRefName(Constants.R_TAGS + name)) { throw new InvalidTagNameException( @@ -238,16 +226,15 @@ public class TagCommand extends GitCommand<Ref> { doSign = gpgConfig.isSignAnnotated(); } if (doSign) { - if (signingKey == null) { - signingKey = gpgConfig.getSigningKey(); - } - if (gpgSigner == null) { - GpgSigner signer = GpgSigner.getDefault(); - if (!(signer instanceof GpgObjectSigner)) { - throw new ServiceUnavailableException( - JGitText.get().signingServiceUnavailable); + if (signer == null) { + signer = Signers.get(gpgConfig.getKeyFormat()); + if (signer == null) { + throw new UnsupportedSigningFormatException( + MessageFormat.format( + JGitText.get().signatureTypeUnknown, + gpgConfig.getKeyFormat() + .toConfigValue())); } - gpgSigner = (GpgObjectSigner) signer; } // The message of a signed tag must end in a newline because // the signature will be appended. @@ -334,22 +321,22 @@ public class TagCommand extends GitCommand<Ref> { } /** - * Sets the {@link GpgSigner} to use if the commit is to be signed. + * Sets the {@link Signer} to use if the commit is to be signed. * * @param signer * to use; if {@code null}, the default signer will be used * @return {@code this} - * @since 5.11 + * @since 7.0 */ - public TagCommand setGpgSigner(GpgObjectSigner signer) { + public TagCommand setSigner(Signer signer) { checkCallable(); - this.gpgSigner = signer; + this.signer = signer; return this; } /** * Sets an external {@link GpgConfig} to use. Whether it will be used is at - * the discretion of the {@link #setGpgSigner(GpgObjectSigner)}. + * the discretion of the {@link #setSigner(Signer)}. * * @param config * to set; if {@code null}, the config will be loaded from the diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java index 1af880d792..30f1bc9cc6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java @@ -22,7 +22,9 @@ import org.eclipse.jgit.transport.Transport; * {@link org.eclipse.jgit.api.TransportConfigCallback}. * * @param <C> + * concrete type of this {@code GitCommand} * @param <T> + * the return type of the {@code GitCommand}'s {@code call()} method */ public abstract class TransportCommand<C extends GitCommand, T> extends GitCommand<T> { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java index 21cddf75b7..f5f4b06e45 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java @@ -9,7 +9,7 @@ */ package org.eclipse.jgit.api; -import org.eclipse.jgit.lib.GpgSignatureVerifier; +import org.eclipse.jgit.lib.SignatureVerifier; import org.eclipse.jgit.revwalk.RevObject; /** @@ -34,8 +34,9 @@ public interface VerificationResult { * Retrieves the signature verification result. * * @return the result, or {@code null} if none was computed + * @since 7.0 */ - GpgSignatureVerifier.SignatureVerification getVerification(); + SignatureVerifier.SignatureVerification getVerification(); /** * Retrieves the git object of which the signature was verified. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java index 6a2a44ea2d..487ff04323 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java @@ -25,11 +25,10 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.GpgConfig; -import org.eclipse.jgit.lib.GpgSignatureVerifier; -import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; -import org.eclipse.jgit.lib.GpgSignatureVerifierFactory; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.SignatureVerifier.SignatureVerification; +import org.eclipse.jgit.lib.SignatureVerifiers; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; @@ -65,12 +64,8 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR private VerifyMode mode = VerifyMode.ANY; - private GpgSignatureVerifier verifier; - private GpgConfig config; - private boolean ownVerifier; - /** * Creates a new {@link VerifySignatureCommand} for the given {@link Repository}. * @@ -140,22 +135,7 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR } /** - * Sets the {@link GpgSignatureVerifier} to use. - * - * @param verifier - * the {@link GpgSignatureVerifier} to use, or {@code null} to - * use the default verifier - * @return {@code this} - */ - public VerifySignatureCommand setVerifier(GpgSignatureVerifier verifier) { - checkCallable(); - this.verifier = verifier; - return this; - } - - /** - * Sets an external {@link GpgConfig} to use. Whether it will be used it at - * the discretion of the {@link #setVerifier(GpgSignatureVerifier)}. + * Sets an external {@link GpgConfig} to use. * * @param config * to set; if {@code null}, the config will be loaded from the @@ -170,16 +150,6 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR } /** - * Retrieves the currently set {@link GpgSignatureVerifier}. Can be used - * after a successful {@link #call()} to get the verifier that was used. - * - * @return the {@link GpgSignatureVerifier} - */ - public GpgSignatureVerifier getVerifier() { - return verifier; - } - - /** * {@link Repository#resolve(String) Resolves} all names added to the * command to git objects and verifies their signature. Non-existing objects * are ignored. @@ -193,9 +163,6 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR * * @return a map of the given names to the corresponding * {@link VerificationResult}, excluding ignored or skipped objects. - * @throws ServiceUnavailableException - * if no {@link GpgSignatureVerifier} was set and no - * {@link GpgSignatureVerifierFactory} is available * @throws WrongObjectTypeException * if a name resolves to an object of a type not allowed by the * {@link #setMode(VerifyMode)} mode @@ -207,16 +174,6 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR checkCallable(); setCallable(false); Map<String, VerificationResult> result = new HashMap<>(); - if (verifier == null) { - GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory - .getDefault(); - if (factory == null) { - throw new ServiceUnavailableException( - JGitText.get().signatureVerificationUnavailable); - } - verifier = factory.getVerifier(); - ownVerifier = true; - } if (config == null) { config = new GpgConfig(repo.getConfig()); } @@ -239,10 +196,6 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR } catch (IOException e) { throw new JGitInternalException( JGitText.get().signatureVerificationError, e); - } finally { - if (ownVerifier) { - verifier.clear(); - } } return result; } @@ -258,8 +211,8 @@ public class VerifySignatureCommand extends GitCommand<Map<String, VerificationR } if (type == Constants.OBJ_COMMIT || type == Constants.OBJ_TAG) { try { - GpgSignatureVerifier.SignatureVerification verification = verifier - .verifySignature(object, config); + SignatureVerification verification = SignatureVerifiers + .verify(repo, config, object); if (verification == null) { // Not signed return null; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/CheckoutConflictException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/CheckoutConflictException.java index 3b71373b6e..5538711192 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/CheckoutConflictException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/CheckoutConflictException.java @@ -37,7 +37,7 @@ */ package org.eclipse.jgit.api.errors; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; /** @@ -94,11 +94,12 @@ public class CheckoutConflictException extends GitAPIException { * Adds a new conflicting path * * @param conflictingPath + * the new conflicting path * @return {@code this} */ CheckoutConflictException addConflictingPath(String conflictingPath) { if (conflictingPaths == null) - conflictingPaths = new LinkedList<>(); + conflictingPaths = new ArrayList<>(); conflictingPaths.add(conflictingPath); return this; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java index 470a92eaf5..9c4d8700a2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.attributes; +import org.eclipse.jgit.annotations.Nullable; + /** * Represents an attribute. * <p> @@ -97,7 +99,6 @@ public final class Attribute { this(key, State.CUSTOM, value); } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) @@ -140,11 +141,11 @@ public final class Attribute { * * @return the attribute value (may be <code>null</code>) */ + @Nullable public String getValue() { return value; } - /** {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; @@ -155,7 +156,6 @@ public final class Attribute { return result; } - /** {@inheritDoc} */ @Override public String toString() { switch (state) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java index 125ee5961a..08bc1da61e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java @@ -231,7 +231,6 @@ public final class Attributes { return true; } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder buf = new StringBuilder(); @@ -246,13 +245,11 @@ public final class Attributes { return buf.toString(); } - /** {@inheritDoc} */ @Override public int hashCode() { return map.hashCode(); } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java index 7ec78597fa..d8857f5192 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java @@ -69,6 +69,7 @@ public class AttributesHandler { * @param treeWalk * a {@link org.eclipse.jgit.treewalk.TreeWalk} * @throws java.io.IOException + * if an IO error occurred * @deprecated since 6.1, use {@link #AttributesHandler(TreeWalk, Supplier)} * instead */ @@ -87,6 +88,7 @@ public class AttributesHandler { * @param attributesTree * the tree to read .gitattributes from * @throws java.io.IOException + * if an IO error occurred * @since 6.1 */ public AttributesHandler(TreeWalk treeWalk, @@ -128,6 +130,7 @@ public class AttributesHandler { * current path represented by the * {@link org.eclipse.jgit.treewalk.TreeWalk} * @throws java.io.IOException + * if an IO error occurred */ public Attributes getAttributes() throws IOException { String entryPath = treeWalk.getPathString(); @@ -206,12 +209,16 @@ public class AttributesHandler { * @param isDirectory * true if the target item is a directory. * @param workingTreeIterator + * the working tree iterator * @param dirCacheIterator + * the dircache iterator * @param otherTree + * another tree * @param result * that will hold the attributes matching this entry path. This * method will NOT override any existing entry in attributes. * @throws IOException + * if an IO error occurred */ private void mergePerDirectoryEntryAttributes(String entryPath, int nameRoot, boolean isDirectory, @@ -357,9 +364,13 @@ public class AttributesHandler { * </p> * * @param treeWalk + * used to walk trees * @param workingTreeIterator + * used to walk the working tree * @param dirCacheIterator + * used to walk the dircache * @param otherTree + * another tree * @return a {@link AttributesNode} of the current entry, * {@link NullPointerException} otherwise. * @throws IOException diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java index 73fd587a14..2039191b8c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java @@ -187,7 +187,6 @@ public class AttributesRule { return match; } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandRegistry.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandRegistry.java index 1c9e9d7f71..bbbb437fd2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandRegistry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandRegistry.java @@ -41,8 +41,8 @@ public class FilterCommandRegistry { * the factory responsible for creating * {@link org.eclipse.jgit.attributes.FilterCommand}s for the * specified name - * @return the previous factory associated with <tt>commandName</tt>, or - * <tt>null</tt> if there was no mapping for <tt>commandName</tt> + * @return the previous factory associated with {@code commandName}, or + * {@code null} if there was no mapping for {@code commandName} */ public static FilterCommandFactory register(String filterCommandName, FilterCommandFactory factory) { @@ -55,8 +55,8 @@ public class FilterCommandRegistry { * * @param filterCommandName * the FilterCommandFactory's filter command name - * @return the previous factory associated with <tt>filterCommandName</tt>, - * or <tt>null</tt> if there was no mapping for <tt>commandName</tt> + * @return the previous factory associated with {@code filterCommandName}, + * or {@code null} if there was no mapping for {@code commandName} */ public static FilterCommandFactory unregister(String filterCommandName) { return filterCommandRegistry.remove(filterCommandName); @@ -69,7 +69,7 @@ public class FilterCommandRegistry { * * @param filterCommandName * the name for which the registry should be checked - * @return <code>true</code> if any factory was registered for the name + * @return {@code true} if any factory was registered for the name */ public static boolean isRegistered(String filterCommandName) { return filterCommandRegistry.containsKey(filterCommandName); @@ -107,6 +107,7 @@ public class FilterCommandRegistry { * @return the command if a command could be created or <code>null</code> if * there was no factory registered for that name * @throws java.io.IOException + * if an IO error occurred */ public static FilterCommand createFilterCommand(String filterCommandName, Repository db, InputStream in, OutputStream out) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java index 77967df2e5..979c8cef88 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java @@ -28,6 +28,8 @@ import org.eclipse.jgit.blame.Candidate.BlobCandidate; import org.eclipse.jgit.blame.Candidate.HeadCandidate; import org.eclipse.jgit.blame.Candidate.ReverseCandidate; import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; +import org.eclipse.jgit.blame.cache.BlameCache; +import org.eclipse.jgit.blame.cache.CacheRegion; import org.eclipse.jgit.diff.DiffAlgorithm; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; @@ -129,8 +131,19 @@ public class BlameGenerator implements AutoCloseable { /** Blame is currently assigned to this source. */ private Candidate outCandidate; + private Region outRegion; + private final BlameCache blameCache; + + /** + * Blame in reverse order needs the source lines, but we don't have them in + * the cache. We need to ignore the cache in that case. + */ + private boolean useCache = true; + + private final Stats stats = new Stats(); + /** * Create a blame generator for the repository and path (relative to * repository) @@ -142,6 +155,25 @@ public class BlameGenerator implements AutoCloseable { * repository). */ public BlameGenerator(Repository repository, String path) { + this(repository, path, null); + } + + /** + * Create a blame generator for the repository and path (relative to + * repository) + * + * @param repository + * repository to access revision data from. + * @param path + * initial path of the file to start scanning (relative to the + * repository). + * @param blameCache + * previously calculated blames. This generator will *not* + * populate it, just consume it. + * @since 7.2 + */ + public BlameGenerator(Repository repository, String path, + @Nullable BlameCache blameCache) { this.repository = repository; this.resultPath = PathFilter.create(path); @@ -150,6 +182,7 @@ public class BlameGenerator implements AutoCloseable { initRevPool(false); remaining = -1; + this.blameCache = blameCache; } private void initRevPool(boolean reverse) { @@ -159,10 +192,12 @@ public class BlameGenerator implements AutoCloseable { if (revPool != null) revPool.close(); - if (reverse) + if (reverse) { + useCache = false; revPool = new ReverseWalk(getRepository()); - else + } else { revPool = new RevWalk(getRepository()); + } SEEN = revPool.newFlag("SEEN"); //$NON-NLS-1$ reader = revPool.getObjectReader(); @@ -245,6 +280,31 @@ public class BlameGenerator implements AutoCloseable { } /** + * Stats about this generator + * + * @return the stats of this generator + * @since 7.2 + */ + public Stats getStats() { + return stats; + } + + /** + * Enable/disable the use of cache (if present). Enabled by default. + * <p> + * If caller need source line numbers, the generator cannot use the cache + * (source lines are not there). Use this method to disable the cache in + * that case. + * + * @param useCache + * should this generator use the cache. + * @since 7.2 + */ + public void setUseCache(boolean useCache) { + this.useCache = useCache; + } + + /** * Push a candidate blob onto the generator's traversal stack. * <p> * Candidates should be pushed in history order from oldest-to-newest. @@ -591,6 +651,7 @@ public class BlameGenerator implements AutoCloseable { Candidate n = pop(); if (n == null) return done(); + stats.candidatesVisited += 1; int pCnt = n.getParentCount(); if (pCnt == 1) { @@ -605,7 +666,7 @@ public class BlameGenerator implements AutoCloseable { // Do not generate a tip of a reverse. The region // survives and should not appear to be deleted. - } else /* if (pCnt == 0) */{ + } else /* if (pCnt == 0) */ { // Root commit, with at least one surviving region. // Assign the remaining blame here. return result(n); @@ -695,6 +756,27 @@ public class BlameGenerator implements AutoCloseable { } } + @Nullable + private Candidate blameFromCache(Candidate n) throws IOException { + if (blameCache == null || !useCache) { + return null; + } + + List<CacheRegion> cachedBlame = blameCache.get(repository, + n.sourceCommit, n.sourcePath.getPath()); + if (cachedBlame == null) { + return null; + } + BlameRegionMerger rb = new BlameRegionMerger(repository, revPool, + cachedBlame); + Candidate fullyBlamed = rb.mergeCandidate(n); + if (fullyBlamed == null) { + return null; + } + stats.cacheHit = true; + return fullyBlamed; + } + private boolean processOne(Candidate n) throws IOException { RevCommit parent = n.getParent(0); if (parent == null) @@ -717,12 +799,17 @@ public class BlameGenerator implements AutoCloseable { if (0 == r.getOldId().prefixCompare(n.sourceBlob)) { // A 100% rename without any content change can also // skip directly to the parent. + Candidate cached = blameFromCache(n); + if (cached != null) { + return result(cached); + } n.sourceCommit = parent; n.sourcePath = PathFilter.create(r.getOldPath()); push(n); return false; } + Candidate next = n.create(getRepository(), parent, PathFilter.create(r.getOldPath())); next.sourceBlob = r.getOldId().toObjectId(); @@ -759,6 +846,11 @@ public class BlameGenerator implements AutoCloseable { return false; } + Candidate cached = blameFromCache(source); + if (cached != null) { + return result(cached); + } + parent.takeBlame(editList, source); if (parent.regionList != null) push(parent); @@ -846,8 +938,8 @@ public class BlameGenerator implements AutoCloseable { editList = new EditList(0); } else { p.loadText(reader); - editList = diffAlgorithm.diff(textComparator, - p.sourceText, n.sourceText); + editList = diffAlgorithm.diff(textComparator, p.sourceText, + n.sourceText); } if (editList.isEmpty()) { @@ -981,6 +1073,10 @@ public class BlameGenerator implements AutoCloseable { /** * Get first line of the source data that has been blamed for the current * region + * <p> + * This value is not reliable when the generator is reusing cached values. + * Cache doesn't keep the source lines, the returned value is based on the + * result and can be off if the region moved in previous commits. * * @return first line of the source data that has been blamed for the * current region. This is line number of where the region was added @@ -994,6 +1090,10 @@ public class BlameGenerator implements AutoCloseable { /** * Get one past the range of the source data that has been blamed for the * current region + * <p> + * This value is not reliable when the generator is reusing cached values. + * Cache doesn't keep the source lines, the returned value is based on the + * result and can be off if the region moved in previous commits. * * @return one past the range of the source data that has been blamed for * the current region. This is line number of where the region was @@ -1124,4 +1224,39 @@ public class BlameGenerator implements AutoCloseable { return ent.getChangeType() == ChangeType.RENAME || ent.getChangeType() == ChangeType.COPY; } + + /** + * Stats about the work done by the generator + * + * @since 7.2 + */ + public static class Stats { + + /** Candidates taken from the queue */ + private int candidatesVisited; + + private boolean cacheHit; + + /** + * Number of candidates taken from the queue + * <p> + * The generator could signal it's done without exhausting all + * candidates if there is no more remaining lines or the last visited + * candidate is found in the cache. + * + * @return number of candidates taken from the queue + */ + public int getCandidatesVisited() { + return candidatesVisited; + } + + /** + * The generator found a blamed version in the cache + * + * @return true if we used results from the cache + */ + public boolean isCacheHit() { + return cacheHit; + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameRegionMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameRegionMerger.java new file mode 100644 index 0000000000..67bc6fb789 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameRegionMerger.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025, Google LLC. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.blame; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jgit.blame.cache.CacheRegion; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; + +/** + * Translates an unblamed region into one or more blamed regions, using the + * fully blamed data from cache. + * <p> + * Blamed and unblamed regions are not symmetrical: An unblamed region is just a + * range of lines over the file. A blamed region is a Candidate (with the commit + * info) with a region inside (the range blamed). + */ +class BlameRegionMerger { + private final Repository repo; + + private final List<CacheRegion> cachedRegions; + + private final RevWalk rw; + + BlameRegionMerger(Repository repo, RevWalk rw, + List<CacheRegion> cachedRegions) { + this.repo = repo; + List<CacheRegion> sorted = new ArrayList<>(cachedRegions); + Collections.sort(sorted); + this.cachedRegions = sorted; + this.rw = rw; + } + + /** + * Return one or more candidates blaming all the regions of the "unblamed" + * incoming candidate. + * + * @param candidate + * a candidate with a list of unblamed regions + * @return A linked list of Candidates with their blamed regions, null if + * there was any error. + */ + Candidate mergeCandidate(Candidate candidate) { + List<Candidate> newCandidates = new ArrayList<>(); + Region r = candidate.regionList; + while (r != null) { + try { + newCandidates.addAll(mergeOneRegion(r)); + } catch (IOException e) { + return null; + } + r = r.next; + } + return asLinkedCandidate(newCandidates); + } + + // Visible for testing + List<Candidate> mergeOneRegion(Region region) throws IOException { + List<CacheRegion> overlaps = findOverlaps(region); + if (overlaps.isEmpty()) { + throw new IOException( + "Cached blame should cover all lines"); + } + /* + * Cached regions cover the whole file. We find first which ones overlap + * with our unblamed region. Then we take the overlapping portions with + * the corresponding blame. + */ + List<Candidate> candidates = new ArrayList<>(); + for (CacheRegion overlap : overlaps) { + Region blamedRegions = intersectRegions(region, overlap); + Candidate c = new Candidate(repo, parse(overlap.getSourceCommit()), + PathFilter.create(overlap.getSourcePath())); + c.regionList = blamedRegions; + candidates.add(c); + } + return candidates; + } + + // Visible for testing + List<CacheRegion> findOverlaps(Region unblamed) { + int unblamedStart = unblamed.sourceStart; + int unblamedEnd = unblamedStart + unblamed.length; + List<CacheRegion> overlapping = new ArrayList<>(); + for (CacheRegion blamed : cachedRegions) { + // End is not included + if (blamed.getEnd() <= unblamedStart) { + // Blamed region is completely before + continue; + } + + if (blamed.getStart() >= unblamedEnd) { + // Blamed region is completely after + // Blamed regions are sorted by start position, nothing will + // match anymore + break; + } + overlapping.add(blamed); + } + return overlapping; + } + + // Visible for testing + /** + * Calculate the intersection between a Region and a CacheRegion, adjusting + * the start if needed. + * <p> + * This should be called only if there is an overlap (filtering the cached + * regions with {@link #findOverlaps(Region)}), otherwise the result is + * meaningless. + * + * @param unblamed + * a region from the blame generator + * @param cached + * a cached region + * @return a new region with the intersection. + */ + static Region intersectRegions(Region unblamed, CacheRegion cached) { + int blamedStart = Math.max(cached.getStart(), unblamed.sourceStart); + int blamedEnd = Math.min(cached.getEnd(), + unblamed.sourceStart + unblamed.length); + int length = blamedEnd - blamedStart; + + // result start and source start should move together + int blameStartDelta = blamedStart - unblamed.sourceStart; + return new Region(unblamed.resultStart + blameStartDelta, blamedStart, + length); + } + + // Tests can override this, so they don't need a real repo, commit and walk + protected RevCommit parse(ObjectId oid) throws IOException { + return rw.parseCommit(oid); + } + + private static Candidate asLinkedCandidate(List<Candidate> c) { + Candidate head = c.get(0); + Candidate tail = head; + for (int i = 1; i < c.size(); i++) { + tail.queueNext = c.get(i); + tail = tail.queueNext; + } + return head; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java index c854b37b84..5e2746cc7c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java @@ -303,7 +303,6 @@ public class BlameResult { } } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder r = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java index ca5370e912..8e2aaecf94 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java @@ -12,6 +12,7 @@ package org.eclipse.jgit.blame; import java.io.IOException; import java.util.List; +import java.util.Objects; import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; import org.eclipse.jgit.diff.Edit; @@ -269,7 +270,7 @@ class Candidate { } boolean canMergeRegions(Candidate other) { - return sourceCommit == other.sourceCommit + return Objects.equals(sourceCommit, other.sourceCommit) && sourcePath.getPath().equals(other.sourcePath.getPath()); } @@ -304,7 +305,6 @@ class Candidate { } } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java index 2236eecbfe..c481eb1927 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Region.java @@ -83,7 +83,6 @@ class Region { return head; } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder buf = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java index e8f3f38adb..fafc4fb1ec 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/ReverseWalk.java @@ -24,7 +24,6 @@ final class ReverseWalk extends RevWalk { super(repo); } - /** {@inheritDoc} */ @Override public ReverseCommit next() throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -36,7 +35,6 @@ final class ReverseWalk extends RevWalk { return c; } - /** {@inheritDoc} */ @Override protected RevCommit createCommit(AnyObjectId id) { return new ReverseCommit(id); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java new file mode 100644 index 0000000000..d44fb5f62b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025, Google LLC. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.blame.cache; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; + +/** + * Keeps the blame information for a path at certain commit. + * <p> + * If there is a result, it covers the whole file at that revision + * + * @since 7.2 + */ +public interface BlameCache { + /** + * Gets the blame of a path at a given commit if available. + * <p> + * Since this cache is used in blame calculation, this get() method should + * only retrieve the cache value, and not re-trigger blame calculation. In + * other words, this acts as "getIfPresent", and not "computeIfAbsent". + * + * @param repo + * repository containing the commit + * @param commitId + * we are looking at the file in this revision + * @param path + * path a file in the repo + * + * @return the blame of a path at a given commit or null if not in cache + * @throws IOException + * error retrieving/parsing values from storage + */ + List<CacheRegion> get(Repository repo, ObjectId commitId, String path) + throws IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java new file mode 100644 index 0000000000..cf3f978044 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2025, Google LLC. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.blame.cache; + +import java.text.MessageFormat; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ObjectId; + +/** + * Region of the blame of a file. + * <p> + * Usually all parameters are non-null, except when the Region was created + * to fill an unblamed gap (to cover for bugs in the calculation). In that + * case, path, commit and author will be null. + * + * @since 7.2 + **/ +public class CacheRegion implements Comparable<CacheRegion> { + private final String sourcePath; + + private final ObjectId sourceCommit; + + private final int end; + + private final int start; + + /** + * A blamed portion of a file + * + * @param path + * location of the file + * @param commit + * commit that is modifying this region + * @param start + * first line of this region (inclusive) + * @param end + * last line of this region (non-inclusive!) + */ + public CacheRegion(String path, ObjectId commit, + int start, int end) { + allOrNoneNull(path, commit); + this.sourcePath = path; + this.sourceCommit = commit; + this.start = start; + this.end = end; + } + + /** + * First line of this region. Starting by 0, inclusive + * + * @return first line of this region. + */ + public int getStart() { + return start; + } + + /** + * One after last line in this region (or: last line non-inclusive) + * + * @return one after last line in this region. + */ + public int getEnd() { + return end; + } + + + /** + * Path of the file this region belongs to + * + * @return path in the repo/commit + */ + public String getSourcePath() { + return sourcePath; + } + + /** + * Commit this region belongs to + * + * @return commit for this region + */ + public ObjectId getSourceCommit() { + return sourceCommit; + } + + @Override + public int compareTo(CacheRegion o) { + return start - o.start; + } + + @SuppressWarnings("nls") + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (sourceCommit != null) { + sb.append(sourceCommit.name(), 0, 7).append(' ') + .append(" (") + .append(sourcePath).append(')'); + } else { + sb.append("<unblamed region>"); + } + sb.append(' ').append("start=").append(start).append(", count=") + .append(end - start); + return sb.toString(); + } + + private static void allOrNoneNull(String path, ObjectId commit) { + if (path != null && commit != null) { + return; + } + + if (path == null && commit == null) { + return; + } + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().cacheRegionAllOrNoneNull, path, commit)); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffAlgorithm.java index 2526cbfae4..7821efd2a6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffAlgorithm.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffAlgorithm.java @@ -58,6 +58,8 @@ public abstract class DiffAlgorithm { /** * Compare two sequences and identify a list of edits between them. * + * @param <S> + * type of sequence being compared. * @param cmp * the comparator supplying the element equivalence function. * @param a @@ -217,6 +219,8 @@ public abstract class DiffAlgorithm { * method, which invokes this method using * {@link org.eclipse.jgit.diff.Subsequence}s. * + * @param <S> + * type of sequence being compared. * @param cmp * the comparator supplying the element equivalence function. * @param a diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffDriver.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffDriver.java new file mode 100644 index 0000000000..b74444400e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffDriver.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024 Qualcomm Innovation Center, 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 v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.diff; + +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Built-in drivers for various languages, sorted by name. These drivers will be + * used to determine function names for a hunk. + * <p> + * When writing or updating patterns, assume the contents are syntactically + * correct. Patterns can be simple and need not cover all syntactical corner + * cases, as long as they are sufficiently permissive. + * + * @since 6.10.1 + */ +@SuppressWarnings({"ImmutableEnumChecker", "nls"}) +public enum DiffDriver { + /** + * Built-in diff driver for <a href= + * "https://learn.microsoft.com/en-us/cpp/cpp/cpp-language-reference">c++</a> + */ + cpp(List.of( + /* Jump targets or access declarations */ + "^[ \\t]*[A-Za-z_][A-Za-z_0-9]*:\\s*($|/[/*])"), List.of( + /* functions/methods, variables, and compounds at top level */ + "^((::\\s*)?[A-Za-z_].*)$")), + /** + * Built-in diff driver for <a href= + * "https://devicetree-specification.readthedocs.io/en/stable/source-language.html">device + * tree files</a> + */ + dts(List.of(";", "="), List.of( + /* lines beginning with a word optionally preceded by '&' or the root */ + "^[ \\t]*((/[ \\t]*\\{|&?[a-zA-Z_]).*)")), + /** + * Built-in diff driver for <a href= + * "https://docs.oracle.com/javase/specs/jls/se21/html/index.html">java</a> + */ + java(List.of( + "^[ \\t]*(catch|do|for|if|instanceof|new|return|switch|throw|while)"), + List.of( + /* Class, enum, interface, and record declarations */ + "^[ \\t]*(([a-z-]+[ \\t]+)*(class|enum|interface|record)[ \\t]+.*)$", + /* Method definitions; note that constructor signatures are not */ + /* matched because they are indistinguishable from method calls. */ + "^[ \\t]*(([A-Za-z_<>&\\]\\[][?&<>.,A-Za-z_0-9]*[ \\t]+)+[A-Za-z_]" + + "[A-Za-z_0-9]*[ \\t]*\\([^;]*)$")), + /** + * Built-in diff driver for + * <a href="https://docs.python.org/3/reference/index.html">python</a> + */ + python(List.of("^[ \\t]*((class|(async[ \\t]+)?def)[ \\t].*)$")), + /** + * Built-in diff driver for + * <a href="https://doc.rust-lang.org/reference/introduction.html">rust</a> + */ + rust(List.of("^[\\t ]*((pub(\\([^\\)]+\\))?[\\t ]+)?" + + "((async|const|unsafe|extern([\\t ]+\"[^\"]+\"))[\\t ]+)?" + + "(struct|enum|union|mod|trait|fn|impl|macro_rules!)[< \\t]+[^;]*)$")); + + private final List<Pattern> negatePatterns; + + private final List<Pattern> matchPatterns; + + DiffDriver(List<String> negate, List<String> match, int flags) { + if (negate != null) { + this.negatePatterns = negate.stream() + .map(r -> Pattern.compile(r, flags)) + .collect(Collectors.toList()); + } else { + this.negatePatterns = null; + } + this.matchPatterns = match.stream().map(r -> Pattern.compile(r, flags)) + .collect(Collectors.toList()); + } + + DiffDriver(List<String> match) { + this(null, match, 0); + } + + DiffDriver(List<String> negate, List<String> match) { + this(negate, match, 0); + } + + /** + * Returns the list of patterns used to exclude certain lines from being + * considered as function names. + * + * @return the list of negate patterns + */ + public List<Pattern> getNegatePatterns() { + return negatePatterns; + } + + /** + * Returns the list of patterns used to match lines for potential function + * names. + * + * @return the list of match patterns + */ + public List<Pattern> getMatchPatterns() { + return matchPatterns; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java index f0ce121f73..f8c5399983 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java @@ -379,6 +379,8 @@ public class DiffEntry { } /** + * Get diff attribute + * * @return the {@link Attribute} determining filters to be applied. * @since 4.11 */ @@ -457,7 +459,6 @@ public class DiffEntry { /** * Whether the mark tree filter with the specified index matched during scan * or not, see {@link #scan(TreeWalk, boolean, TreeFilter...)}. Example: - * <p> * * <pre> * TreeFilter filterA = ...; @@ -506,7 +507,6 @@ public class DiffEntry { return side == Side.OLD ? getOldId() : getNewId(); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java index 1a5f74f98a..cbac3f90b7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java @@ -30,7 +30,9 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.attributes.Attribute; import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.dircache.DirCacheIterator; @@ -703,7 +705,7 @@ public class DiffFormatter implements AutoCloseable { */ public void format(DiffEntry ent) throws IOException { FormatResult res = createFormatResult(ent); - format(res.header, res.a, res.b); + format(res.header, res.a, res.b, getDiffDriver(ent)); } private static byte[] writeGitLinkText(AbbreviatedObjectId id) { @@ -749,11 +751,14 @@ public class DiffFormatter implements AutoCloseable { * text source for the post-image version of the content. This * must match the content of * {@link org.eclipse.jgit.patch.FileHeader#getNewId()}. + * @param diffDriver + * the diff driver used to obtain function names in hunk headers * @throws java.io.IOException - * writing to the supplied stream failed. + * writing to the supplied stream failed. + * @since 6.10.1 */ - public void format(FileHeader head, RawText a, RawText b) - throws IOException { + public void format(FileHeader head, RawText a, RawText b, + DiffDriver diffDriver) 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. @@ -763,8 +768,49 @@ public class DiffFormatter implements AutoCloseable { if (!head.getHunks().isEmpty()) end = head.getHunks().get(0).getStartOffset(); out.write(head.getBuffer(), start, end - start); - if (head.getPatchType() == PatchType.UNIFIED) - format(head.toEditList(), a, b); + if (head.getPatchType() == PatchType.UNIFIED) { + format(head.toEditList(), a, b, diffDriver); + } + } + + /** + * 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 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 org.eclipse.jgit.patch.FileHeader#getOldId()}. + * @param b + * text source for the post-image version of the content. This must match + * the content of {@link org.eclipse.jgit.patch.FileHeader#getNewId()}. + * @throws java.io.IOException + * writing to the supplied stream failed. + */ + public void format(FileHeader head, RawText a, RawText b) + throws IOException { + format(head, a, b, null); + } + + /** + * Formats a list of edits in unified diff format + * + * @param edits + * some differences which have been calculated between A and B + * @param a + * the text A which was compared + * @param b + * the text B which was compared + * @throws java.io.IOException + * if an IO error occurred + */ + public void format(EditList edits, RawText a, RawText b) + throws IOException { + format(edits, a, b, null); } /** @@ -776,10 +822,14 @@ public class DiffFormatter implements AutoCloseable { * the text A which was compared * @param b * the text B which was compared + * @param diffDriver + * the diff driver used to obtain function names in hunk headers * @throws java.io.IOException + * if an IO error occurred + * @since 6.10.1 */ - public void format(EditList edits, RawText a, RawText b) - throws IOException { + public void format(EditList edits, RawText a, RawText b, + DiffDriver diffDriver) throws IOException { for (int curIdx = 0; curIdx < edits.size();) { Edit curEdit = edits.get(curIdx); final int endIdx = findCombinedEnd(edits, curIdx); @@ -790,7 +840,8 @@ public class DiffFormatter implements AutoCloseable { final int aEnd = (int) Math.min(a.size(), (long) endEdit.getEndA() + context); final int bEnd = (int) Math.min(b.size(), (long) endEdit.getEndB() + context); - writeHunkHeader(aCur, aEnd, bCur, bEnd); + writeHunkHeader(aCur, aEnd, bCur, bEnd, + getFuncName(a, aCur - 1, diffDriver)); while (aCur < aEnd || bCur < bEnd) { if (aCur < curEdit.getBeginA() || endIdx + 1 < curIdx) { @@ -825,6 +876,7 @@ public class DiffFormatter implements AutoCloseable { * @param line * the line number within text * @throws java.io.IOException + * if an IO error occurred */ protected void writeContextLine(RawText text, int line) throws IOException { @@ -843,6 +895,7 @@ public class DiffFormatter implements AutoCloseable { * @param line * the line number within text * @throws java.io.IOException + * if an IO error occurred */ protected void writeAddedLine(RawText text, int line) throws IOException { @@ -857,6 +910,7 @@ public class DiffFormatter implements AutoCloseable { * @param line * the line number within text * @throws java.io.IOException + * if an IO error occurred */ protected void writeRemovedLine(RawText text, int line) throws IOException { @@ -875,9 +929,32 @@ public class DiffFormatter implements AutoCloseable { * @param bEndLine * within second source * @throws java.io.IOException + * if an IO error occurred */ - protected void writeHunkHeader(int aStartLine, int aEndLine, - int bStartLine, int bEndLine) throws IOException { + protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, + int bEndLine) throws IOException { + writeHunkHeader(aStartLine, aEndLine, bStartLine, bEndLine, null); + } + + /** + * Output a hunk header + * + * @param aStartLine + * within first source + * @param aEndLine + * within first source + * @param bStartLine + * within second source + * @param bEndLine + * within second source + * @param funcName + * function name of this hunk + * @throws java.io.IOException + * if an IO error occurred + * @since 6.10.1 + */ + protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, + int bEndLine, String funcName) throws IOException { out.write('@'); out.write('@'); writeRange('-', aStartLine + 1, aEndLine - aStartLine); @@ -885,6 +962,10 @@ public class DiffFormatter implements AutoCloseable { out.write(' '); out.write('@'); out.write('@'); + if (funcName != null) { + out.write(' '); + out.write(funcName.getBytes()); + } out.write('\n'); } @@ -1242,4 +1323,50 @@ public class DiffFormatter implements AutoCloseable { private static boolean end(Edit edit, int a, int b) { return edit.getEndA() <= a && edit.getEndB() <= b; } + + private String getFuncName(RawText text, int startAt, + DiffDriver diffDriver) { + if (diffDriver != null) { + while (startAt > 0) { + String line = text.getString(startAt); + startAt--; + if (matchesAny(diffDriver.getNegatePatterns(), line)) { + continue; + } + if (matchesAny(diffDriver.getMatchPatterns(), line)) { + String funcName = line.replaceAll("^[ \\t]+", ""); //$NON-NLS-1$//$NON-NLS-2$ + return funcName.substring(0, + Math.min(funcName.length(), 80)).trim(); + } + } + } + return null; + } + + private boolean matchesAny(List<Pattern> patterns, String text) { + if (patterns != null) { + for (Pattern p : patterns) { + if (p.matcher(text).find()) { + return true; + } + } + } + return false; + } + + private DiffDriver getDiffDriver(DiffEntry entry) { + Attribute diffAttr = entry.getDiffAttribute(); + if (diffAttr == null) { + return null; + } + String diffAttrValue = diffAttr.getValue(); + if (diffAttrValue == null) { + return null; + } + try { + return DiffDriver.valueOf(diffAttrValue); + } catch (IllegalArgumentException e) { + return null; + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java index 219a187e27..2968dbaa8f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java @@ -238,13 +238,11 @@ public class Edit { endB = sEnd; } - /** {@inheritDoc} */ @Override public int hashCode() { return beginA ^ endA; } - /** {@inheritDoc} */ @Override public boolean equals(Object o) { if (o instanceof Edit) { @@ -255,7 +253,6 @@ public class Edit { return false; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/EditList.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/EditList.java index d40a64099a..85e23e9a92 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/EditList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/EditList.java @@ -49,7 +49,6 @@ public class EditList extends ArrayList<Edit> { super(capacity); } - /** {@inheritDoc} */ @Override public String toString() { return "EditList" + super.toString(); //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/HashedSequence.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/HashedSequence.java index 9cb8e7364b..82ab21c11e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/HashedSequence.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/HashedSequence.java @@ -34,7 +34,6 @@ public final class HashedSequence<S extends Sequence> extends Sequence { this.hashes = hashes; } - /** {@inheritDoc} */ @Override public int size() { return base.size(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/HashedSequenceComparator.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/HashedSequenceComparator.java index 0380208565..c7dfd9a18f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/HashedSequenceComparator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/HashedSequenceComparator.java @@ -33,7 +33,6 @@ public final class HashedSequenceComparator<S extends Sequence> extends this.cmp = cmp; } - /** {@inheritDoc} */ @Override public boolean equals(HashedSequence<S> a, int ai, // HashedSequence<S> b, int bi) { @@ -41,7 +40,6 @@ public final class HashedSequenceComparator<S extends Sequence> extends && cmp.equals(a.base, ai, b.base, bi); } - /** {@inheritDoc} */ @Override public int hash(HashedSequence<S> seq, int ptr) { return seq.hashes[ptr]; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/HistogramDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/HistogramDiff.java index 4035a1e48e..bb72e1faed 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/HistogramDiff.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/HistogramDiff.java @@ -98,7 +98,6 @@ public class HistogramDiff extends LowLevelDiffAlgorithm { maxChainLength = maxLen; } - /** {@inheritDoc} */ @Override public <S extends Sequence> void diffNonCommon(EditList edits, HashedSequenceComparator<S> cmp, HashedSequence<S> a, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/LowLevelDiffAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/LowLevelDiffAlgorithm.java index 7f3c251864..39be43d12d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/LowLevelDiffAlgorithm.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/LowLevelDiffAlgorithm.java @@ -14,7 +14,6 @@ package org.eclipse.jgit.diff; * Compares two sequences primarily based upon hash codes. */ public abstract class LowLevelDiffAlgorithm extends DiffAlgorithm { - /** {@inheritDoc} */ @Override public <S extends Sequence> EditList diffNonCommon( SequenceComparator<? super S> cmp, S a, S b) { @@ -40,6 +39,8 @@ public abstract class LowLevelDiffAlgorithm extends DiffAlgorithm { * method, which invokes this method using * {@link org.eclipse.jgit.diff.Subsequence}s. * + * @param <S> + * type of Sequence compared * @param edits * result list to append the region's edits onto. * @param cmp diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/MyersDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/MyersDiff.java index cf6826ab92..bedb0b335c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/MyersDiff.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/MyersDiff.java @@ -11,6 +11,10 @@ package org.eclipse.jgit.diff; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.text.MessageFormat; import org.eclipse.jgit.errors.DiffInterruptedException; @@ -330,7 +334,7 @@ if (k < beginK || k > endK) abstract boolean meets(int d, int k, int x, long snake); final long newSnake(int k, int x) { - long y = k + x; + long y = (long) k + x; long ret = ((long) x) << 32; return ret | y; } @@ -531,7 +535,7 @@ if (k < beginK || k > endK) */ public static void main(String[] args) { if (args.length != 2) { - System.err.println(JGitText.get().need2Arguments); + err().println(JGitText.get().need2Arguments); System.exit(1); } try { @@ -540,7 +544,13 @@ if (k < beginK || k > endK) EditList r = INSTANCE.diff(RawTextComparator.DEFAULT, a, b); System.out.println(r.toString()); } catch (Exception e) { - e.printStackTrace(); + PrintWriter err = err(); + err.println(e.getMessage()); + e.printStackTrace(err); } } + + private static PrintWriter err() { + return new PrintWriter(new OutputStreamWriter(System.err, UTF_8)); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java index 53cd2ea5d7..b401bbe73d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java @@ -43,14 +43,12 @@ public class PatchIdDiffFormatter extends DiffFormatter { return ObjectId.fromRaw(digest.digest()); } - /** {@inheritDoc} */ @Override - protected void writeHunkHeader(int aStartLine, int aEndLine, - int bStartLine, int bEndLine) throws IOException { + protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, + int bEndLine, String funcName) throws IOException { // The hunk header is not taken into account for patch id calculation } - /** {@inheritDoc} */ @Override protected void formatIndexLine(OutputStream o, DiffEntry ent) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java index b52803513d..fdfe533618 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java @@ -106,6 +106,8 @@ public class RawText extends Sequence { } /** + * Get the raw content + * * @return the raw, unprocessed content read. * @since 4.11 */ @@ -114,7 +116,6 @@ public class RawText extends Sequence { } /** @return total number of items in the sequence. */ - /** {@inheritDoc} */ @Override public int size() { // The line map is always 2 entries larger than the number of lines in @@ -359,18 +360,22 @@ public class RawText extends Sequence { length = maxLength; isComplete = false; } - byte last = 'x'; // Just something inconspicuous. - for (int ptr = 0; ptr < length; ptr++) { - byte curr = raw[ptr]; - if (isBinary(curr, last)) { + + int ptr = -1; + byte current; + while (ptr < length - 2) { + current = raw[++ptr]; + if (current == '\0' || (current == '\r' && raw[++ptr] != '\n')) { return true; } - last = curr; } - if (isComplete) { - // Buffer contains everything... - return last == '\r'; // ... so this must be a lone CR + + if (ptr == length - 2) { + // if '\r' be last, then if isComplete then return binary + current = raw[++ptr]; + return current == '\0' || (current == '\r' && isComplete); } + return false; } @@ -466,26 +471,30 @@ public class RawText extends Sequence { */ public static boolean isCrLfText(byte[] raw, int length, boolean complete) { boolean has_crlf = false; - byte last = 'x'; // Just something inconspicuous - for (int ptr = 0; ptr < length; ptr++) { - byte curr = raw[ptr]; - if (isBinary(curr, last)) { + + int ptr = -1; + byte current; + while (ptr < length - 2) { + current = raw[++ptr]; + if (current == '\0') { return false; } - if (curr == '\n' && last == '\r') { + if (current == '\r') { + if (raw[++ptr] != '\n') { + return false; + } has_crlf = true; } - last = curr; } - if (last == '\r') { - if (complete) { - // Lone CR: it's binary after all. + + if (ptr == length - 2) { + // if '\r' be last, then if isComplete then return binary + current = raw[++ptr]; + if (current == '\0' || (current == '\r' && complete)) { return false; } - // Tough call. If the next byte, which we don't have, would be a - // '\n', it'd be a CR-LF text, otherwise it'd be binary. Just decide - // based on what we already scanned; it wasn't binary until now. } + return has_crlf; } @@ -577,4 +586,5 @@ public class RawText extends Sequence { return new RawText(data, RawParseUtils.lineMapOrBinary(data, 0, (int) sz)); } } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawTextComparator.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawTextComparator.java index 0c41b8598b..e06e2f8290 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawTextComparator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawTextComparator.java @@ -227,7 +227,6 @@ public abstract class RawTextComparator extends SequenceComparator<RawText> { return hashRegion(seq.content, begin, end); } - /** {@inheritDoc} */ @Override public Edit reduceCommonStartEnd(RawText a, RawText b, Edit e) { // This is a faster exact match based form that tries to improve diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java index c33f53adde..fd84bc6c87 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java @@ -713,15 +713,16 @@ public class RenameDetector { /** * Find the best match by file path for a given DiffEntry from a list of - * DiffEntrys. The returned DiffEntry will be of the same type as <src>. If - * no DiffEntry can be found that has the same type, this method will return - * null. + * DiffEntrys. The returned DiffEntry will be of the same type as + * <src>. If no DiffEntry can be found that has the same type, this + * method will return null. * * @param src * the DiffEntry to try to find a match for * @param list * a list of DiffEntrys to search through - * @return the DiffEntry from <list> who's file path best matches <src> + * @return the DiffEntry from <list> who's file path best matches + * <src> */ private static DiffEntry bestPathMatch(DiffEntry src, List<DiffEntry> list) { DiffEntry best = null; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java index 9ac94895b1..fb98df7c9e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java @@ -80,7 +80,7 @@ class SimilarityRenameDetector { private long[] matrix; /** Score a pair must exceed to be considered a rename. */ - private int renameScore = 60; + private int renameScore = 50; /** * File size threshold (in bytes) for detecting renames. Files larger @@ -407,6 +407,7 @@ class SimilarityRenameDetector { | encodeFile(dstIdx); } + @SuppressWarnings("IntLongMath") private static long encodeFile(int idx) { // We invert the index so that the first file in the list sorts // later in the table. This permits us to break ties favoring @@ -415,6 +416,7 @@ class SimilarityRenameDetector { return INDEX_MASK - idx; } + @SuppressWarnings("IntLongMath") private static int decodeFile(int v) { return INDEX_MASK - v; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/Subsequence.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Subsequence.java index 61f133ca55..878c66b30f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/Subsequence.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Subsequence.java @@ -25,6 +25,8 @@ public final class Subsequence<S extends Sequence> extends Sequence { /** * Construct a subsequence around the A region/base sequence. * + * @param <S> + * type of returned Sequence * @param a * the A sequence. * @param region @@ -38,6 +40,8 @@ public final class Subsequence<S extends Sequence> extends Sequence { /** * Construct a subsequence around the B region/base sequence. * + * @param <S> + * type of returned Sequence * @param b * the B sequence. * @param region @@ -51,6 +55,8 @@ public final class Subsequence<S extends Sequence> extends Sequence { /** * Adjust the Edit to reflect positions in the base sequence. * + * @param <S> + * type of returned Sequence * @param e * edit to adjust in-place. Prior to invocation the indexes are * in terms of the two subsequences; after invocation the indexes @@ -72,6 +78,8 @@ public final class Subsequence<S extends Sequence> extends Sequence { /** * Adjust the Edits to reflect positions in the base sequence. * + * @param <S> + * type of returned Sequence * @param edits * edits to adjust in-place. Prior to invocation the indexes are * in terms of the two subsequences; after invocation the indexes @@ -116,7 +124,6 @@ public final class Subsequence<S extends Sequence> extends Sequence { this.size = end - begin; } - /** {@inheritDoc} */ @Override public int size() { return size; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SubsequenceComparator.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SubsequenceComparator.java index c60945361f..707df59259 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SubsequenceComparator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SubsequenceComparator.java @@ -36,13 +36,11 @@ public final class SubsequenceComparator<S extends Sequence> extends this.cmp = cmp; } - /** {@inheritDoc} */ @Override public boolean equals(Subsequence<S> a, int ai, Subsequence<S> b, int bi) { return cmp.equals(a.base, ai + a.begin, b.base, bi + b.begin); } - /** {@inheritDoc} */ @Override public int hash(Subsequence<S> seq, int ptr) { return cmp.hash(seq.base, ptr + seq.begin); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java index 86e60e5b73..68296ef330 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java @@ -25,9 +25,11 @@ import org.eclipse.jgit.errors.DirCacheNameConflictException; * services to applications. */ abstract class BaseDirCacheEditor { + @SuppressWarnings("doclint:missing") /** The cache instance this editor updates during {@link #finish()}. */ protected DirCache cache; + @SuppressWarnings("doclint:missing") /** * Entry table this builder will eventually replace into {@link #cache}. * <p> @@ -40,7 +42,7 @@ abstract class BaseDirCacheEditor { */ protected DirCacheEntry[] entries; - /** Total number of valid entries in {@link #entries}. */ + /** Total number of valid entries in {@link BaseDirCacheEditor#entries}. */ protected int entryCnt; /** @@ -137,7 +139,8 @@ abstract class BaseDirCacheEditor { public abstract void finish(); /** - * Update the DirCache with the contents of {@link #entries}. + * Update the DirCache with the contents of + * {@link BaseDirCacheEditor#entries}. * <p> * This method should be invoked only during an implementation of * {@link #finish()}, and only after {@link #entries} is sorted. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java new file mode 100644 index 0000000000..de02aecdb9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2023, Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.dircache; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.StandardCopyOption; +import java.text.MessageFormat; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.FileModeCache; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; +import org.eclipse.jgit.lib.CoreConfig.SymLinks; +import org.eclipse.jgit.lib.FileModeCache.CacheItem; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * An object that can be used to check out many files. + * + * @since 6.6.1 + */ +public class Checkout { + + private final FileModeCache cache; + + private final WorkingTreeOptions options; + + private boolean recursiveDelete; + + /** + * Creates a new {@link Checkout} for checking out from the given + * repository. + * + * @param repo + * the {@link Repository} to check out from + */ + public Checkout(@NonNull Repository repo) { + this(repo, null); + } + + /** + * Creates a new {@link Checkout} for checking out from the given + * repository. + * + * @param repo + * the {@link Repository} to check out from + * @param options + * the {@link WorkingTreeOptions} to use; if {@code null}, + * read from the {@code repo} config when this object is + * created + */ + public Checkout(@NonNull Repository repo, WorkingTreeOptions options) { + this.cache = new FileModeCache(repo); + this.options = options != null ? options + : repo.getConfig().get(WorkingTreeOptions.KEY); + } + + /** + * Retrieves the {@link WorkingTreeOptions} of the repository that are + * used. + * + * @return the {@link WorkingTreeOptions} + */ + public WorkingTreeOptions getWorkingTreeOptions() { + return options; + } + + /** + * Defines whether directories that are in the way of the file to be checked + * out shall be deleted recursively. + * + * @param recursive + * whether to delete such directories recursively + * @return {@code this} + */ + public Checkout setRecursiveDeletion(boolean recursive) { + this.recursiveDelete = recursive; + return this; + } + + /** + * Ensure that the given parent directory exists, and cache the information + * that gitPath refers to a file. + * + * @param gitPath + * of the file to be written + * @param parentDir + * directory in which the file shall be placed, assumed to be the + * parent of the {@code gitPath} + * @param makeSpace + * whether to delete a possibly existing file at + * {@code parentDir} + * @throws IOException + * if the directory cannot be created, if necessary + */ + public void safeCreateParentDirectory(String gitPath, File parentDir, + boolean makeSpace) throws IOException { + cache.safeCreateParentDirectory(gitPath, parentDir, makeSpace); + } + + /** + * Checks out the gitlink given by the {@link DirCacheEntry}. + * + * @param entry + * {@link DirCacheEntry} to check out + * @param gitPath + * the git path of the entry, if known already; otherwise + * {@code null} and it's read from the entry itself + * @throws IOException + * if the gitlink cannot be checked out + */ + public void checkoutGitlink(DirCacheEntry entry, String gitPath) + throws IOException { + FS fs = cache.getRepository().getFS(); + File workingTree = cache.getRepository().getWorkTree(); + String path = gitPath != null ? gitPath : entry.getPathString(); + File gitlinkDir = new File(workingTree, path); + File parentDir = gitlinkDir.getParentFile(); + CacheItem cachedParent = cache.safeCreateDirectory(path, parentDir, + false); + FileUtils.mkdirs(gitlinkDir, true); + cachedParent.insert(path.substring(path.lastIndexOf('/') + 1), + FileMode.GITLINK); + entry.setLastModified(fs.lastModifiedInstant(gitlinkDir)); + } + + /** + * Checks out the file given by the {@link DirCacheEntry}. + * + * @param entry + * {@link DirCacheEntry} to check out + * @param metadata + * {@link CheckoutMetadata} to use for CR/LF handling and + * smudge filtering + * @param reader + * {@link ObjectReader} to use + * @param gitPath + * the git path of the entry, if known already; otherwise + * {@code null} and it's read from the entry itself + * @throws IOException + * if the file cannot be checked out + */ + public void checkout(DirCacheEntry entry, CheckoutMetadata metadata, + ObjectReader reader, String gitPath) throws IOException { + if (metadata == null) { + metadata = CheckoutMetadata.EMPTY; + } + FS fs = cache.getRepository().getFS(); + ObjectLoader ol = reader.open(entry.getObjectId()); + String path = gitPath != null ? gitPath : entry.getPathString(); + File f = new File(cache.getRepository().getWorkTree(), path); + File parentDir = f.getParentFile(); + CacheItem cachedParent = cache.safeCreateDirectory(path, parentDir, + true); + if (entry.getFileMode() == FileMode.SYMLINK + && options.getSymLinks() == SymLinks.TRUE) { + byte[] bytes = ol.getBytes(); + String target = RawParseUtils.decode(bytes); + if (recursiveDelete && Files.isDirectory(f.toPath(), + LinkOption.NOFOLLOW_LINKS)) { + FileUtils.delete(f, FileUtils.RECURSIVE); + } + fs.createSymLink(f, target); + cachedParent.insert(f.getName(), FileMode.SYMLINK); + entry.setLength(bytes.length); + entry.setLastModified(fs.lastModifiedInstant(f)); + return; + } + + String name = f.getName(); + if (name.length() > 200) { + name = name.substring(0, 200); + } + File tmpFile = File.createTempFile("._" + name, null, parentDir); //$NON-NLS-1$ + + DirCacheCheckout.getContent(cache.getRepository(), path, metadata, ol, + options, + new FileOutputStream(tmpFile)); + + // The entry needs to correspond to the on-disk file size. If the + // content was filtered (either by autocrlf handling or smudge + // filters) ask the file system again for the length. Otherwise the + // object loader knows the size + if (metadata.eolStreamType == EolStreamType.DIRECT + && metadata.smudgeFilterCommand == null) { + entry.setLength(ol.getSize()); + } else { + entry.setLength(tmpFile.length()); + } + + if (options.isFileMode() && fs.supportsExecute()) { + if (FileMode.EXECUTABLE_FILE.equals(entry.getRawMode())) { + if (!fs.canExecute(tmpFile)) + fs.setExecute(tmpFile, true); + } else { + if (fs.canExecute(tmpFile)) + fs.setExecute(tmpFile, false); + } + } + try { + boolean isDir = Files.isDirectory(f.toPath(), + LinkOption.NOFOLLOW_LINKS); + if (recursiveDelete && isDir) { + FileUtils.delete(f, FileUtils.RECURSIVE); + } + if (cache.getRepository().isWorkTreeCaseInsensitive() && !isDir) { + // We cannot rely on rename via Files.move() to work correctly + // if the target exists in a case variant. For instance with JDK + // 17 on Mac OS, the existing case-variant name is kept. On + // Windows 11 it would work and use the name given in 'f'. + FileUtils.delete(f, FileUtils.SKIP_MISSING); + } + FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE); + cachedParent.remove(f.getName()); + } catch (IOException e) { + throw new IOException( + MessageFormat.format(JGitText.get().renameFileFailed, + tmpFile.getPath(), f.getPath()), + e); + } finally { + if (tmpFile.exists()) { + FileUtils.delete(tmpFile); + } + } + entry.setLastModified(fs.lastModifiedInstant(f)); + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java index e56061223c..c650d6e8e7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java @@ -429,6 +429,7 @@ public class DirCache { * * @return {@code true} if the memory state differs from the index file * @throws java.io.IOException + * if an IO error occurred */ public boolean isOutdated() throws IOException { if (liveFile == null || !liveFile.exists()) @@ -1001,6 +1002,7 @@ public class DirCache { * Update any smudged entries with information from the working tree. * * @throws IOException + * if an IO error occurred */ private void updateSmudgedEntries() throws IOException { List<String> paths = new ArrayList<>(128); @@ -1035,7 +1037,12 @@ public class DirCache { } } - enum DirCacheVersion implements ConfigEnum { + /** + * DirCache versions + * + * @since 7.2 + */ + public enum DirCacheVersion implements ConfigEnum { /** Minimum index version on-disk format that we support. */ DIRC_VERSION_MINIMUM(2), @@ -1058,6 +1065,9 @@ public class DirCache { this.version = versionCode; } + /** + * @return the version code for this version + */ public int getVersionCode() { return version; } @@ -1076,6 +1086,13 @@ public class DirCache { } } + /** + * Create DirCacheVersion from integer value of the version code. + * + * @param val + * integer value of the version code. + * @return the DirCacheVersion instance of the version code. + */ public static DirCacheVersion fromInt(int val) { for (DirCacheVersion v : DirCacheVersion.values()) { if (val == v.getVersionCode()) { @@ -1096,9 +1113,8 @@ public class DirCache { boolean manyFiles = cfg.getBoolean( ConfigConstants.CONFIG_FEATURE_SECTION, ConfigConstants.CONFIG_KEY_MANYFILES, false); - indexVersion = cfg.getEnum(DirCacheVersion.values(), - ConfigConstants.CONFIG_INDEX_SECTION, null, - ConfigConstants.CONFIG_KEY_VERSION, + indexVersion = cfg.getEnum(ConfigConstants.CONFIG_INDEX_SECTION, + null, ConfigConstants.CONFIG_KEY_VERSION, manyFiles ? DirCacheVersion.DIRC_VERSION_PATHCOMPRESS : DirCacheVersion.DIRC_VERSION_EXTENDED); skipHash = cfg.getBoolean(ConfigConstants.CONFIG_INDEX_SECTION, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java index f6e4d360a6..befd8067ab 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java @@ -74,7 +74,6 @@ public class DirCacheBuildIterator extends DirCacheIterator { builder = p.builder; } - /** {@inheritDoc} */ @Override public AbstractTreeIterator createSubtreeIterator(ObjectReader reader) throws IncorrectObjectTypeException, IOException { @@ -84,7 +83,6 @@ public class DirCacheBuildIterator extends DirCacheIterator { return new DirCacheBuildIterator(this, currentSubtree); } - /** {@inheritDoc} */ @Override public void skip() throws CorruptObjectException { if (currentSubtree != null) @@ -94,7 +92,6 @@ public class DirCacheBuildIterator extends DirCacheIterator { next(1); } - /** {@inheritDoc} */ @Override public void stopWalk() { final int cur = ptr; @@ -103,7 +100,6 @@ public class DirCacheBuildIterator extends DirCacheIterator { builder.keep(cur, cnt - cur); } - /** {@inheritDoc} */ @Override protected boolean needsStopWalk() { return ptr < cache.getEntryCount(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuilder.java index 9fe77f36e8..28d2502005 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuilder.java @@ -186,7 +186,6 @@ public class DirCacheBuilder extends BaseDirCacheEditor { return e; } - /** {@inheritDoc} */ @Override public void finish() { if (!sorted) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java index 1fb81b71e9..18d77482e0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -5,7 +5,7 @@ * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> * Copyright (C) 2010, Chrisian Halstrick <christian.halstrick@sap.com> * Copyright (C) 2019, 2020, Andre Bossert <andre.bossert@siemens.com> - * Copyright (C) 2017, 2022, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2017, 2025, Thomas Wolf <twolf@apache.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -19,11 +19,9 @@ package org.eclipse.jgit.dircache; import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.time.Instant; import java.util.ArrayList; @@ -33,6 +31,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.FilterFailedException; @@ -49,7 +48,6 @@ import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.AutoCRLF; import org.eclipse.jgit.lib.CoreConfig.EolStreamType; -import org.eclipse.jgit.lib.CoreConfig.SymLinks; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectChecker; @@ -69,9 +67,6 @@ import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS.ExecutionResult; -import org.eclipse.jgit.util.FileUtils; -import org.eclipse.jgit.util.IntList; -import org.eclipse.jgit.util.RawParseUtils; import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.io.EolStreamTypeUtil; import org.slf4j.Logger; @@ -100,7 +95,9 @@ public class DirCacheCheckout { /** * @param eolStreamType + * how to convert EOL characters during stream conversion * @param smudgeFilterCommand + * command used as smudge filter during checkout */ public CheckoutMetadata(EolStreamType eolStreamType, String smudgeFilterCommand) { @@ -116,9 +113,11 @@ public class DirCacheCheckout { private Map<String, CheckoutMetadata> updated = new LinkedHashMap<>(); + private Set<String> existing; + private ArrayList<String> conflicts = new ArrayList<>(); - private ArrayList<String> removed = new ArrayList<>(); + private TreeSet<String> removed; private ArrayList<String> kept = new ArrayList<>(); @@ -144,7 +143,7 @@ public class DirCacheCheckout { private boolean performingCheckout; - private WorkingTreeOptions options; + private Checkout checkout; private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; @@ -188,7 +187,7 @@ public class DirCacheCheckout { * @return a list of all files removed by this checkout */ public List<String> getRemoved() { - return removed; + return new ArrayList<>(removed); } /** @@ -206,6 +205,7 @@ public class DirCacheCheckout { * @param workingTree * an iterator over the repositories Working Tree * @throws java.io.IOException + * if an IO error occurred */ public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc, ObjectId mergeCommitTree, WorkingTreeIterator workingTree) @@ -216,6 +216,14 @@ public class DirCacheCheckout { this.mergeCommitTree = mergeCommitTree; this.workingTree = workingTree; this.initialCheckout = !repo.isBare() && !repo.getIndexFile().exists(); + boolean caseInsensitive = !repo.isBare() + && repo.isWorkTreeCaseInsensitive(); + this.removed = caseInsensitive + ? new TreeSet<>(String::compareToIgnoreCase) + : new TreeSet<>(); + this.existing = caseInsensitive + ? new TreeSet<>(String::compareToIgnoreCase) + : null; } /** @@ -233,6 +241,7 @@ public class DirCacheCheckout { * @param mergeCommitTree * the id of the tree we want to fast-forward to * @throws java.io.IOException + * if an IO error occurred */ public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc, ObjectId mergeCommitTree) throws IOException { @@ -252,6 +261,7 @@ public class DirCacheCheckout { * @param workingTree * an iterator over the repositories Working Tree * @throws java.io.IOException + * if an IO error occurred */ public DirCacheCheckout(Repository repo, DirCache dc, ObjectId mergeCommitTree, WorkingTreeIterator workingTree) @@ -271,6 +281,7 @@ public class DirCacheCheckout { * @param mergeCommitTree * the id of the tree of the * @throws java.io.IOException + * if an IO error occurred */ public DirCacheCheckout(Repository repo, DirCache dc, ObjectId mergeCommitTree) throws IOException { @@ -294,7 +305,9 @@ public class DirCacheCheckout { * operations. * * @throws org.eclipse.jgit.errors.CorruptObjectException + * if a corrupt object was found * @throws java.io.IOException + * if an IO error occurred */ public void preScanTwoTrees() throws CorruptObjectException, IOException { removed.clear(); @@ -324,9 +337,13 @@ public class DirCacheCheckout { * there is no head yet. * * @throws org.eclipse.jgit.errors.MissingObjectException + * if an object was found missing * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException + * if an object didn't have the expected type * @throws org.eclipse.jgit.errors.CorruptObjectException + * if an object is corrupt * @throws java.io.IOException + * if an IO error occurred */ public void prescanOneTree() throws MissingObjectException, IncorrectObjectTypeException, @@ -372,6 +389,7 @@ public class DirCacheCheckout { * @param f * the working tree * @throws IOException + * if an IO error occurred */ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i, WorkingTreeIterator f) throws IOException { @@ -392,9 +410,11 @@ public class DirCacheCheckout { // content to be checked out. update(m); } - } else + } else { update(m); - } else if (f == null || !m.idEqual(i)) { + } + } else if (f == null || !m.idEqual(i) + || m.getEntryRawMode() != i.getEntryRawMode()) { // The working tree file is missing or the merge content differs // from index content update(m); @@ -402,11 +422,11 @@ public class DirCacheCheckout { // The index contains a file (and not a folder) if (f.isModified(i.getDirCacheEntry(), true, this.walk.getObjectReader()) - || i.getDirCacheEntry().getStage() != 0) + || i.getDirCacheEntry().getStage() != 0) { // The working tree file is dirty or the index contains a // conflict update(m); - else { + } else { // update the timestamp of the index with the one from the // file if not set, as we are sure to be in sync here. DirCacheEntry entry = i.getDirCacheEntry(); @@ -416,9 +436,10 @@ public class DirCacheCheckout { } keep(i.getEntryPathString(), entry, f); } - } else + } else { // The index contains a folder keep(i.getEntryPathString(), i.getDirCacheEntry(), f); + } } else { // There is no entry in the merge commit. Means: we want to delete // what's currently in the index and working tree @@ -465,6 +486,7 @@ public class DirCacheCheckout { * successful and the working tree was updated for all other files. * <code>true</code> is returned when no such problem occurred * @throws java.io.IOException + * if an IO error occurred */ public boolean checkout() throws IOException { try { @@ -495,9 +517,8 @@ public class DirCacheCheckout { MissingObjectException, IncorrectObjectTypeException, CheckoutConflictException, IndexWriteException, CanceledException { toBeDeleted.clear(); - options = repo.getConfig() - .get(WorkingTreeOptions.KEY); try (ObjectReader objectReader = repo.getObjectDatabase().newReader()) { + checkout = new Checkout(repo, null); if (headCommitTree != null) preScanTwoTrees(); else @@ -513,6 +534,13 @@ public class DirCacheCheckout { // update our index builder.finish(); + // On case-insensitive file systems we may have a case variant kept + // and another one removed. In that case, don't remove it. + if (existing != null) { + removed.removeAll(existing); + existing.clear(); + } + // init progress reporting int numTotal = removed.size() + updated.size() + conflicts.size(); monitor.beginTask(JGitText.get().checkingOutFiles, numTotal); @@ -523,9 +551,9 @@ public class DirCacheCheckout { // when deleting files process them in the opposite order as they have // been reported. This ensures the files are deleted before we delete // their parent folders - IntList nonDeleted = new IntList(); - for (int i = removed.size() - 1; i >= 0; i--) { - String r = removed.get(i); + Iterator<String> iter = removed.descendingIterator(); + while (iter.hasNext()) { + String r = iter.next(); file = new File(repo.getWorkTree(), r); if (!file.delete() && repo.getFS().exists(file)) { // The list of stuff to delete comes from the index @@ -534,7 +562,7 @@ public class DirCacheCheckout { // to delete it. A submodule is not empty, so it // is safe to check this after a failed delete. if (!repo.getFS().isDirectory(file)) { - nonDeleted.add(i); + iter.remove(); toBeDeleted.add(r); } } else { @@ -552,8 +580,6 @@ public class DirCacheCheckout { if (file != null) { removeEmptyParents(file); } - removed = filterOut(removed, nonDeleted); - nonDeleted = null; Iterator<Map.Entry<String, CheckoutMetadata>> toUpdate = updated .entrySet().iterator(); Map.Entry<String, CheckoutMetadata> e = null; @@ -564,10 +590,9 @@ public class DirCacheCheckout { CheckoutMetadata meta = e.getValue(); DirCacheEntry entry = dc.getEntry(path); if (FileMode.GITLINK.equals(entry.getRawMode())) { - checkoutGitlink(path, entry); + checkout.checkoutGitlink(entry, path); } else { - checkoutEntry(repo, entry, objectReader, false, meta, - options); + checkout.checkout(entry, meta, objectReader, path); } e = null; @@ -602,8 +627,8 @@ public class DirCacheCheckout { break; } if (entry.getStage() == DirCacheEntry.STAGE_3) { - checkoutEntry(repo, entry, objectReader, false, - null, options); + checkout.checkout(entry, null, objectReader, + conflict); break; } ++entryIdx; @@ -626,44 +651,6 @@ public class DirCacheCheckout { return toBeDeleted.isEmpty(); } - private void checkoutGitlink(String path, DirCacheEntry entry) - throws IOException { - File gitlinkDir = new File(repo.getWorkTree(), path); - FileUtils.mkdirs(gitlinkDir, true); - FS fs = repo.getFS(); - entry.setLastModified(fs.lastModifiedInstant(gitlinkDir)); - } - - private static ArrayList<String> filterOut(ArrayList<String> strings, - IntList indicesToRemove) { - int n = indicesToRemove.size(); - if (n == strings.size()) { - return new ArrayList<>(0); - } - switch (n) { - case 0: - return strings; - case 1: - strings.remove(indicesToRemove.get(0)); - return strings; - default: - int length = strings.size(); - ArrayList<String> result = new ArrayList<>(length - n); - // Process indicesToRemove from the back; we know that it - // contains indices in descending order. - int j = n - 1; - int idx = indicesToRemove.get(j); - for (int i = 0; i < length; i++) { - if (i == idx) { - idx = (--j >= 0) ? indicesToRemove.get(j) : -1; - } else { - result.add(strings.get(i)); - } - } - return result; - } - } - private static boolean isSamePrefix(String a, String b) { int as = a.lastIndexOf('/'); int bs = b.lastIndexOf('/'); @@ -684,9 +671,13 @@ public class DirCacheCheckout { * Compares whether two pairs of ObjectId and FileMode are equal. * * @param id1 + * id of first object * @param mode1 + * mode of first object * @param id2 + * id of second object * @param mode2 + * mode of second object * @return <code>true</code> if FileModes and ObjectIds are equal. * <code>false</code> otherwise */ @@ -712,6 +703,7 @@ public class DirCacheCheckout { * @param f * the file in the working tree * @throws IOException + * if an IO error occurred */ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, @@ -1229,13 +1221,17 @@ public class DirCacheCheckout { if (!FileMode.TREE.equals(e.getFileMode())) { builder.add(e); } + if (existing != null) { + existing.add(path); + } if (force) { if (f == null || f.isModified(e, true, walk.getObjectReader())) { kept.add(path); - checkoutEntry(repo, e, walk.getObjectReader(), false, + checkout.checkout(e, new CheckoutMetadata(walk.getEolStreamType(CHECKOUT_OP), walk.getFilterCommand( - Constants.ATTR_FILTER_TYPE_SMUDGE)), options); + Constants.ATTR_FILTER_TYPE_SMUDGE)), + walk.getObjectReader(), path); } } } @@ -1295,6 +1291,7 @@ public class DirCacheCheckout { * {@link #failOnConflict} is false * * @throws CheckoutConflictException + * if a conflict occurred during merge checkout */ private void cleanUpConflicts() throws CheckoutConflictException { // TODO: couldn't we delete unsaved worktree content here? @@ -1308,13 +1305,16 @@ public class DirCacheCheckout { } /** - * Checks whether the subtree starting at a given path differs between Index and - * workingtree. + * Checks whether the subtree starting at a given path differs between Index + * and workingtree. * * @param path + * given subtree path * @return true if the subtrees differ * @throws CorruptObjectException + * if a corrupt object was found * @throws IOException + * if an IO error occurred */ private boolean isModifiedSubtree_IndexWorkingtree(String path) throws CorruptObjectException, IOException { @@ -1355,15 +1355,18 @@ public class DirCacheCheckout { } /** - * Checks whether the subtree starting at a given path differs between Index and - * some tree. + * Checks whether the subtree starting at a given path differs between Index + * and some tree. * * @param path + * given path * @param tree * the tree to compare * @return true if the subtrees differ * @throws CorruptObjectException + * if a corrupt object was found * @throws IOException + * if an IO error occurred */ private boolean isModifiedSubtree_IndexTree(String path, ObjectId tree) throws CorruptObjectException, IOException { @@ -1389,191 +1392,6 @@ public class DirCacheCheckout { } /** - * Updates the file in the working tree with content and mode from an entry - * in the index. The new content is first written to a new temporary file in - * the same directory as the real file. Then that new file is renamed to the - * final filename. - * - * <p> - * <b>Note:</b> if the entry path on local file system exists as a non-empty - * directory, and the target entry type is a link or file, the checkout will - * fail with {@link java.io.IOException} since existing non-empty directory - * cannot be renamed to file or link without deleting it recursively. - * </p> - * - * @param repo - * repository managing the destination work tree. - * @param entry - * the entry containing new mode and content - * @param or - * object reader to use for checkout - * @throws java.io.IOException - * @since 3.6 - * @deprecated since 5.1, use - * {@link #checkoutEntry(Repository, DirCacheEntry, ObjectReader, boolean, CheckoutMetadata, WorkingTreeOptions)} - * instead - */ - @Deprecated - public static void checkoutEntry(Repository repo, DirCacheEntry entry, - ObjectReader or) throws IOException { - checkoutEntry(repo, entry, or, false, null, null); - } - - - /** - * Updates the file in the working tree with content and mode from an entry - * in the index. The new content is first written to a new temporary file in - * the same directory as the real file. Then that new file is renamed to the - * final filename. - * - * <p> - * <b>Note:</b> if the entry path on local file system exists as a file, it - * will be deleted and if it exists as a directory, it will be deleted - * recursively, independently if has any content. - * </p> - * - * @param repo - * repository managing the destination work tree. - * @param entry - * the entry containing new mode and content - * @param or - * object reader to use for checkout - * @param deleteRecursive - * true to recursively delete final path if it exists on the file - * system - * @param checkoutMetadata - * containing - * <ul> - * <li>smudgeFilterCommand to be run for smudging the entry to be - * checked out</li> - * <li>eolStreamType used for stream conversion</li> - * </ul> - * @throws java.io.IOException - * @since 4.2 - * @deprecated since 6.3, use - * {@link #checkoutEntry(Repository, DirCacheEntry, ObjectReader, boolean, CheckoutMetadata, WorkingTreeOptions)} - * instead - */ - @Deprecated - public static void checkoutEntry(Repository repo, DirCacheEntry entry, - ObjectReader or, boolean deleteRecursive, - CheckoutMetadata checkoutMetadata) throws IOException { - checkoutEntry(repo, entry, or, deleteRecursive, checkoutMetadata, null); - } - - /** - * Updates the file in the working tree with content and mode from an entry - * in the index. The new content is first written to a new temporary file in - * the same directory as the real file. Then that new file is renamed to the - * final filename. - * - * <p> - * <b>Note:</b> if the entry path on local file system exists as a file, it - * will be deleted and if it exists as a directory, it will be deleted - * recursively, independently if has any content. - * </p> - * - * @param repo - * repository managing the destination work tree. - * @param entry - * the entry containing new mode and content - * @param or - * object reader to use for checkout - * @param deleteRecursive - * true to recursively delete final path if it exists on the file - * system - * @param checkoutMetadata - * containing - * <ul> - * <li>smudgeFilterCommand to be run for smudging the entry to be - * checked out</li> - * <li>eolStreamType used for stream conversion</li> - * </ul> - * @param options - * {@link WorkingTreeOptions} that are effective; if {@code null} - * they are loaded from the repository config - * @throws java.io.IOException - * @since 6.3 - */ - public static void checkoutEntry(Repository repo, DirCacheEntry entry, - ObjectReader or, boolean deleteRecursive, - CheckoutMetadata checkoutMetadata, WorkingTreeOptions options) - throws IOException { - if (checkoutMetadata == null) { - checkoutMetadata = CheckoutMetadata.EMPTY; - } - ObjectLoader ol = or.open(entry.getObjectId()); - File f = new File(repo.getWorkTree(), entry.getPathString()); - File parentDir = f.getParentFile(); - if (parentDir.isFile()) { - FileUtils.delete(parentDir); - } - FileUtils.mkdirs(parentDir, true); - FS fs = repo.getFS(); - WorkingTreeOptions opt = options != null ? options - : repo.getConfig().get(WorkingTreeOptions.KEY); - if (entry.getFileMode() == FileMode.SYMLINK - && opt.getSymLinks() == SymLinks.TRUE) { - byte[] bytes = ol.getBytes(); - String target = RawParseUtils.decode(bytes); - if (deleteRecursive && f.isDirectory()) { - FileUtils.delete(f, FileUtils.RECURSIVE); - } - fs.createSymLink(f, target); - entry.setLength(bytes.length); - entry.setLastModified(fs.lastModifiedInstant(f)); - return; - } - - String name = f.getName(); - if (name.length() > 200) { - name = name.substring(0, 200); - } - File tmpFile = File.createTempFile( - "._" + name, null, parentDir); //$NON-NLS-1$ - - getContent(repo, entry.getPathString(), checkoutMetadata, ol, opt, - new FileOutputStream(tmpFile)); - - // The entry needs to correspond to the on-disk filesize. If the content - // was filtered (either by autocrlf handling or smudge filters) ask the - // filesystem again for the length. Otherwise the objectloader knows the - // size - if (checkoutMetadata.eolStreamType == EolStreamType.DIRECT - && checkoutMetadata.smudgeFilterCommand == null) { - entry.setLength(ol.getSize()); - } else { - entry.setLength(tmpFile.length()); - } - - if (opt.isFileMode() && fs.supportsExecute()) { - if (FileMode.EXECUTABLE_FILE.equals(entry.getRawMode())) { - if (!fs.canExecute(tmpFile)) - fs.setExecute(tmpFile, true); - } else { - if (fs.canExecute(tmpFile)) - fs.setExecute(tmpFile, false); - } - } - try { - if (deleteRecursive && f.isDirectory()) { - FileUtils.delete(f, FileUtils.RECURSIVE); - } - FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE); - } catch (IOException e) { - throw new IOException( - MessageFormat.format(JGitText.get().renameFileFailed, - tmpFile.getPath(), f.getPath()), - e); - } finally { - if (tmpFile.exists()) { - FileUtils.delete(tmpFile); - } - } - entry.setLastModified(fs.lastModifiedInstant(f)); - } - - /** * Return filtered content for a specific object (blob). EOL handling and * smudge-filter handling are applied in the same way as it would be done * during a checkout. @@ -1599,6 +1417,7 @@ public class DirCacheCheckout { * the output stream the filtered content is written to. The * caller is responsible to close the stream. * @throws IOException + * if an IO error occurred * * @since 5.7 */ @@ -1654,6 +1473,7 @@ public class DirCacheCheckout { * the output stream the filtered content is written to. The * caller is responsible to close the stream. * @throws IOException + * if an IO error occurred * @since 6.3 */ public static void getContent(Repository repo, String path, @@ -1697,6 +1517,8 @@ public class DirCacheCheckout { filterProcessBuilder.directory(repo.getWorkTree()); filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath()); + filterProcessBuilder.environment().put(Constants.GIT_COMMON_DIR_KEY, + repo.getCommonDirectory().getAbsolutePath()); ExecutionResult result; int rc; try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java index 8c342e267d..b1f4e7db21 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java @@ -37,7 +37,6 @@ import org.eclipse.jgit.util.Paths; * 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 org.eclipse.jgit.dircache.DirCacheBuilder}. - * <p> * * @see DirCacheBuilder */ @@ -80,7 +79,6 @@ public class DirCacheEditor extends BaseDirCacheEditor { edits.add(edit); } - /** {@inheritDoc} */ @Override public boolean commit() throws IOException { if (edits.isEmpty()) { @@ -92,7 +90,6 @@ public class DirCacheEditor extends BaseDirCacheEditor { return super.commit(); } - /** {@inheritDoc} */ @Override public void finish() { if (!edits.isEmpty()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java index 67edf50f44..5a22938694 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java @@ -396,33 +396,12 @@ public class DirCacheEntry { * 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. - * @deprecated use {@link #mightBeRacilyClean(Instant)} instead - */ - @Deprecated - public final boolean mightBeRacilyClean(int smudge_s, int smudge_ns) { - return mightBeRacilyClean(Instant.ofEpochSecond(smudge_s, smudge_ns)); - } - - /** - * 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 * index's last modified time. * @return true if extra careful checks should be used. * @since 5.1.9 */ + @SuppressWarnings("JavaInstantGetSecondsGetNano") public final boolean mightBeRacilyClean(Instant smudge) { // If the index has a modification time then it came from disk // and was not generated from scratch in memory. In such cases @@ -652,22 +631,6 @@ public class DirCacheEntry { } /** - * 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). - * @deprecated use {@link #getLastModifiedInstant()} instead - */ - @Deprecated - public long getLastModified() { - return decodeTS(P_MTIME); - } - - /** * Get the cached last modification date of this file. * <p> * One of the indicators that the file has been modified by an application @@ -682,18 +645,6 @@ public class DirCacheEntry { } /** - * Set the cached last modification date of this file, using milliseconds. - * - * @param when - * new cached modification date of the file, in milliseconds. - * @deprecated use {@link #setLastModified(Instant)} instead - */ - @Deprecated - public void setLastModified(long when) { - encodeTS(P_MTIME, when); - } - - /** * Set the cached last modification date of this file. * * @param when @@ -864,6 +815,8 @@ public class DirCacheEntry { } /** + * Whether the entry contains extended flags + * * @return true if the entry contains extended flags. */ boolean isExtended() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java index aed1c341f2..9cc34a0f11 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java @@ -104,7 +104,6 @@ public class DirCacheIterator extends AbstractTreeIterator { parseEntry(); } - /** {@inheritDoc} */ @Override public AbstractTreeIterator createSubtreeIterator(ObjectReader reader) throws IncorrectObjectTypeException, IOException { @@ -114,7 +113,6 @@ public class DirCacheIterator extends AbstractTreeIterator { return new DirCacheIterator(this, currentSubtree); } - /** {@inheritDoc} */ @Override public EmptyTreeIterator createEmptyTreeIterator() { final byte[] n = new byte[Math.max(pathLen + 1, DEFAULT_PATH_SIZE)]; @@ -123,7 +121,6 @@ public class DirCacheIterator extends AbstractTreeIterator { return new EmptyTreeIterator(this, n, pathLen + 1); } - /** {@inheritDoc} */ @Override public boolean hasId() { if (currentSubtree != null) @@ -131,7 +128,6 @@ public class DirCacheIterator extends AbstractTreeIterator { return currentEntry != null; } - /** {@inheritDoc} */ @Override public byte[] idBuffer() { if (currentSubtree != null) @@ -141,7 +137,6 @@ public class DirCacheIterator extends AbstractTreeIterator { return zeroid; } - /** {@inheritDoc} */ @Override public int idOffset() { if (currentSubtree != null) @@ -151,7 +146,6 @@ public class DirCacheIterator extends AbstractTreeIterator { return 0; } - /** {@inheritDoc} */ @Override public void reset() { if (!first()) { @@ -164,19 +158,16 @@ public class DirCacheIterator extends AbstractTreeIterator { } } - /** {@inheritDoc} */ @Override public boolean first() { return ptr == treeStart; } - /** {@inheritDoc} */ @Override public boolean eof() { return ptr == treeEnd; } - /** {@inheritDoc} */ @Override public void next(int delta) { while (--delta >= 0) { @@ -190,7 +181,6 @@ public class DirCacheIterator extends AbstractTreeIterator { } } - /** {@inheritDoc} */ @Override public void back(int delta) { while (--delta >= 0) { @@ -271,6 +261,7 @@ public class DirCacheIterator extends AbstractTreeIterator { * @return {@link org.eclipse.jgit.attributes.AttributesNode} for the * current entry. * @throws java.io.IOException + * if an IO error occurred * @since 3.7 */ public AttributesNode getEntryAttributesNode(ObjectReader reader) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java index e0c1e93918..2561ae999b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java @@ -534,7 +534,6 @@ public class DirCacheTree { return -1; } - /** {@inheritDoc} */ @Override public String toString() { return getNameString(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/LargeObjectException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/LargeObjectException.java index 4a535619d7..40aa1c3b7c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/LargeObjectException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/LargeObjectException.java @@ -84,7 +84,6 @@ public class LargeObjectException extends RuntimeException { objectId = id.copy(); } - /** {@inheritDoc} */ @Override public String getMessage() { return MessageFormat.format(JGitText.get().largeObjectException, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java index 1fd80867b9..38982fdf23 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java @@ -23,18 +23,6 @@ public class PackInvalidException extends IOException { private static final long serialVersionUID = 1L; /** - * Construct a pack invalid error. - * - * @param path - * path of the invalid pack file. - * @deprecated Use {@link #PackInvalidException(File, Throwable)}. - */ - @Deprecated - public PackInvalidException(File path) { - this(path, null); - } - - /** * Construct a pack invalid error with cause. * * @param path @@ -48,18 +36,6 @@ public class PackInvalidException extends IOException { } /** - * Construct a pack invalid error. - * - * @param path - * path of the invalid pack file. - * @deprecated Use {@link #PackInvalidException(String, Throwable)}. - */ - @Deprecated - public PackInvalidException(String path) { - this(path, null); - } - - /** * Construct a pack invalid error with cause. * * @param path diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java index 7a2c70de79..e630f529e3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java @@ -51,9 +51,4 @@ public class PackMismatchException extends IOException { public boolean isPermanent() { return permanent; } - - @Override - public String toString() { - return super.toString() + ", permanent: " + permanent; //$NON-NLS-1$ - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevisionSyntaxException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevisionSyntaxException.java index 084d67c02b..293b4c6818 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevisionSyntaxException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevisionSyntaxException.java @@ -44,9 +44,13 @@ public class RevisionSyntaxException extends IllegalArgumentException { this.revstr = revstr; } - /** {@inheritDoc} */ - @Override - public String toString() { - return super.toString() + ":" + revstr; //$NON-NLS-1$ + /** + * Get the problematic revision string + * + * @return the problematic revision string + * @since 6.8 + */ + public String getRevstr() { + return revstr; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/ConfigChangedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/ConfigChangedEvent.java index e6342542d0..0c2aa46aa0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/events/ConfigChangedEvent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/ConfigChangedEvent.java @@ -14,13 +14,11 @@ package org.eclipse.jgit.events; * Describes a change to one or more keys in the configuration. */ public class ConfigChangedEvent extends RepositoryEvent<ConfigChangedListener> { - /** {@inheritDoc} */ @Override public Class<ConfigChangedListener> getListenerType() { return ConfigChangedListener.class; } - /** {@inheritDoc} */ @Override public void dispatch(ConfigChangedListener listener) { listener.onConfigChanged(this); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/IndexChangedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/IndexChangedEvent.java index f5a6d64b16..72d004bc47 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/events/IndexChangedEvent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/IndexChangedEvent.java @@ -29,6 +29,8 @@ public class IndexChangedEvent extends RepositoryEvent<IndexChangedListener> { } /** + * Whether the index was changed by the same JGit process + * * @return {@code true} if the index was changed by the same JGit process * @since 5.0 */ @@ -36,13 +38,11 @@ public class IndexChangedEvent extends RepositoryEvent<IndexChangedListener> { return internal; } - /** {@inheritDoc} */ @Override public Class<IndexChangedListener> getListenerType() { return IndexChangedListener.class; } - /** {@inheritDoc} */ @Override public void dispatch(IndexChangedListener listener) { listener.onIndexChanged(this); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerHandle.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerHandle.java index 44897605db..94d49fd93e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerHandle.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerHandle.java @@ -35,7 +35,6 @@ public class ListenerHandle { parent.remove(this); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java index 476c37c1c3..92a227750f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java @@ -71,6 +71,8 @@ public class ListenerList { /** * Add a listener to the list. * + * @param <T> + * type of {@code RepositoryListener} * @param type * type of listener being registered. * @param listener diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/RefsChangedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/RefsChangedEvent.java index 6f1e8d5d66..9bd1ef0653 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/events/RefsChangedEvent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/RefsChangedEvent.java @@ -14,13 +14,11 @@ package org.eclipse.jgit.events; * Describes a change to one or more references of a repository. */ public class RefsChangedEvent extends RepositoryEvent<RefsChangedListener> { - /** {@inheritDoc} */ @Override public Class<RefsChangedListener> getListenerType() { return RefsChangedListener.class; } - /** {@inheritDoc} */ @Override public void dispatch(RefsChangedListener listener) { listener.onRefsChanged(this); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/RepositoryEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/RepositoryEvent.java index 81f2e73de2..288074e15f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/events/RepositoryEvent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/RepositoryEvent.java @@ -61,7 +61,6 @@ public abstract class RepositoryEvent<T extends RepositoryListener> { */ public abstract void dispatch(T listener); - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java index f9888eae34..c3275153ea 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java @@ -87,13 +87,11 @@ public class WorkingTreeModifiedEvent return result; } - /** {@inheritDoc} */ @Override public Class<WorkingTreeModifiedListener> getListenerType() { return WorkingTreeModifiedListener.class; } - /** {@inheritDoc} */ @Override public void dispatch(WorkingTreeModifiedListener listener) { listener.onWorkingTreeModified(this); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java index d54bf65a27..8668ea9c0e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java @@ -45,7 +45,6 @@ abstract class AbstractHead implements Head { this.newHeads = newHeads; } - /** {@inheritDoc} */ @Override public List<Head> getNextHeads(char c) { if (matches(c)) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/CharacterHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/CharacterHead.java index faf4ee66c9..92fee62188 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/CharacterHead.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/CharacterHead.java @@ -24,13 +24,11 @@ final class CharacterHead extends AbstractHead { this.expectedCharacter = expectedCharacter; } - /** {@inheritDoc} */ @Override protected final boolean matches(char c) { return c == expectedCharacter; } - /** {@inheritDoc} */ @Override public String toString() { return String.valueOf(expectedCharacter); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/GroupHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/GroupHead.java index 5ffcafc41e..3b240c3b74 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/GroupHead.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/GroupHead.java @@ -96,7 +96,6 @@ final class GroupHead extends AbstractHead { } } - /** {@inheritDoc} */ @Override protected final boolean matches(char c) { for (CharacterPattern pattern : characterClasses) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/LastHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/LastHead.java index 6aac3faab3..d4cc850a12 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/LastHead.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/LastHead.java @@ -22,7 +22,6 @@ final class LastHead implements Head { // defined because of javadoc and visibility modifier. } - /** {@inheritDoc} */ @Override 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 index 14b6038c44..6b4526f825 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/RestrictedWildCardHead.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/RestrictedWildCardHead.java @@ -18,13 +18,11 @@ final class RestrictedWildCardHead extends AbstractHead { this.excludedCharacter = excludedCharacter; } - /** {@inheritDoc} */ @Override protected final boolean matches(char c) { return c != excludedCharacter; } - /** {@inheritDoc} */ @Override public String toString() { return isStar() ? "*" : "?"; //$NON-NLS-1$ //$NON-NLS-2$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/WildCardHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/WildCardHead.java index 3665a70476..62b6f42c52 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/WildCardHead.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/WildCardHead.java @@ -15,7 +15,6 @@ final class WildCardHead extends AbstractHead { super(star); } - /** {@inheritDoc} */ @Override protected final boolean matches(char c) { return true; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java index f63cc6d644..e511a68d2e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java @@ -107,7 +107,7 @@ class BareSuperprojectWriter { PersonIdent author, RemoteReader callback, BareWriterConfig config, List<ExtraContent> extraContents) { - assert (repo.isBare()); + assert repo.isBare(); this.repo = repo; this.targetUri = targetUri; this.targetBranch = targetBranch; @@ -156,6 +156,9 @@ class BareSuperprojectWriter { ObjectId objectId; if (ObjectId.isId(proj.getRevision())) { objectId = ObjectId.fromString(proj.getRevision()); + if (config.recordRemoteBranch && proj.getUpstream() != null) { + cfg.setString("submodule", name, "ref", proj.getUpstream()); //$NON-NLS-1$//$NON-NLS-2$ + } } else { objectId = callback.sha1(url, proj.getRevision()); if (objectId == null && !config.ignoreRemoteFailures) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java index aa69a05112..58b4d3dc56 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java @@ -75,7 +75,9 @@ public class ManifestParser extends DefaultHandler { * The relative path to the file to read * @return the {@code InputStream} of the file. * @throws GitAPIException + * a JGit API exception * @throws IOException + * if an IO error occurred */ public InputStream readIncludeFile(String path) throws GitAPIException, IOException; @@ -134,12 +136,23 @@ public class ManifestParser extends DefaultHandler { * @param inputStream * a {@link java.io.InputStream} object. * @throws java.io.IOException + * if an IO error occurred */ public void read(InputStream inputStream) throws IOException { xmlInRead++; final XMLReader xr; try { - xr = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); + SAXParserFactory spf = SAXParserFactory.newInstance(); + spf.setFeature( + "http://xml.org/sax/features/external-general-entities", //$NON-NLS-1$ + false); + spf.setFeature( + "http://xml.org/sax/features/external-parameter-entities", //$NON-NLS-1$ + false); + spf.setFeature( + "http://apache.org/xml/features/disallow-doctype-decl", //$NON-NLS-1$ + true); + xr = spf.newSAXParser().getXMLReader(); } catch (SAXException | ParserConfigurationException e) { throw new IOException(JGitText.get().noXMLParserAvailable, e); } @@ -151,7 +164,6 @@ public class ManifestParser extends DefaultHandler { } } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public void startElement( @@ -174,6 +186,10 @@ public class ManifestParser extends DefaultHandler { attributes.getValue("groups")); currentProject .setRecommendShallow(attributes.getValue("clone-depth")); + currentProject + .setUpstream(attributes.getValue("upstream")); + currentProject + .setDestBranch(attributes.getValue("dest-branch")); break; case "remote": String alias = attributes.getValue("alias"); @@ -239,7 +255,6 @@ public class ManifestParser extends DefaultHandler { } } - /** {@inheritDoc} */ @Override public void endElement( String uri, @@ -251,7 +266,6 @@ public class ManifestParser extends DefaultHandler { } } - /** {@inheritDoc} */ @Override public void endDocument() throws SAXException { xmlInRead--; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java index afab9943a7..7632b367c1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java @@ -64,8 +64,8 @@ class RegularSuperprojectWriter { private void addSubmodule(String name, String url, String path, String revision, List<CopyFile> copyfiles, List<LinkFile> linkfiles, Git git) throws GitAPIException, IOException { - assert (!repo.isBare()); - assert (git != null); + assert !repo.isBare(); + assert git != null; if (!linkfiles.isEmpty()) { throw new UnsupportedOperationException( JGitText.get().nonBareLinkFilesNotSupported); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java index 6e943e5d36..be77fca459 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java @@ -105,34 +105,12 @@ public class RepoCommand extends GitCommand<RevCommit> { * @return the sha1 of the remote repository, or null if the ref does * not exist. * @throws GitAPIException + * a JGit API exception */ @Nullable public ObjectId sha1(String uri, String ref) throws GitAPIException; /** - * Read a file from a remote repository. - * - * @param uri - * The URI of the remote repository - * @param ref - * The ref (branch/tag/etc.) to read - * @param path - * The relative path (inside the repo) to the file to read - * @return the file content. - * @throws GitAPIException - * @throws IOException - * @since 3.5 - * - * @deprecated Use {@link #readFileWithMode(String, String, String)} - * instead - */ - @Deprecated - public default byte[] readFile(String uri, String ref, String path) - throws GitAPIException, IOException { - return readFileWithMode(uri, ref, path).getContents(); - } - - /** * Read contents and mode (i.e. permissions) of the file from a remote * repository. * @@ -197,6 +175,8 @@ public class RepoCommand extends GitCommand<RevCommit> { } /** + * Get file mode + * * @return Git file mode for this file (e.g. executable or regular) */ @NonNull @@ -249,7 +229,8 @@ public class RepoCommand extends GitCommand<RevCommit> { @SuppressWarnings("serial") static class ManifestErrorException extends GitAPIException { ManifestErrorException(Throwable cause) { - super(RepoText.get().invalidManifest, cause); + super(RepoText.get().invalidManifest + " " + cause.getMessage(), //$NON-NLS-1$ + cause); } } @@ -528,7 +509,6 @@ public class RepoCommand extends GitCommand<RevCommit> { return this; } - /** {@inheritDoc} */ @Override public RevCommit call() throws GitAPIException { checkCallable(); @@ -610,6 +590,7 @@ public class RepoCommand extends GitCommand<RevCommit> { p.setUrl(proj.getUrl()); p.addCopyFiles(proj.getCopyFiles()); p.addLinkFiles(proj.getLinkFiles()); + p.setUpstream(proj.getUpstream()); ret.add(p); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java index c5ead09523..2630da34e0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java @@ -38,6 +38,8 @@ public class RepoProject implements Comparable<RepoProject> { private final Set<String> groups; private final List<CopyFile> copyfiles; private final List<LinkFile> linkfiles; + private String upstream; + private String destBranch; private String recommendShallow; private String url; private String defaultRevision; @@ -93,6 +95,7 @@ public class RepoProject implements Comparable<RepoProject> { * Do the copy file action. * * @throws IOException + * if an IO error occurred */ public void copy() throws IOException { File srcFile = new File(repo.getWorkTree(), @@ -180,7 +183,7 @@ public class RepoProject implements Comparable<RepoProject> { */ public RepoProject(String name, String path, String revision, String remote, String groupsParam) { - this(name, path, revision, remote, new HashSet<String>(), null); + this(name, path, revision, remote, new HashSet<>(), null); if (groupsParam != null && groupsParam.length() > 0) this.setGroups(groupsParam); } @@ -388,6 +391,57 @@ public class RepoProject implements Comparable<RepoProject> { this.linkfiles.clear(); } + /** + * Return the upstream attribute of the project + * + * @return the upstream value if present, null otherwise. + * + * @since 6.10 + */ + public String getUpstream() { + return this.upstream; + } + + /** + * Return the dest-branch attribute of the project + * + * @return the dest-branch value if present, null otherwise. + * + * @since 6.10 + */ + public String getDestBranch() { + return this.destBranch; + } + + /** + * Set the upstream attribute of the project + * + * Name of the git ref in which a sha1 can be found, when the revision is a + * sha1. + * + * @param upstream + * value of the attribute in the manifest + * + * @since 6.10 + */ + public void setUpstream(String upstream) { + this.upstream = upstream; + } + + /** + * Set the dest-branch attribute of the project + * + * Name of a Git branch. + * + * @param destBranch + * value of the attribute in the manifest + * + * @since 6.10 + */ + public void setDestBranch(String destBranch) { + this.destBranch = destBranch; + } + private String getPathWithSlash() { if (path.endsWith("/")) { //$NON-NLS-1$ return path; @@ -418,7 +472,6 @@ public class RepoProject implements Comparable<RepoProject> { return thatPath.startsWith(getPathWithSlash()); } - /** {@inheritDoc} */ @Override public boolean equals(Object o) { if (o instanceof RepoProject) { @@ -428,13 +481,11 @@ public class RepoProject implements Comparable<RepoProject> { return false; } - /** {@inheritDoc} */ @Override public int hashCode() { return this.getPathWithSlash().hashCode(); } - /** {@inheritDoc} */ @Override public int compareTo(RepoProject that) { return this.getPathWithSlash().compareTo(that.getPathWithSlash()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java index 68c57663e3..a012d94c32 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/internal/RepoText.java @@ -16,6 +16,7 @@ import org.eclipse.jgit.nls.TranslationBundle; /** * Translation bundle for repo command */ +@SuppressWarnings("MissingSummary") public class RepoText extends TranslationBundle { /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java index 023aef1c1f..17fdd7e7cb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java @@ -71,7 +71,6 @@ public class CommitMsgHook extends GitHook<String> { super(repo, outputStream, errorStream); } - /** {@inheritDoc} */ @Override public String call() throws IOException, AbortedByHookException { if (commitMessage == null) { @@ -94,7 +93,6 @@ public class CommitMsgHook extends GitHook<String> { return getCommitEditMessageFilePath() != null && commitMessage != null; } - /** {@inheritDoc} */ @Override public String getHookName() { return NAME; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java index b9dafcca31..3988ee06ca 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java @@ -61,14 +61,12 @@ public class PostCommitHook extends GitHook<Void> { super(repo, outputStream, errorStream); } - /** {@inheritDoc} */ @Override public Void call() throws IOException, AbortedByHookException { doRun(); return null; } - /** {@inheritDoc} */ @Override public String getHookName() { return NAME; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java index 321f476620..b12ea83b99 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java @@ -60,14 +60,12 @@ public class PreCommitHook extends GitHook<Void> { super(repo, outputStream, errorStream); } - /** {@inheritDoc} */ @Override public Void call() throws IOException, AbortedByHookException { doRun(); return null; } - /** {@inheritDoc} */ @Override public String getHookName() { return NAME; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java index 43dbc37f4f..e36312b3da 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java @@ -74,13 +74,11 @@ public class PrePushHook extends GitHook<String> { super(repo, outputStream, errorStream); } - /** {@inheritDoc} */ @Override protected String getStdinArgs() { return refs; } - /** {@inheritDoc} */ @Override public String call() throws IOException, AbortedByHookException { if (canRun()) { @@ -96,7 +94,6 @@ public class PrePushHook extends GitHook<String> { return true; } - /** {@inheritDoc} */ @Override public String getHookName() { return NAME; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java index 9dd565ff0a..b041729e28 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java @@ -208,7 +208,6 @@ public class FastIgnoreRule { return matcher == NO_MATCH; } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -221,7 +220,6 @@ public class FastIgnoreRule { } - /** {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; @@ -232,7 +230,6 @@ public class FastIgnoreRule { return result; } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java index 4e7f126a60..33dceb0717 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java @@ -190,7 +190,6 @@ public class IgnoreNode { return null; } - /** {@inheritDoc} */ @Override public String toString() { return rules.toString(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/AbstractMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/AbstractMatcher.java index 0737ed8878..a3f365ea0e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/AbstractMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/AbstractMatcher.java @@ -33,19 +33,16 @@ public abstract class AbstractMatcher implements IMatcher { this.dirOnly = dirOnly; } - /** {@inheritDoc} */ @Override public String toString() { return pattern; } - /** {@inheritDoc} */ @Override public int hashCode() { return pattern.hashCode(); } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java index 73e2ad3977..84376be53e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java @@ -22,7 +22,6 @@ public class LeadingAsteriskMatcher extends NameMatcher { "Pattern must have leading asterisk: " + pattern); //$NON-NLS-1$ } - /** {@inheritDoc} */ @Override public boolean matches(String segment, int startIncl, int endExcl) { // faster local access, same as in string.indexOf() diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java index 39fa74f6ea..4f8e149b4b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java @@ -38,7 +38,6 @@ public class NameMatcher extends AbstractMatcher { } } - /** {@inheritDoc} */ @Override public boolean matches(String path, boolean assumeDirectory, boolean pathMatch) { @@ -88,7 +87,6 @@ public class NameMatcher extends AbstractMatcher { return false; } - /** {@inheritDoc} */ @Override public boolean matches(String segment, int startIncl, int endExcl) { // faster local access, same as in string.indexOf() diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java index ba77b3dd87..a40568de08 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java @@ -91,6 +91,7 @@ public class PathMatcher extends AbstractMatcher { * a boolean. * @return never null * @throws org.eclipse.jgit.errors.InvalidPatternException + * if pattern is invalid */ public static IMatcher createPathMatcher(String pattern, Character pathSeparator, boolean dirOnly) @@ -149,7 +150,6 @@ public class PathMatcher extends AbstractMatcher { } } - /** {@inheritDoc} */ @Override public boolean matches(String path, boolean assumeDirectory, boolean pathMatch) { @@ -192,7 +192,6 @@ public class PathMatcher extends AbstractMatcher { return false; } - /** {@inheritDoc} */ @Override public boolean matches(String segment, int startIncl, int endExcl) { throw new UnsupportedOperationException( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java index fbce08adf4..3305a74af1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java @@ -232,16 +232,19 @@ public class Strings { .compile("\\[\\[[.=]\\w+[.=]\\]\\]"); //$NON-NLS-1$ /** - * Conversion from glob to Java regex following two sources: <li> - * http://man7.org/linux/man-pages/man7/glob.7.html <li> - * org.eclipse.jgit.fnmatch.FileNameMatcher.java Seems that there are + * Conversion from glob to Java regex following two sources: + * <ul> + * <li>http://man7.org/linux/man-pages/man7/glob.7.html + * <li>org.eclipse.jgit.fnmatch.FileNameMatcher.java Seems that there are * various ways to define what "glob" can be. + * </ul> * * @param pattern * non null pattern * * @return Java regex pattern corresponding to given glob pattern * @throws InvalidPatternException + * if pattern is invalid */ static Pattern convertGlob(String pattern) throws InvalidPatternException { if (UNSUPPORTED.matcher(pattern).find()) @@ -419,6 +422,7 @@ public class Strings { /** * @param buffer + * buffer * @return zero of the buffer is empty, otherwise the last character from * buffer */ @@ -427,10 +431,13 @@ public class Strings { } /** + * Lookahead next character after given index in pattern + * * @param pattern + * the pattern * @param i * current pointer in the pattern - * @return zero of the index is out of range, otherwise the next character + * @return zero if the index is out of range, otherwise the next character * from given position */ private static char lookAhead(String pattern, int i) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java index 7fa076b855..1f1c223270 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java @@ -22,7 +22,6 @@ public class TrailingAsteriskMatcher extends NameMatcher { "Pattern must have trailing asterisk: " + pattern); //$NON-NLS-1$ } - /** {@inheritDoc} */ @Override public boolean matches(String segment, int startIncl, int endExcl) { // faster local access, same as in string.indexOf() diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java index 76a88641c9..d976f6ea5b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java @@ -31,7 +31,6 @@ public class WildCardMatcher extends NameMatcher { p = convertGlob(subPattern); } - /** {@inheritDoc} */ @Override public boolean matches(String segment, int startIncl, int endExcl) { return p.matcher(segment.substring(startIncl, endExcl)).matches(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildMatcher.java index 1baa9abf19..606b585cdf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildMatcher.java @@ -26,7 +26,6 @@ public final class WildMatcher extends AbstractMatcher { super(WILDMATCH, dirOnly); } - /** {@inheritDoc} */ @Override public final boolean matches(String path, boolean assumeDirectory, boolean pathMatch) { @@ -34,7 +33,6 @@ public final class WildMatcher extends AbstractMatcher { || (!pathMatch && isSubdirectory(path)); } - /** {@inheritDoc} */ @Override public final boolean matches(String segment, int startIncl, int endExcl) { return true; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 518e0b7d9b..8928f47290 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -17,6 +17,7 @@ import org.eclipse.jgit.nls.TranslationBundle; /** * Translation bundle for JGit core */ +@SuppressWarnings("MissingSummary") public class JGitText extends TranslationBundle { /** @@ -46,6 +47,8 @@ public class JGitText extends TranslationBundle { /***/ public String applyBinaryOidTooShort; /***/ public String applyBinaryPatchTypeNotSupported; /***/ public String applyBinaryResultOidWrong; + /***/ public String applyPatchDestInvalid; + /***/ public String applyPatchSourceInvalid; /***/ public String applyPatchWithoutSourceOnAlreadyExistingSource; /***/ public String applyPatchWithCreationOverAlreadyExistingDestination; /***/ public String applyPatchWithSourceOnNonExistentSource; @@ -93,6 +96,7 @@ public class JGitText extends TranslationBundle { /***/ public String binaryHunkMissingNewline; /***/ public String bitmapMissingObject; /***/ public String bitmapsMustBePrepared; + /***/ public String bitmapUseNoopNoListener; /***/ public String blameNotCommittedYet; /***/ public String blockLimitNotMultipleOfBlockSize; /***/ public String blockLimitNotPositive; @@ -102,6 +106,7 @@ public class JGitText extends TranslationBundle { /***/ public String buildingBitmaps; /***/ public String cachedPacksPreventsIndexCreation; /***/ public String cachedPacksPreventsListingObjects; + /***/ public String cacheRegionAllOrNoneNull; /***/ public String cannotAccessLastModifiedForSafeDeletion; /***/ public String cannotBeCombined; /***/ public String cannotBeRecursiveWhenTreesAreIncluded; @@ -185,6 +190,7 @@ public class JGitText extends TranslationBundle { /***/ public String commitGraphChunkRepeated; /***/ public String commitGraphChunkUnknown; /***/ public String commitGraphFileIsTooLargeForJgit; + /***/ public String commitGraphUnexpectedSize; /***/ public String commitGraphWritingCancelled; /***/ public String commitMessageNotSpecified; /***/ public String commitOnRepoWithoutHEADCurrentlyNotSupported; @@ -192,6 +198,7 @@ public class JGitText extends TranslationBundle { /***/ public String commitsHaveAlreadyBeenMarkedAsStart; /***/ public String compressingObjects; /***/ public String computingCommitGeneration; + /***/ public String computingPathBloomFilters; /***/ public String configSubsectionContainsNewline; /***/ public String configSubsectionContainsNullByte; /***/ public String configValueContainsNullByte; @@ -252,6 +259,7 @@ public class JGitText extends TranslationBundle { /***/ public String corruptObjectTruncatedInName; /***/ public String corruptObjectTruncatedInObjectId; /***/ public String corruptObjectZeroId; + /***/ public String corruptReverseIndexChecksumIncorrect; /***/ public String corruptPack; /***/ public String corruptUseCnt; /***/ public String couldNotFindTabInLine; @@ -284,7 +292,9 @@ public class JGitText extends TranslationBundle { /***/ public String deletedOrphanInPackDir; /***/ public String deleteRequiresZeroNewId; /***/ public String deleteTagUnexpectedResult; + /***/ public String deletingBranches; /***/ public String deletingNotSupported; + /***/ public String deprecatedTrustFolderStat; /***/ public String depthMustBeAt1; /***/ public String depthWithUnshallow; /***/ public String destinationIsNotAWildcard; @@ -305,6 +315,9 @@ public class JGitText extends TranslationBundle { /***/ public String downloadCancelled; /***/ public String downloadCancelledDuringIndexing; /***/ public String duplicateAdvertisementsOf; + /***/ public String duplicateCacheTablesGiven; + /***/ public String duplicatePackExtensionsForCacheTables; + /***/ public String duplicatePackExtensionsSet; /***/ public String duplicateRef; /***/ public String duplicateRefAttribute; /***/ public String duplicateRemoteRefUpdateIsIllegal; @@ -414,6 +427,7 @@ public class JGitText extends TranslationBundle { /***/ public String initFailedNonBareRepoSameDirs; /***/ public String inMemoryBufferLimitExceeded; /***/ public String inputDidntMatchLength; + /***/ public String inputStreamClosed; /***/ public String inputStreamMustSupportMark; /***/ public String integerValueNotInRange; /***/ public String integerValueNotInRangeSubSection; @@ -479,6 +493,7 @@ public class JGitText extends TranslationBundle { /***/ public String invalidTimeUnitValue2; /***/ public String invalidTimeUnitValue3; /***/ public String invalidTreeZeroLengthName; + /***/ public String invalidTrustStat; /***/ public String invalidURL; /***/ public String invalidWildcards; /***/ public String invalidRefSpec; @@ -511,6 +526,8 @@ public class JGitText extends TranslationBundle { /***/ public String logLargerFiletimeDiff; /***/ public String logSmallerFiletime; /***/ public String logXDGConfigHomeInvalid; + + /***/ public String logXDGCacheHomeInvalid; /***/ public String looseObjectHandleIsStale; /***/ public String maxCountMustBeNonNegative; /***/ public String mergeConflictOnNonNoteEntries; @@ -523,6 +540,9 @@ public class JGitText extends TranslationBundle { /***/ public String mergeToolNotGivenError; /***/ public String mergeToolNullError; /***/ public String messageAndTaggerNotAllowedInUnannotatedTags; + /***/ public String midxChunkNeeded; + /***/ public String midxChunkRepeated; + /***/ public String midxChunkUnknown; /***/ public String minutesAgo; /***/ public String mismatchOffset; /***/ public String mismatchCRC; @@ -543,6 +563,10 @@ public class JGitText extends TranslationBundle { /***/ public String month; /***/ public String months; /***/ public String monthsAgo; + /***/ public String multiPackIndexFileIsTooLargeForJgit; + /***/ public String multiPackIndexPackCountMismatch; + /***/ public String multiPackIndexUnexpectedSize; + /***/ public String multiPackIndexWritingCancelled; /***/ public String multipleMergeBasesFor; /***/ public String nameMustNotBeNullOrEmpty; /***/ public String need2Arguments; @@ -558,6 +582,8 @@ public class JGitText extends TranslationBundle { /***/ public String noMergeHeadSpecified; /***/ public String nonBareLinkFilesNotSupported; /***/ public String nonCommitToHeads; + /***/ public String noPackExtConfigurationGiven; + /***/ public String noPackExtGivenForConfiguration; /***/ public String noPathAttributesFound; /***/ public String noSuchRef; /***/ public String noSuchRefKnown; @@ -567,6 +593,7 @@ public class JGitText extends TranslationBundle { /***/ public String notACommitGraph; /***/ public String notADIRCFile; /***/ public String notAGitDirectory; + /***/ public String notAMIDX; /***/ public String notAPACKFile; /***/ public String notARef; /***/ public String notASCIIString; @@ -578,6 +605,7 @@ public class JGitText extends TranslationBundle { /***/ public String notMergedExceptionMessage; /***/ public String notShallowedUnshallow; /***/ public String noXMLParserAvailable; + /***/ public String nullRevCommit; /***/ public String numberDoesntFit; /***/ public String objectAtHasBadZlibStream; /***/ public String objectIsCorrupt; @@ -589,7 +617,6 @@ public class JGitText extends TranslationBundle { /***/ public String oldIdMustNotBeNull; /***/ public String onlyOneFetchSupported; /***/ public String onlyOneOperationCallPerConnectionIsSupported; - /***/ public String onlyOpenPgpSupportedForSigning; /***/ public String openFilesMustBeAtLeast1; /***/ public String openingConnection; /***/ public String operationCanceled; @@ -611,6 +638,8 @@ public class JGitText extends TranslationBundle { /***/ public String packingCancelledDuringObjectsWriting; /***/ public String packObjectCountMismatch; /***/ public String packRefs; + /***/ public String packRefsFailed; + /***/ public String packRefsSuccessful; /***/ public String packSizeNotSetYet; /***/ public String packTooLargeForIndexVersion1; /***/ public String packWasDeleted; @@ -627,6 +656,7 @@ public class JGitText extends TranslationBundle { /***/ public String personIdentEmailNonNull; /***/ public String personIdentNameNonNull; /***/ public String postCommitHookFailed; + /***/ public String precedenceTrustConfig; /***/ public String prefixRemote; /***/ public String problemWithResolvingPushRefSpecsLocally; /***/ public String progressMonUploading; @@ -654,8 +684,6 @@ public class JGitText extends TranslationBundle { /***/ public String readerIsRequired; /***/ public String readingObjectsFromLocalRepositoryFailed; /***/ public String readLastModifiedFailed; - /***/ public String readPipeIsNotAllowed; - /***/ public String readPipeIsNotAllowedRequiredPermission; /***/ public String readTimedOut; /***/ public String receivePackObjectTooLarge1; /***/ public String receivePackObjectTooLarge2; @@ -732,6 +760,11 @@ public class JGitText extends TranslationBundle { /***/ public String shortReadOfBlock; /***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes; /***/ public String shortSkipOfBlock; + /***/ public String shutdownCleanup; + /***/ public String shutdownCleanupFailed; + /***/ public String shutdownCleanupListenerFailed; + /***/ public String signatureServiceConflict; + /***/ public String signatureTypeUnknown; /***/ public String signatureVerificationError; /***/ public String signatureVerificationUnavailable; /***/ public String signedTagMessageNoLf; @@ -818,6 +851,7 @@ public class JGitText extends TranslationBundle { /***/ public String unableToCheckConnectivity; /***/ public String unableToCreateNewObject; /***/ public String unableToReadFullInt; + /***/ public String unableToReadFullArray; /***/ public String unableToReadPackfile; /***/ public String unableToRemovePath; /***/ public String unableToWrite; @@ -843,6 +877,7 @@ public class JGitText extends TranslationBundle { /***/ public String unknownObjectInIndex; /***/ public String unknownObjectType; /***/ public String unknownObjectType2; + /***/ public String unknownPackExtension; /***/ public String unknownPositionEncoding; /***/ public String unknownRefStorageFormat; /***/ public String unknownRepositoryFormat; @@ -854,6 +889,8 @@ public class JGitText extends TranslationBundle { /***/ public String unmergedPaths; /***/ public String unpackException; /***/ public String unreadableCommitGraph; + /***/ public String unreadableMIDX; + /***/ public String unreadableObjectSizeIndex; /***/ public String unreadablePackIndex; /***/ public String unrecognizedPackExtension; /***/ public String unrecognizedRef; @@ -866,6 +903,9 @@ public class JGitText extends TranslationBundle { /***/ public String unsupportedEncryptionVersion; /***/ public String unsupportedGC; /***/ public String unsupportedMark; + /***/ public String unsupportedMIDXVersion; + /***/ public String unsupportedObjectIdVersion; + /***/ public String unsupportedObjectSizeIndexVersion; /***/ public String unsupportedOperationNotAddAtEnd; /***/ public String unsupportedPackIndexVersion; /***/ public String unsupportedPackReverseIndexVersion; @@ -874,6 +914,7 @@ public class JGitText extends TranslationBundle { /***/ public String unsupportedRepositoryDescription; /***/ public String unsupportedSizesObjSizeIndex; /***/ public String updateRequiresOldIdAndNewId; + /***/ public String updatingConfig; /***/ public String updatingHeadFailed; /***/ public String updatingReferences; /***/ public String updatingRefFailed; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java index d65624fc6a..56e5fa46d3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java @@ -48,12 +48,15 @@ public class FilteredRenameDetector { } /** + * Compute diff entries + * * @param diffs * The set of changes to check. * @param pathFilter * Filter out changes that didn't affect this path. * @return The subset of changes that affect only the filtered path. * @throws IOException + * if an IO error occurred */ public List<DiffEntry> compute(List<DiffEntry> diffs, PathFilter pathFilter) throws IOException { @@ -73,6 +76,7 @@ public class FilteredRenameDetector { * Filter out changes that didn't affect these paths. * @return The subset of changes that affect only the filtered paths. * @throws IOException + * if an IO error occurred * @see RenameDetector#compute() */ public List<DiffEntry> compute(List<DiffEntry> changes, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java index ebef5247e6..c64a844af1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java @@ -54,6 +54,8 @@ public class CommandExecutor { } /** + * Run command + * * @param command * the command string * @param workingDir @@ -62,8 +64,11 @@ public class CommandExecutor { * the environment * @return the execution result * @throws ToolException + * if a tool raised an error * @throws InterruptedException + * if thread was interrupted * @throws IOException + * if an IO error occurred */ public ExecutionResult run(String command, File workingDir, Map<String, String> env) @@ -101,6 +106,8 @@ public class CommandExecutor { } /** + * Check whether executable file is available + * * @param path * the executable path * @param workingDir @@ -109,8 +116,11 @@ public class CommandExecutor { * the environment * @return the execution result * @throws ToolException + * if a tool raised an error * @throws InterruptedException + * if thread was interrupted * @throws IOException + * if an IO error occurred */ public boolean checkExecutable(String path, File workingDir, Map<String, String> env) @@ -155,6 +165,9 @@ public class CommandExecutor { if (fs instanceof FS_POSIX) { commandArray = new String[1]; commandArray[0] = commandFile.getCanonicalPath(); + } else if (fs instanceof FS_Win32_Cygwin) { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$ } else if (fs instanceof FS_Win32) { if (useMsys2) { commandArray = new String[3]; @@ -166,9 +179,6 @@ public class CommandExecutor { commandArray = new String[1]; commandArray[0] = commandFile.getCanonicalPath(); } - } else if (fs instanceof FS_Win32_Cygwin) { - commandArray = new String[1]; - commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$ } else { throw new ToolException( "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java index 00dec32718..1e09796d23 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java @@ -205,6 +205,8 @@ public enum CommandLineDiffTool { private final String parameters; /** + * Get path + * * @return path */ public String getPath() { @@ -212,6 +214,8 @@ public enum CommandLineDiffTool { } /** + * Get parameters + * * @return parameters as one string */ public String getParameters() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java index 3a22124328..d8287f45d1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java @@ -273,11 +273,6 @@ public enum CommandLineMergeTool { this.exitCodeTrustable = exitCodeTrustable; } - CommandLineMergeTool(CommandLineMergeTool from) { - this(from.getPath(), from.getParameters(true), - from.getParameters(false), from.isExitCodeTrustable()); - } - CommandLineMergeTool(String path, CommandLineMergeTool from) { this(path, from.getParameters(true), from.getParameters(false), from.isExitCodeTrustable()); @@ -292,6 +287,8 @@ public enum CommandLineMergeTool { private final boolean exitCodeTrustable; /** + * Get path + * * @return path */ public String getPath() { @@ -299,6 +296,8 @@ public enum CommandLineMergeTool { } /** + * Get parameters + * * @param withBase * return parameters with base present? * @return parameters with or without base present @@ -311,6 +310,8 @@ public enum CommandLineMergeTool { } /** + * Whether exit code can be trusted + * * @return parameters */ public boolean isExitCodeTrustable() { @@ -318,6 +319,8 @@ public enum CommandLineMergeTool { } /** + * Whether command with with base present is valid + * * @return true if command with base present is valid, false otherwise */ public boolean canMergeWithoutBasePresent() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java index c8b04f90f2..e74337a8a1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java @@ -75,6 +75,8 @@ public class DiffToolConfig { } /** + * Get default tool name + * * @return the default diff tool name (diff.tool) */ public String getDefaultToolName() { @@ -82,6 +84,8 @@ public class DiffToolConfig { } /** + * Get default GUI tool name + * * @return the default GUI diff tool name (diff.guitool) */ public String getDefaultGuiToolName() { @@ -89,6 +93,8 @@ public class DiffToolConfig { } /** + * Get difftool.prompt option + * * @return the diff tool "prompt" option (difftool.prompt) */ public boolean isPrompt() { @@ -96,6 +102,8 @@ public class DiffToolConfig { } /** + * Get difftool.trustExitCode option + * * @return the diff tool "trust exit code" option (difftool.trustExitCode) */ public boolean isTrustExitCode() { @@ -103,6 +111,8 @@ public class DiffToolConfig { } /** + * Get tools map + * * @return the tools map */ public Map<String, ExternalDiffTool> getTools() { @@ -110,6 +120,8 @@ public class DiffToolConfig { } /** + * Get tool names + * * @return the tool names */ public Set<String> getToolNames() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index d0034df3bc..6a67bf3a1a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -172,6 +172,7 @@ public class DiffTools { * the "trust exit code" option * @return the execution result from tool * @throws ToolException + * when the tool fails */ public ExecutionResult compare(FileElement localFile, FileElement remoteFile, ExternalDiffTool tool, @@ -244,6 +245,7 @@ public class DiffTools { * path to the node in repository to parse git attributes for * @return name of the difftool if set * @throws ToolException + * when the tool failed */ public Optional<String> getExternalToolFromAttributes(final String path) throws ToolException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java index e01b892a53..e02697b772 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java @@ -16,21 +16,29 @@ package org.eclipse.jgit.internal.diffmergetool; public interface ExternalDiffTool { /** + * Get tool name + * * @return the tool name */ String getName(); /** + * Get tool path + * * @return the tool path */ String getPath(); /** + * Get tool command + * * @return the tool command */ String getCommand(); /** + * Whether tool is available + * * @return availability of the tool: true if tool can be executed and false * if not */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java index 0c3ddf9afe..022cd27f44 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java @@ -18,11 +18,15 @@ import org.eclipse.jgit.lib.internal.BooleanTriState; public interface ExternalMergeTool extends ExternalDiffTool { /** + * Get the tool "trust exit code" option + * * @return the tool "trust exit code" option */ BooleanTriState getTrustExitCode(); /** + * Get tool command + * * @param withBase * get command with base present (true) or without base present * (false) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java index b2dd846d70..e5947102eb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java @@ -57,6 +57,7 @@ public class ExternalToolUtils { * the base file (can be null) * @return the prepared (with replaced variables) command string * @throws IOException + * if an IO error occurred */ public static String prepareCommand(String command, FileElement localFile, FileElement remoteFile, FileElement mergedFile, @@ -91,6 +92,7 @@ public class ExternalToolUtils { * the base file (can be null) * @return the environment map with variables and values (file paths) * @throws IOException + * if an IO error occurred */ public static Map<String, String> prepareEnvironment(File gitDir, FileElement localFile, FileElement remoteFile, @@ -115,6 +117,8 @@ public class ExternalToolUtils { } /** + * Quote path + * * @param path * the path to be quoted * @return quoted path if it contains spaces @@ -136,6 +140,8 @@ public class ExternalToolUtils { } /** + * Whether tool is available + * * @param fs * the file system abstraction * @param gitDir @@ -160,6 +166,8 @@ public class ExternalToolUtils { } /** + * Create sorted tool set + * * @param defaultName * the default tool name * @param userDefinedNames @@ -209,6 +217,7 @@ public class ExternalToolUtils { * config key name for the tool * @return attribute value for the given tool key if set * @throws ToolException + * if the tool failed */ public static Optional<String> getExternalToolFromAttributes( final Repository repository, final String path, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java index ba8ca54c58..37eb2d9f7d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java @@ -28,7 +28,6 @@ public class FileElement { /** * The file element type. - * */ public enum Type { /** @@ -91,6 +90,8 @@ public class FileElement { } /** + * Create file element + * * @param path * the file path * @param type @@ -111,6 +112,8 @@ public class FileElement { } /** + * Get path + * * @return the file path */ public String getPath() { @@ -118,6 +121,8 @@ public class FileElement { } /** + * Get type + * * @return the element type */ public Type getType() { @@ -125,6 +130,8 @@ public class FileElement { } /** + * Get file + * <p> * Return * <ul> * <li>a temporary file if already created and stream is not valid</li> @@ -138,6 +145,7 @@ public class FileElement { * * @return the object stream * @throws IOException + * if an IO error occurred */ public File getFile() throws IOException { // if we have already temp file and no stream @@ -179,6 +187,7 @@ public class FileElement { * temporary directory is used * @return temporary file in directory or in the system temporary directory * @throws IOException + * if an IO error occurred */ public File createTempFile(File directory) throws IOException { if (tempFile == null) { @@ -204,6 +213,7 @@ public class FileElement { * the input string * @return the replaced input string * @throws IOException + * if an IO error occurred */ public String replaceVariable(String input) throws IOException { return input.replace("$" + type.name(), getFile().getPath()); //$NON-NLS-1$ @@ -215,6 +225,7 @@ public class FileElement { * @param env * the environment where this element should be added * @throws IOException + * if an IO error occurred */ public void addToEnv(Map<String, String> env) throws IOException { env.put(type.name(), getFile().getPath()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java index 9625d5f101..40cb820602 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java @@ -89,6 +89,8 @@ public class MergeToolConfig { } /** + * Get default tool name + * * @return the default merge tool name (merge.tool) */ public String getDefaultToolName() { @@ -96,6 +98,8 @@ public class MergeToolConfig { } /** + * Get default GUI tool name + * * @return the default GUI merge tool name (merge.guitool) */ public String getDefaultGuiToolName() { @@ -103,6 +107,8 @@ public class MergeToolConfig { } /** + * Get mergetool.prompt option + * * @return the merge tool "prompt" option (mergetool.prompt) */ public boolean isPrompt() { @@ -110,6 +116,8 @@ public class MergeToolConfig { } /** + * Get tool "keep backup" option + * * @return the tool "keep backup" option */ public boolean isKeepBackup() { @@ -117,6 +125,8 @@ public class MergeToolConfig { } /** + * Get tool "keep temporaries" option + * * @return the tool "keepTemporaries" option */ public boolean isKeepTemporaries() { @@ -124,6 +134,8 @@ public class MergeToolConfig { } /** + * Get the tool "write to temp" option + * * @return the tool "write to temp" option */ public boolean isWriteToTemp() { @@ -131,6 +143,8 @@ public class MergeToolConfig { } /** + * Get the tools map + * * @return the tools map */ public Map<String, ExternalMergeTool> getTools() { @@ -138,6 +152,8 @@ public class MergeToolConfig { } /** + * Get tool names + * * @return the tool names */ public Set<String> getToolNames() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java index b903201264..213ce6871e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -26,14 +26,13 @@ import java.util.Set; import java.util.TreeMap; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.internal.diffmergetool.FileElement.Type; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.FS; -import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.StringUtils; /** * Manages merge tools. @@ -184,6 +183,7 @@ public class MergeTools { * the selected tool * @return the execution result from tool * @throws ToolException + * if the tool failed */ public ExecutionResult merge(FileElement localFile, FileElement remoteFile, FileElement mergedFile, FileElement baseFile, File tempDir, @@ -241,7 +241,7 @@ public class MergeTools { FileElement backup = null; Path path = Paths.get(from.getPath()); if (Files.exists(path)) { - backup = new FileElement(from.getPath(), Type.BACKUP); + backup = new FileElement(from.getPath(), FileElement.Type.BACKUP); Files.copy(path, backup.createTempFile(toParentDir).toPath(), StandardCopyOption.REPLACE_EXISTING); } @@ -254,6 +254,7 @@ public class MergeTools { * @return the created temporary directory if (mergetol.writeToTemp == true) * or null if not configured or false. * @throws IOException + * if an IO error occurred */ public File createTempDirectory() throws IOException { return config.isWriteToTemp() @@ -271,6 +272,8 @@ public class MergeTools { } /** + * Get predefined tool names + * * @return the predefined tool names */ public Set<String> getPredefinedToolNames() { @@ -305,6 +308,7 @@ public class MergeTools { * path to the node in repository to parse git attributes for * @return name of the difftool if set * @throws ToolException + * if the tool failed */ public Optional<String> getExternalToolFromAttributes(final String path) throws ToolException { @@ -329,6 +333,8 @@ public class MergeTools { } /** + * Get user defined tools + * * @return the user defined tools */ public Map<String, ExternalMergeTool> getUserDefinedTools() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java index e1169a2d60..c1d69b4f11 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java @@ -43,6 +43,7 @@ public class PreDefinedDiffTool extends UserDefinedDiffTool { /** * @param path + * path string */ @Override public void setPath(String path) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java index 73d3588906..cd11325433 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java @@ -34,13 +34,15 @@ public class ToolException extends Exception { private static final long serialVersionUID = 1L; /** - * + * Create tool exception */ public ToolException() { this(null, null, false); } /** + * Create tool exception + * * @param message * the exception message */ @@ -49,6 +51,8 @@ public class ToolException extends Exception { } /** + * Create tool exception + * * @param message * the exception message * @param result @@ -64,6 +68,8 @@ public class ToolException extends Exception { } /** + * Create tool exception + * * @param message * the exception message * @param cause @@ -76,6 +82,8 @@ public class ToolException extends Exception { } /** + * Create tool exception + * * @param cause * the cause for throw */ @@ -86,6 +94,8 @@ public class ToolException extends Exception { } /** + * Whether result is valid + * * @return true if result is valid, false else */ public boolean isResult() { @@ -93,6 +103,8 @@ public class ToolException extends Exception { } /** + * Get execution result + * * @return the execution result */ public ExecutionResult getResult() { @@ -100,6 +112,8 @@ public class ToolException extends Exception { } /** + * Whether execution failed with an error + * * @return true if command execution error appears, false otherwise */ public boolean isCommandExecutionError() { @@ -107,6 +121,8 @@ public class ToolException extends Exception { } /** + * Get buffered stderr as a String + * * @return the result Stderr */ public String getResultStderr() { @@ -123,6 +139,8 @@ public class ToolException extends Exception { } /** + * Get buffered stdout as a String + * * @return the result Stdout */ public String getResultStdout() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java index eb72d01cdb..62bde28feb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java @@ -110,6 +110,8 @@ public class UserDefinedDiffTool implements ExternalDiffTool { } /** + * Set whether tool is available + * * @param available * true if tool can be found and false if not */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java index 1dd2f0d793..b1a5a22ef5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java @@ -49,6 +49,8 @@ public class UserDefinedMergeTool extends UserDefinedDiffTool } /** + * Set "trust exit code" flag + * * @param trustExitCode * the new "trust exit code" flag */ @@ -57,9 +59,11 @@ public class UserDefinedMergeTool extends UserDefinedDiffTool } /** + * Get command + * * @param withBase * not used, because user-defined merge tool can only define one - * cmd -> it must handle with and without base present (empty) + * cmd -> it must handle with and without base present (empty) * @return the tool command */ @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java index 8c1f773e19..e173ab5edc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java @@ -46,17 +46,29 @@ public class FsckError { this.errorType = errorType; } - /** @return identifier of the object. */ + /** + * Get Id + * + * @return identifier of the object. + */ public ObjectId getId() { return id; } - /** @return type of the object. */ + /** + * Get type + * + * @return type of the object. + */ public int getType() { return type; } - /** @return error type of the corruption. */ + /** + * Get error type + * + * @return error type of the corruption. + */ @Nullable public ObjectChecker.ErrorType getErrorType() { return errorType; @@ -81,12 +93,20 @@ public class FsckError { this.errorType = errorType; } - /** @return the file name of the index file. */ + /** + * Get file name + * + * @return the file name of the index file. + */ public String getFileName() { return fileName; } - /** @return the error type of the corruption. */ + /** + * Get error type + * + * @return the error type of the corruption. + */ public ErrorType getErrorType() { return errorType; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java index 3b94984561..795f4404e8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java @@ -66,7 +66,6 @@ public class FsckPackParser extends PackParser { this.blockSize = channel.blockSize() > 0 ? channel.blockSize() : 65536; } - /** {@inheritDoc} */ @Override protected void onPackHeader(long objCnt) throws IOException { if (expectedObjectCount >= 0) { @@ -78,48 +77,41 @@ public class FsckPackParser extends PackParser { } } - /** {@inheritDoc} */ @Override protected void onBeginWholeObject(long streamPosition, int type, long inflatedSize) throws IOException { crc.reset(); } - /** {@inheritDoc} */ @Override protected void onObjectHeader(Source src, byte[] raw, int pos, int len) throws IOException { crc.update(raw, pos, len); } - /** {@inheritDoc} */ @Override protected void onObjectData(Source src, byte[] raw, int pos, int len) throws IOException { crc.update(raw, pos, len); } - /** {@inheritDoc} */ @Override protected void onEndWholeObject(PackedObjectInfo info) throws IOException { info.setCRC((int) crc.getValue()); } - /** {@inheritDoc} */ @Override protected void onBeginOfsDelta(long deltaStreamPosition, long baseStreamPosition, long inflatedSize) throws IOException { crc.reset(); } - /** {@inheritDoc} */ @Override protected void onBeginRefDelta(long deltaStreamPosition, AnyObjectId baseId, long inflatedSize) throws IOException { crc.reset(); } - /** {@inheritDoc} */ @Override protected UnresolvedDelta onEndDelta() throws IOException { UnresolvedDelta delta = new UnresolvedDelta(); @@ -127,14 +119,12 @@ public class FsckPackParser extends PackParser { return delta; } - /** {@inheritDoc} */ @Override protected void onInflatedObjectData(PackedObjectInfo obj, int typeCode, byte[] data) throws IOException { // FsckPackParser ignores this event. } - /** {@inheritDoc} */ @Override protected void verifySafeObject(final AnyObjectId id, final int type, final byte[] data) { @@ -146,13 +136,11 @@ public class FsckPackParser extends PackParser { } } - /** {@inheritDoc} */ @Override protected void onPackFooter(byte[] hash) throws IOException { // Do nothing. } - /** {@inheritDoc} */ @Override protected boolean onAppendBase(int typeCode, byte[] data, PackedObjectInfo info) throws IOException { @@ -160,13 +148,11 @@ public class FsckPackParser extends PackParser { return false; } - /** {@inheritDoc} */ @Override protected void onEndThinPack() throws IOException { // Do nothing. } - /** {@inheritDoc} */ @Override protected ObjectTypeAndSize seekDatabase(PackedObjectInfo obj, ObjectTypeAndSize info) throws IOException { @@ -175,7 +161,6 @@ public class FsckPackParser extends PackParser { return readObjectHeader(info); } - /** {@inheritDoc} */ @Override protected ObjectTypeAndSize seekDatabase(UnresolvedDelta delta, ObjectTypeAndSize info) throws IOException { @@ -184,7 +169,6 @@ public class FsckPackParser extends PackParser { return readObjectHeader(info); } - /** {@inheritDoc} */ @Override protected int readDatabase(byte[] dst, int pos, int cnt) throws IOException { @@ -229,13 +213,11 @@ public class FsckPackParser extends PackParser { return buf.array(); } - /** {@inheritDoc} */ @Override protected boolean checkCRC(int oldCRC) { return oldCRC == (int) crc.getValue(); } - /** {@inheritDoc} */ @Override protected void onStoreStream(byte[] raw, int pos, int len) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddToBitmapFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddToBitmapFilter.java index 86d4722000..84f1cb968a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddToBitmapFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddToBitmapFilter.java @@ -40,7 +40,6 @@ public class AddToBitmapFilter extends RevFilter { this.bitmap = bitmap; } - /** {@inheritDoc} */ @Override public final boolean include(RevWalk walker, RevCommit cmit) { Bitmap visitedBitmap; @@ -61,13 +60,11 @@ public class AddToBitmapFilter extends RevFilter { return false; } - /** {@inheritDoc} */ @Override public final RevFilter clone() { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public final boolean requiresCommitBody() { return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddToBitmapWithCacheFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddToBitmapWithCacheFilter.java index d7ccadfbe7..a95432ce73 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddToBitmapWithCacheFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddToBitmapWithCacheFilter.java @@ -53,7 +53,6 @@ public class AddToBitmapWithCacheFilter extends RevFilter { this.bitmap = bitmap; } - /** {@inheritDoc} */ @Override public final boolean include(RevWalk rw, RevCommit c) { Bitmap visitedBitmap; @@ -76,13 +75,11 @@ public class AddToBitmapWithCacheFilter extends RevFilter { return false; } - /** {@inheritDoc} */ @Override public final RevFilter clone() { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public final boolean requiresCommitBody() { return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddUnseenToBitmapFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddUnseenToBitmapFilter.java index da9f9d031e..5ac267e789 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddUnseenToBitmapFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/AddUnseenToBitmapFilter.java @@ -48,7 +48,6 @@ public class AddUnseenToBitmapFilter extends RevFilter { this.bitmap = bitmap; } - /** {@inheritDoc} */ @Override public final boolean include(RevWalk walker, RevCommit cmit) { Bitmap visitedBitmap; @@ -69,13 +68,11 @@ public class AddUnseenToBitmapFilter extends RevFilter { return false; } - /** {@inheritDoc} */ @Override public final RevFilter clone() { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public final boolean requiresCommitBody() { return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityChecker.java index 37721ad1ea..f5f51e4830 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityChecker.java @@ -119,7 +119,6 @@ public class BitmappedReachabilityChecker implements ReachabilityChecker { this.reached = repoBitmaps.newBitmapBuilder(); } - /** {@inheritDoc} */ @Override public final boolean include(RevWalk walker, RevCommit cmit) { Bitmap commitBitmap; @@ -149,13 +148,11 @@ public class BitmappedReachabilityChecker implements ReachabilityChecker { } } - /** {@inheritDoc} */ @Override public final RevFilter clone() { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public final boolean requiresCommitBody() { return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/ChangedPathFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/ChangedPathFilter.java new file mode 100644 index 0000000000..ad3ce50181 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/ChangedPathFilter.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2023, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.commitgraph; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.Set; + +import org.apache.commons.codec.digest.MurmurHash3; + +/** + * A changed path filter for a commit. + * + * @since 6.7 + */ +public class ChangedPathFilter { + /** + * The number of times a path is hashed, as described in man + * gitformat-commit-graph(5). The value of this constant is the only value + * JGit currently supports. + */ + public static final int PATH_HASH_COUNT = 7; + + /** + * The minimum bits per entry, as described in man + * gitformat-commit-graph(5). The value of this constant is the only value + * JGit currently supports. + */ + public static final int BITS_PER_ENTRY = 10; + + /** + * Seed value as described in man gitformat-commit-graph(5). + */ + private static final int SEED1 = 0x293ae76f; + + /** + * Seed value as described in man gitformat-commit-graph(5). + */ + private static final int SEED2 = 0x7e646e2c; + + /** + * A filter that matches every path. + */ + public static final ChangedPathFilter FULL = new ChangedPathFilter( + new byte[] { (byte) 0xff }, 0, 1); + + private static final ChangedPathFilter EMPTY = new ChangedPathFilter( + new byte[] { (byte) 0 }, 0, 1); + + private final byte[] data; + + private final int offset; + + private final int length; + + /** + * Constructs a changed path filter. + * + * @param data + * data (possibly read from a commit graph file) + * @param offset + * offset into data + * @param length + * length of data + */ + private ChangedPathFilter(byte[] data, int offset, int length) { + this.data = data; + this.offset = offset; + this.length = length; + } + + /** + * Returns a filter that matches all given paths. + * <p> + * Because of the nature of Bloom filters, this filter may also match paths + * not in the given set. + * + * @param paths + * the paths that the filter must match + * @return the corresponding filter + */ + @SuppressWarnings("ByteBufferBackingArray") + public static ChangedPathFilter fromPaths(Set<ByteBuffer> paths) { + if (paths.isEmpty()) { + return EMPTY; + } + byte[] bloom = new byte[-Math + .floorDiv(-paths.size() * ChangedPathFilter.BITS_PER_ENTRY, 8)]; + for (ByteBuffer path : paths) { + add(bloom, path.array(), path.position(), + path.limit() - path.position()); + } + return new ChangedPathFilter(bloom, 0, bloom.length); + } + + /** + * Returns a filter read from a file. + * + * @param data + * data (read from a commit graph file) + * @param offset + * offset into data + * @param length + * length of data + * + * @return the corresponding filter + */ + public static ChangedPathFilter fromFile(byte[] data, int offset, + int length) { + return new ChangedPathFilter(data, offset, length); + } + + private static void add(byte[] changedPathFilterData, byte[] path, + int offset, int length) { + + int hash0 = MurmurHash3.hash32x86(path, offset, length, SEED1); + int hash1 = MurmurHash3.hash32x86(path, offset, length, SEED2); + for (int i = 0; i < PATH_HASH_COUNT; i++) { + int pos = Integer.remainderUnsigned(hash0 + i * hash1, + changedPathFilterData.length * 8); + changedPathFilterData[pos / 8] |= (byte) (1 << (pos % 8)); + } + } + + /** + * Checks if this changed path filter could contain path. + * + * @param path + * path to check existence of + * @return true if the filter could contain path, false if the filter + * definitely does not contain path + */ + public boolean maybeContains(byte[] path) { + int hash0 = MurmurHash3.hash32x86(path, 0, path.length, SEED1); + int hash1 = MurmurHash3.hash32x86(path, 0, path.length, SEED2); + int bloomFilterBits = length * 8; + for (int i = 0; i < PATH_HASH_COUNT; i++) { + int pos = Integer.remainderUnsigned(hash0 + i * hash1, + bloomFilterBits); + if ((data[offset + (pos / 8)] & (byte) (1 << (pos % 8))) == 0) { + return false; + } + } + return true; + } + + /** + * Writes this filter to the given stream. + * + * @param s + * stream to write to + */ + public void writeTo(ByteArrayOutputStream s) { + s.write(data, offset, length); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraph.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraph.java index 0796293f52..d1178c2850 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraph.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraph.java @@ -32,25 +32,26 @@ public interface CommitGraph { /** Empty {@link CommitGraph} with no results. */ CommitGraph EMPTY = new CommitGraph() { - /** {@inheritDoc} */ @Override public int findGraphPosition(AnyObjectId commit) { return -1; } - /** {@inheritDoc} */ @Override public CommitData getCommitData(int graphPos) { return null; } - /** {@inheritDoc} */ @Override public ObjectId getObjectId(int graphPos) { return null; } - /** {@inheritDoc} */ + @Override + public ChangedPathFilter getChangedPathFilter(int graphPos) { + return null; + } + @Override public long getCommitCnt() { return 0; @@ -97,6 +98,15 @@ public interface CommitGraph { ObjectId getObjectId(int graphPos); /** + * Get the changed path filter of the object at the commit-graph position. + * + * @param graphPos + * the position in the commit-graph of the object. + * @return the bloom filter or null if it's not found. + */ + ChangedPathFilter getChangedPathFilter(int graphPos); + + /** * Obtain the total number of commits described by this commit-graph. * * @return number of commits in this commit-graph. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphBuilder.java index a6af3bc592..5e993eab22 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphBuilder.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.internal.storage.commitgraph; +import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA; +import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX; import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_COMMIT_DATA; import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_EXTRA_EDGE_LIST; import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_OID_FANOUT; @@ -35,7 +37,15 @@ class CommitGraphBuilder { private byte[] extraList; - /** @return A builder of {@link CommitGraph}. */ + private byte[] bloomFilterIndex; + + private byte[] bloomFilterData; + + /** + * Create builder + * + * @return A builder of {@link CommitGraph}. + */ static CommitGraphBuilder builder() { return new CommitGraphBuilder(OBJECT_ID_LENGTH); } @@ -72,6 +82,20 @@ class CommitGraphBuilder { return this; } + CommitGraphBuilder addBloomFilterIndex(byte[] buffer) + throws CommitGraphFormatException { + assertChunkNotSeenYet(bloomFilterIndex, CHUNK_ID_BLOOM_FILTER_INDEX); + bloomFilterIndex = buffer; + return this; + } + + CommitGraphBuilder addBloomFilterData(byte[] buffer) + throws CommitGraphFormatException { + assertChunkNotSeenYet(bloomFilterData, CHUNK_ID_BLOOM_FILTER_DATA); + bloomFilterData = buffer; + return this; + } + CommitGraph build() throws CommitGraphFormatException { assertChunkNotNull(oidFanout, CHUNK_ID_OID_FANOUT); assertChunkNotNull(oidLookup, CHUNK_ID_OID_LOOKUP); @@ -81,7 +105,9 @@ class CommitGraphBuilder { oidLookup); GraphCommitData commitDataChunk = new GraphCommitData(hashLength, commitData, extraList); - return new CommitGraphV1(index, commitDataChunk); + GraphChangedPathFilterData cpfData = new GraphChangedPathFilterData( + bloomFilterIndex, bloomFilterData); + return new CommitGraphV1(index, commitDataChunk, cpfData); } private void assertChunkNotNull(Object object, int chunkId) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphConstants.java index a074833fa5..8d2789cf52 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphConstants.java @@ -25,6 +25,10 @@ class CommitGraphConstants { static final int CHUNK_ID_EXTRA_EDGE_LIST = 0x45444745; /* "EDGE" */ + static final int CHUNK_ID_BLOOM_FILTER_INDEX = 0x42494458; /* "BIDX" */ + + static final int CHUNK_ID_BLOOM_FILTER_DATA = 0x42444154; /* "BDAT" */ + /** * First 4 bytes describe the chunk id. Value 0 is a terminating label. * Other 8 bytes provide the byte-offset in current file for chunk to start. @@ -41,15 +45,15 @@ class CommitGraphConstants { /** Mask to make the last edgeValue into position */ static final int GRAPH_EDGE_LAST_MASK = 0x7fffffff; - /** EdgeValue & GRAPH_LAST_EDGE != 0 means it is the last edgeValue */ + /** EdgeValue & GRAPH_LAST_EDGE != 0 means it is the last edgeValue */ static final int GRAPH_LAST_EDGE = 0x80000000; /** EdgeValue == GRAPH_NO_PARENT means it has no parents */ static final int GRAPH_NO_PARENT = 0x70000000; /** - * EdgeValue & GRAPH_EXTRA_EDGES_NEEDED != 0 means its other parents are in - * Chunk Extra Edge List + * EdgeValue & GRAPH_EXTRA_EDGES_NEEDED != 0 means its other parents are + * in Chunk Extra Edge List */ static final int GRAPH_EXTRA_EDGES_NEEDED = 0x80000000; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphLoader.java index 571f5f4ebe..7e9220dc0d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphLoader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphLoader.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.internal.storage.commitgraph; +import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA; +import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX; import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_COMMIT_DATA; import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_EXTRA_EDGE_LIST; import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_OID_FANOUT; @@ -25,9 +27,12 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.io.SilentFileInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -93,6 +98,46 @@ public class CommitGraphLoader { */ public static CommitGraph read(InputStream fd) throws CommitGraphFormatException, IOException { + + boolean readChangedPathFilters; + try { + readChangedPathFilters = SystemReader.getInstance().getJGitConfig() + .getBoolean(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION, + ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS, + false); + } catch (ConfigInvalidException e) { + // Use the default value if, for some reason, the config couldn't be + // read. + readChangedPathFilters = false; + } + + return read(fd, readChangedPathFilters); + } + + /** + * Read an existing commit-graph file from a buffered stream. + * <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. + * + * @param fd + * stream to read the commit-graph file from. The stream must be + * buffered as some small IOs are performed against the stream. + * The caller is responsible for closing the stream. + * + * @param readChangedPathFilters + * enable reading bloom filter chunks. + * + * @return a copy of the commit-graph file in memory + * @throws CommitGraphFormatException + * the commit-graph file's format is different from we expected. + * @throws java.io.IOException + * the stream cannot be read. + */ + public static CommitGraph read(InputStream fd, + boolean readChangedPathFilters) + throws CommitGraphFormatException, IOException { byte[] hdr = new byte[8]; IO.readFully(fd, hdr, 0, hdr.length); @@ -164,6 +209,16 @@ public class CommitGraphLoader { case CHUNK_ID_EXTRA_EDGE_LIST: builder.addExtraList(buffer); break; + case CHUNK_ID_BLOOM_FILTER_INDEX: + if (readChangedPathFilters) { + builder.addBloomFilterIndex(buffer); + } + break; + case CHUNK_ID_BLOOM_FILTER_DATA: + if (readChangedPathFilters) { + builder.addBloomFilterData(buffer); + } + break; default: LOG.warn(MessageFormat.format( JGitText.get().commitGraphChunkUnknown, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphV1.java index da172192e4..b0a9c83848 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphV1.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphV1.java @@ -24,18 +24,20 @@ class CommitGraphV1 implements CommitGraph { private final GraphCommitData commitData; - CommitGraphV1(GraphObjectIndex index, GraphCommitData commitData) { + private final GraphChangedPathFilterData cpfData; + + CommitGraphV1(GraphObjectIndex index, GraphCommitData commitData, + GraphChangedPathFilterData cpfData) { this.idx = index; this.commitData = commitData; + this.cpfData = cpfData; } - /** {@inheritDoc} */ @Override public int findGraphPosition(AnyObjectId commit) { return idx.findGraphPosition(commit); } - /** {@inheritDoc} */ @Override public CommitData getCommitData(int graphPos) { if (graphPos < 0 || graphPos >= getCommitCnt()) { @@ -44,13 +46,16 @@ class CommitGraphV1 implements CommitGraph { return commitData.getCommitData(graphPos); } - /** {@inheritDoc} */ @Override public ObjectId getObjectId(int graphPos) { return idx.getObjectId(graphPos); } - /** {@inheritDoc} */ + @Override + public ChangedPathFilter getChangedPathFilter(int graphPos) { + return cpfData.getChangedPathFilter(graphPos); + } + @Override public long getCommitCnt() { return idx.getCommitCnt(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriter.java index a58a9eb632..55539e2a66 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriter.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.internal.storage.commitgraph; +import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA; +import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX; import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_COMMIT_DATA; import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_EXTRA_EDGE_LIST; import static org.eclipse.jgit.internal.storage.commitgraph.CommitGraphConstants.CHUNK_ID_OID_FANOUT; @@ -24,21 +26,33 @@ import static org.eclipse.jgit.lib.Constants.COMMIT_GENERATION_NOT_COMPUTED; import static org.eclipse.jgit.lib.Constants.COMMIT_GENERATION_UNKNOWN; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InterruptedIOException; import java.io.OutputStream; -import java.text.MessageFormat; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Stack; +import java.util.Optional; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.io.CancellableDigestOutputStream; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.NB; /** @@ -56,10 +70,17 @@ public class CommitGraphWriter { private static final int GENERATION_NUMBER_MAX = 0x3FFFFFFF; + private static final int MAX_CHANGED_PATHS = 512; + + private static final PathDiffCalculator PATH_DIFF_CALCULATOR + = new PathDiffCalculator(); + private final int hashsz; private final GraphCommits graphCommits; + private final boolean generateChangedPathFilters; + /** * Create commit-graph writer for these commits. * @@ -67,8 +88,22 @@ public class CommitGraphWriter { * the commits which will be writen to the commit-graph. */ public CommitGraphWriter(@NonNull GraphCommits graphCommits) { + this(graphCommits, false); + } + + /** + * Create commit-graph writer for these commits. + * + * @param graphCommits + * the commits which will be writen to the commit-graph. + * @param generateChangedPathFilters + * whether changed path filters are generated + */ + public CommitGraphWriter(@NonNull GraphCommits graphCommits, + boolean generateChangedPathFilters) { this.graphCommits = graphCommits; this.hashsz = OBJECT_ID_LENGTH; + this.generateChangedPathFilters = generateChangedPathFilters; } /** @@ -80,37 +115,46 @@ public class CommitGraphWriter { * output stream of commit-graph data. The stream should be * buffered by the caller. The caller is responsible for closing * the stream. + * @return statistics gathered during the run * @throws IOException + * if an error occurred */ - public void write(@NonNull ProgressMonitor monitor, + public Stats write(@NonNull ProgressMonitor monitor, @NonNull OutputStream commitGraphStream) throws IOException { if (graphCommits.size() == 0) { - return; + return Stats.EMPTY; } - List<ChunkHeader> chunks = createChunks(); - long writeCount = 256 + 2 * graphCommits.size() - + graphCommits.getExtraEdgeCnt(); - monitor.beginTask( - MessageFormat.format(JGitText.get().writingOutCommitGraph, - Integer.valueOf(chunks.size())), - (int) writeCount); + BloomFilterChunks bloomFilterChunks = generateChangedPathFilters + ? computeBloomFilterChunks(monitor) + : null; + List<ChunkHeader> chunks = new ArrayList<>(); + chunks.addAll(createCoreChunks(hashsz, graphCommits)); + chunks.addAll(createBloomFilterChunkHeaders(bloomFilterChunks)); + chunks = Collections.unmodifiableList(chunks); + long expectedSize = calculateExpectedSize(chunks); try (CancellableDigestOutputStream out = new CancellableDigestOutputStream( monitor, commitGraphStream)) { writeHeader(out, chunks.size()); writeChunkLookup(out, chunks); - writeChunks(monitor, out, chunks); + writeChunks(out, chunks); writeCheckSum(out); + if (expectedSize != out.length()) { + throw new IllegalStateException(String.format( + JGitText.get().commitGraphUnexpectedSize, + Long.valueOf(expectedSize), + Long.valueOf(out.length()))); + } } catch (InterruptedIOException e) { throw new IOException(JGitText.get().commitGraphWritingCancelled, e); - } finally { - monitor.endTask(); } + return Stats.from(bloomFilterChunks); } - private List<ChunkHeader> createChunks() { + private static List<ChunkHeader> createCoreChunks(int hashsz, + GraphCommits graphCommits) { List<ChunkHeader> chunks = new ArrayList<>(); chunks.add(new ChunkHeader(CHUNK_ID_OID_FANOUT, GRAPH_FANOUT_SIZE)); chunks.add(new ChunkHeader(CHUNK_ID_OID_LOOKUP, @@ -121,7 +165,25 @@ public class CommitGraphWriter { chunks.add(new ChunkHeader(CHUNK_ID_EXTRA_EDGE_LIST, graphCommits.getExtraEdgeCnt() * 4)); } - return chunks; + return Collections.unmodifiableList(chunks); + } + + private static List<ChunkHeader> createBloomFilterChunkHeaders( + @Nullable BloomFilterChunks bloomFilterChunks) { + List<ChunkHeader> chunks = new ArrayList<>(); + if (bloomFilterChunks != null) { + chunks.add(new ChunkHeader(CHUNK_ID_BLOOM_FILTER_INDEX, + bloomFilterChunks.index)); + chunks.add(new ChunkHeader(CHUNK_ID_BLOOM_FILTER_DATA, + bloomFilterChunks.data)); + } + return Collections.unmodifiableList(chunks); + } + + private static long calculateExpectedSize(List<ChunkHeader> chunks) { + int chunkLookup = (chunks.size() + 1) * CHUNK_LOOKUP_WIDTH; + long chunkContent = chunks.stream().mapToLong(c -> c.size).sum(); + return /* header */ 8 + chunkLookup + chunkContent + /* CRC */ 20; } private void writeHeader(CancellableDigestOutputStream out, int numChunks) @@ -138,7 +200,7 @@ public class CommitGraphWriter { private void writeChunkLookup(CancellableDigestOutputStream out, List<ChunkHeader> chunks) throws IOException { int numChunks = chunks.size(); - long chunkOffset = 8 + (numChunks + 1) * CHUNK_LOOKUP_WIDTH; + long chunkOffset = 8 + (numChunks + 1L) * CHUNK_LOOKUP_WIDTH; byte[] buffer = new byte[CHUNK_LOOKUP_WIDTH]; for (ChunkHeader chunk : chunks) { NB.encodeInt32(buffer, 0, chunk.id); @@ -151,9 +213,8 @@ public class CommitGraphWriter { out.write(buffer); } - private void writeChunks(ProgressMonitor monitor, - CancellableDigestOutputStream out, List<ChunkHeader> chunks) - throws IOException { + private void writeChunks(CancellableDigestOutputStream out, + List<ChunkHeader> chunks) throws IOException { for (ChunkHeader chunk : chunks) { int chunkId = chunk.id; @@ -165,11 +226,22 @@ public class CommitGraphWriter { writeOidLookUp(out); break; case CHUNK_ID_COMMIT_DATA: - writeCommitData(monitor, out); + writeCommitData(out); break; case CHUNK_ID_EXTRA_EDGE_LIST: writeExtraEdges(out); break; + case CHUNK_ID_BLOOM_FILTER_INDEX: + case CHUNK_ID_BLOOM_FILTER_DATA: + if (!chunk.data.isPresent()) { + throw new IllegalStateException( + "data for this chunk must be precomputed"); //$NON-NLS-1$ + } + chunk.data.get().writeTo(out); + break; + default: + throw new IllegalStateException( + "Don't know how to write chunk " + chunkId); //$NON-NLS-1$ } } } @@ -193,7 +265,6 @@ public class CommitGraphWriter { for (int n : fanout) { NB.encodeInt32(tmp, 0, n); out.write(tmp, 0, 4); - out.getWriteMonitor().update(1); } } @@ -204,13 +275,15 @@ public class CommitGraphWriter { for (RevCommit c : graphCommits) { c.copyRawTo(tmp, 0); out.write(tmp, 0, hashsz); - out.getWriteMonitor().update(1); } } - private void writeCommitData(ProgressMonitor monitor, - CancellableDigestOutputStream out) throws IOException { + private void writeCommitData(CancellableDigestOutputStream out) + throws IOException { + ProgressMonitor monitor = out.getWriteMonitor(); int[] generations = computeGenerationNumbers(monitor); + monitor.beginTask(JGitText.get().writingOutCommitGraph, + graphCommits.size()); int num = 0; byte[] tmp = new byte[hashsz + COMMIT_DATA_WIDTH]; int i = 0; @@ -248,9 +321,10 @@ public class CommitGraphWriter { NB.encodeInt32(tmp, hashsz + 12, packedDate[1]); out.write(tmp); - out.getWriteMonitor().update(1); + monitor.update(1); i++; } + monitor.endTask(); } private int[] computeGenerationNumbers(ProgressMonitor monitor) @@ -266,10 +340,10 @@ public class CommitGraphWriter { continue; } - Stack<RevCommit> commitStack = new Stack<>(); + ArrayDeque<RevCommit> commitStack = new ArrayDeque<>(); commitStack.push(cmit); - while (!commitStack.empty()) { + while (!commitStack.isEmpty()) { int maxGeneration = 0; boolean allParentComputed = true; RevCommit current = commitStack.peek(); @@ -304,6 +378,54 @@ public class CommitGraphWriter { return generations; } + private BloomFilterChunks computeBloomFilterChunks(ProgressMonitor monitor) + throws MissingObjectException, IncorrectObjectTypeException, + CorruptObjectException, IOException { + + ByteArrayOutputStream index = new ByteArrayOutputStream(); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + long filtersReused = 0; + long filtersComputed =0; + + // Allocate scratch buffer for converting integers into + // big-endian bytes. + byte[] scratch = new byte[4]; + + NB.encodeInt32(scratch, 0, 1); // version 1 + data.write(scratch); + NB.encodeInt32(scratch, 0, ChangedPathFilter.PATH_HASH_COUNT); + data.write(scratch); + NB.encodeInt32(scratch, 0, ChangedPathFilter.BITS_PER_ENTRY); + data.write(scratch); + int dataHeaderSize = data.size(); + + try (RevWalk rw = new RevWalk(graphCommits.getObjectReader())) { + monitor.beginTask(JGitText.get().computingPathBloomFilters, + graphCommits.size()); + for (RevCommit cmit : graphCommits) { + ChangedPathFilter cpf = cmit.getChangedPathFilter(rw); + if (cpf != null) { + filtersReused++; + } else { + filtersComputed++; + Optional<HashSet<ByteBuffer>> paths = PATH_DIFF_CALCULATOR + .changedPaths(graphCommits.getObjectReader(), cmit); + if (paths.isEmpty()) { + cpf = ChangedPathFilter.FULL; + } else { + cpf = ChangedPathFilter.fromPaths(paths.get()); + } + } + cpf.writeTo(data); + NB.encodeInt32(scratch, 0, data.size() - dataHeaderSize); + index.write(scratch); + monitor.update(1); + } + monitor.endTask(); + return new BloomFilterChunks(index, data, filtersReused, filtersComputed); + } + } + private void writeExtraEdges(CancellableDigestOutputStream out) throws IOException { byte[] tmp = new byte[4]; @@ -319,20 +441,129 @@ public class CommitGraphWriter { } NB.encodeInt32(tmp, 0, edgeValue); out.write(tmp); - out.getWriteMonitor().update(1); } } } } + // Visible for testing + static class PathDiffCalculator { + + // Walk steps in the last invocation of changedPaths + int stepCounter; + + Optional<HashSet<ByteBuffer>> changedPaths( + ObjectReader or, RevCommit cmit) throws MissingObjectException, + IncorrectObjectTypeException, CorruptObjectException, IOException { + stepCounter = 0; + HashSet<ByteBuffer> paths = new HashSet<>(); + try (TreeWalk walk = new TreeWalk(null, or)) { + walk.setRecursive(true); + walk.setFilter(TreeFilter.ANY_DIFF); + if (cmit.getParentCount() == 0) { + walk.addTree(new EmptyTreeIterator()); + } else { + walk.addTree(cmit.getParent(0).getTree()); + } + walk.addTree(cmit.getTree()); + while (walk.next()) { + stepCounter += 1; + byte[] rawPath = walk.getRawPath(); + paths.add(ByteBuffer.wrap(rawPath)); + for (int i = 0; i < rawPath.length; i++) { + if (rawPath[i] == '/') { + paths.add(ByteBuffer.wrap(rawPath, 0, i)); + } + if (paths.size() > MAX_CHANGED_PATHS) { + return Optional.empty(); + } + } + } + } + return Optional.of(paths); + } + } + private static class ChunkHeader { final int id; final long size; + final Optional<ByteArrayOutputStream> data; + public ChunkHeader(int id, long size) { this.id = id; this.size = size; + this.data = Optional.empty(); + } + + ChunkHeader(int id, ByteArrayOutputStream data) { + this.id = id; + this.size = data.size(); + this.data = Optional.of(data); + } + } + + private static class BloomFilterChunks { + final ByteArrayOutputStream index; + + final ByteArrayOutputStream data; + + final long filtersReused; + + final long filtersComputed; + + BloomFilterChunks(ByteArrayOutputStream index, + ByteArrayOutputStream data, + long filtersReused, + long filtersComputed) { + this.index = index; + this.data = data; + this.filtersReused = filtersReused; + this.filtersComputed = filtersComputed; + } + } + + /** + * Statistics collected during a single commit graph write. + */ + public static class Stats { + + static final Stats EMPTY = new Stats(); + + static final Stats from(@Nullable BloomFilterChunks bloomFilterChunks) { + Stats stats = new Stats(); + if (bloomFilterChunks != null) { + stats.changedPathFiltersComputed = bloomFilterChunks.filtersComputed; + stats.changedPathFiltersReused = bloomFilterChunks.filtersReused; + } + return stats; + } + + private Stats() {} + + private long changedPathFiltersReused = 0; + + private long changedPathFiltersComputed = 0; + + /** + * Returns the number of existing changed path filters that were reused + * when writing, for statistical purposes. + * + * @return count of changed path filters + */ + public long getChangedPathFiltersReused() { + return changedPathFiltersReused; + } + + /** + * Returns the number of changed path filters that were computed from + * scratch, for statistical purposes. + * + * @return count of changed path filters + */ + public long getChangedPathFiltersComputed() { + return changedPathFiltersComputed; } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphChangedPathFilterData.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphChangedPathFilterData.java new file mode 100644 index 0000000000..738a42a01d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphChangedPathFilterData.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023, Google LLC. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.commitgraph; + +import org.eclipse.jgit.util.NB; + +/** + * Represents the BIDX and BDAT data found in a commit graph file. + */ +class GraphChangedPathFilterData { + + private static final int BIDX_BYTES_PER_ENTRY = 4; + + private static final int BDAT_HEADER_BYTES = 12; + + private final byte[] bloomFilterIndex; + + private final byte[] bloomFilterData; + + /** + * Initialize the GraphChangedPathFilterData. + * + * @param bloomFilterIndex + * content of BIDX chunk, if it exists + * @param bloomFilterData + * content of BDAT chunk, if it exists + */ + GraphChangedPathFilterData(byte[] bloomFilterIndex, + byte[] bloomFilterData) { + + if ((bloomFilterIndex == null) != (bloomFilterData == null)) { + bloomFilterIndex = null; + bloomFilterData = null; + } + if (bloomFilterData != null + && (NB.decodeUInt32(bloomFilterData, + 4) != ChangedPathFilter.PATH_HASH_COUNT + || NB.decodeUInt32(bloomFilterData, + 8) != ChangedPathFilter.BITS_PER_ENTRY)) { + bloomFilterIndex = null; + bloomFilterData = null; + } + + this.bloomFilterIndex = bloomFilterIndex; + this.bloomFilterData = bloomFilterData; + } + + ChangedPathFilter getChangedPathFilter(int graphPos) { + if (bloomFilterIndex == null) { + return null; + } + int priorCumul = graphPos == 0 ? 0 + : NB.decodeInt32(bloomFilterIndex, + graphPos * BIDX_BYTES_PER_ENTRY - BIDX_BYTES_PER_ENTRY); + int cumul = NB.decodeInt32(bloomFilterIndex, graphPos * BIDX_BYTES_PER_ENTRY); + return ChangedPathFilter.fromFile(bloomFilterData, + priorCumul + BDAT_HEADER_BYTES, + cumul - priorCumul); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphCommits.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphCommits.java index ccf6d0e66a..c77d950377 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphCommits.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphCommits.java @@ -23,6 +23,7 @@ import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdOwnerMap; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; @@ -56,6 +57,7 @@ public class GraphCommits implements Iterable<RevCommit> { * @return the commits' collection which are used by the commit-graph * writer. Never null. * @throws IOException + * if an error occurred */ public static GraphCommits fromWalk(ProgressMonitor pm, @NonNull Set<? extends ObjectId> wants, @NonNull RevWalk walk) @@ -78,7 +80,7 @@ public class GraphCommits implements Iterable<RevCommit> { commits.add(c); } pm.endTask(); - return new GraphCommits(commits); + return new GraphCommits(commits, walk.getObjectReader()); } private final List<RevCommit> sortedCommits; @@ -87,13 +89,17 @@ public class GraphCommits implements Iterable<RevCommit> { private final int extraEdgeCnt; + private final ObjectReader objectReader; + /** * Initialize the GraphCommits. * * @param commits * list of commits with their headers already parsed. + * @param objectReader + * object reader */ - private GraphCommits(List<RevCommit> commits) { + private GraphCommits(List<RevCommit> commits, ObjectReader objectReader) { Collections.sort(commits); // sorted by name sortedCommits = commits; commitPosMap = new ObjectIdOwnerMap<>(); @@ -106,6 +112,7 @@ public class GraphCommits implements Iterable<RevCommit> { commitPosMap.add(new CommitWithPosition(c, i)); } this.extraEdgeCnt = cnt; + this.objectReader = objectReader; } int getOidPosition(RevCommit c) throws MissingObjectException { @@ -124,7 +131,10 @@ public class GraphCommits implements Iterable<RevCommit> { return sortedCommits.size(); } - /** {@inheritDoc} */ + ObjectReader getObjectReader() { + return objectReader; + } + @Override public Iterator<RevCommit> iterator() { return sortedCommits.iterator(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndex.java index b0df46732e..a15602a168 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndex.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndex.java @@ -56,7 +56,7 @@ class GraphObjectIndex { long uint32; for (int k = 0; k < table.length; k++) { uint32 = NB.decodeUInt32(oidFanout, k * 4); - if (table[k] > Integer.MAX_VALUE) { + if (uint32 > Integer.MAX_VALUE) { throw new CommitGraphFormatException( JGitText.get().commitGraphFileIsTooLargeForJgit); } @@ -80,7 +80,7 @@ class GraphObjectIndex { if (levelOne > 0) { low = fanoutTable[levelOne - 1]; } - do { + while (low < high) { int mid = (low + high) >>> 1; int pos = objIdOffset(mid); int cmp = id.compareTo(oidLookup, pos); @@ -91,7 +91,7 @@ class GraphObjectIndex { } else { low = mid + 1; } - } while (low < high); + } return -1; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStats.java new file mode 100644 index 0000000000..295b702fa7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStats.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2024, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats; + +import java.util.List; + +import org.eclipse.jgit.internal.storage.pack.PackExt; + +/** + * Aggregates values for all given {@link BlockCacheStats}. + */ +class AggregatedBlockCacheStats implements BlockCacheStats { + private final List<BlockCacheStats> blockCacheStats; + + static BlockCacheStats fromStatsList( + List<BlockCacheStats> blockCacheStats) { + if (blockCacheStats.size() == 1) { + return blockCacheStats.get(0); + } + return new AggregatedBlockCacheStats(blockCacheStats); + } + + private AggregatedBlockCacheStats(List<BlockCacheStats> blockCacheStats) { + this.blockCacheStats = blockCacheStats; + } + + @Override + public String getName() { + return AggregatedBlockCacheStats.class.getName(); + } + + @Override + public long[] getCurrentSize() { + long[] sums = emptyPackStats(); + for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) { + sums = add(sums, blockCacheStatsEntry.getCurrentSize()); + } + return sums; + } + + @Override + public long[] getHitCount() { + long[] sums = emptyPackStats(); + for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) { + sums = add(sums, blockCacheStatsEntry.getHitCount()); + } + return sums; + } + + @Override + public long[] getMissCount() { + long[] sums = emptyPackStats(); + for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) { + sums = add(sums, blockCacheStatsEntry.getMissCount()); + } + return sums; + } + + @Override + public long[] getTotalRequestCount() { + long[] sums = emptyPackStats(); + for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) { + sums = add(sums, blockCacheStatsEntry.getTotalRequestCount()); + } + return sums; + } + + @Override + public long[] getHitRatio() { + long[] hit = getHitCount(); + long[] miss = getMissCount(); + long[] ratio = new long[Math.max(hit.length, miss.length)]; + for (int i = 0; i < ratio.length; i++) { + if (i >= hit.length) { + ratio[i] = 0; + } else if (i >= miss.length) { + ratio[i] = 100; + } else { + long total = hit[i] + miss[i]; + ratio[i] = total == 0 ? 0 : hit[i] * 100 / total; + } + } + return ratio; + } + + @Override + public long[] getEvictions() { + long[] sums = emptyPackStats(); + for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) { + sums = add(sums, blockCacheStatsEntry.getEvictions()); + } + return sums; + } + + private static long[] emptyPackStats() { + return new long[PackExt.values().length]; + } + + private static long[] add(long[] first, long[] second) { + long[] sums = new long[Integer.max(first.length, second.length)]; + int i; + for (i = 0; i < Integer.min(first.length, second.length); i++) { + sums[i] = first[i] + second[i]; + } + for (int j = i; j < first.length; j++) { + sums[j] = first[i]; + } + for (int j = i; j < second.length; j++) { + sums[j] = second[i]; + } + return sums; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BeforeDfsPackIndexLoadedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BeforeDfsPackIndexLoadedEvent.java index cdfd3e9592..68fc296d3e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BeforeDfsPackIndexLoadedEvent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BeforeDfsPackIndexLoadedEvent.java @@ -41,13 +41,11 @@ public class BeforeDfsPackIndexLoadedEvent return pack; } - /** {@inheritDoc} */ @Override public Class<BeforeDfsPackIndexLoadedListener> getListenerType() { return BeforeDfsPackIndexLoadedListener.class; } - /** {@inheritDoc} */ @Override public void dispatch(BeforeDfsPackIndexLoadedListener listener) { listener.onBeforeDfsPackIndexLoaded(this); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BlockBasedFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BlockBasedFile.java index b1b09baf20..ab07475663 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BlockBasedFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BlockBasedFile.java @@ -100,7 +100,9 @@ abstract class BlockBasedFile { DfsBlock getOrLoadBlock(long pos, DfsReader ctx) throws IOException { try (LazyChannel c = new LazyChannel(ctx, desc, ext)) { - return cache.getOrLoad(this, pos, ctx, c); + DfsBlock block = cache.getOrLoad(this, pos, ctx, c); + ctx.emitBlockLoad(this, pos, block); + return block; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java new file mode 100644 index 0000000000..587d482583 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java @@ -0,0 +1,575 @@ +/* + * Copyright (c) 2024, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import java.util.stream.LongStream; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.ReadableChannelSupplier; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.Ref; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.RefLoader; +import org.eclipse.jgit.internal.storage.pack.PackExt; + +/** + * Default implementation of the {@link DfsBlockCacheTable}. + * <p> + * This cache implements a clock replacement algorithm, giving each block at + * least one chance to have been accessed during a sweep of the cache to save + * itself from eviction. The number of swipe chances is configurable per pack + * extension. + * <p> + * Entities created by the cache are held under hard references, preventing the + * Java VM from clearing anything. Blocks are discarded by the replacement + * algorithm when adding a new block would cause the cache to exceed its + * configured maximum size. + * <p> + * Whenever a cache miss occurs, loading is invoked by exactly one thread for + * the given <code>(DfsStreamKey,position)</code> key tuple. This is ensured by + * an array of locks, with the tuple hashed to a lock instance. + * <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. + */ +final class ClockBlockCacheTable implements DfsBlockCacheTable { + /** + * Table name. + */ + private final String name; + + /** Number of entries in {@link #table}. */ + private final int tableSize; + + /** Maximum number of bytes the cache should hold. */ + private final long maxBytes; + + /** + * Used to reserve space for blocks. + * <p> + * The value for blockSize must be a power of 2. + */ + private final int blockSize; + + private final Hash hash; + + /** Hash bucket directory; entries are chained below. */ + private final AtomicReferenceArray<HashEntry> table; + + /** + * Locks to prevent concurrent loads for same (PackFile,position) block. The + * number of locks is {@link DfsBlockCacheConfig#getConcurrencyLevel()} to + * cap the overall concurrent block loads. + */ + private final ReentrantLock[] loadLocks; + + /** + * A separate pool of locks per pack extension to prevent concurrent loads + * for same index or bitmap from PackFile. + */ + private final ReentrantLock[][] refLocks; + + /** Protects the clock and its related data. */ + private final ReentrantLock clockLock; + + /** Current position of the clock. */ + private Ref clockHand; + + private final DfsBlockCacheStats dfsBlockCacheStats; + + /** + * A consumer of object reference lock wait time milliseconds. May be used + * to build a metric. + */ + private final Consumer<Long> refLockWaitTime; + + /** Consumer of loading and eviction events of indexes. */ + private final DfsBlockCacheConfig.IndexEventConsumer indexEventConsumer; + + /** Stores timestamps of the last eviction of indexes. */ + private final Map<EvictKey, Long> indexEvictionMap = new ConcurrentHashMap<>(); + + ClockBlockCacheTable(DfsBlockCacheConfig cfg) { + this.tableSize = tableSize(cfg); + if (tableSize < 1) { + throw new IllegalArgumentException( + JGitText.get().tSizeMustBeGreaterOrEqual1); + } + int concurrencyLevel = cfg.getConcurrencyLevel(); + this.maxBytes = cfg.getBlockLimit(); + this.blockSize = cfg.getBlockSize(); + int blockSizeShift = Integer.numberOfTrailingZeros(blockSize); + this.hash = new Hash(blockSizeShift); + table = new AtomicReferenceArray<>(tableSize); + + loadLocks = new ReentrantLock[concurrencyLevel]; + for (int i = 0; i < loadLocks.length; i++) { + loadLocks[i] = new ReentrantLock(/* fair= */ true); + } + refLocks = new ReentrantLock[PackExt.values().length][concurrencyLevel]; + for (int i = 0; i < PackExt.values().length; i++) { + for (int j = 0; j < concurrencyLevel; ++j) { + refLocks[i][j] = new ReentrantLock(/* fair= */ true); + } + } + + clockLock = new ReentrantLock(/* fair= */ true); + String none = ""; //$NON-NLS-1$ + clockHand = new Ref<>( + DfsStreamKey.of(new DfsRepositoryDescription(none), none, null), + -1, 0, null); + clockHand.next = clockHand; + + this.name = cfg.getName(); + this.dfsBlockCacheStats = new DfsBlockCacheStats(this.name); + this.refLockWaitTime = cfg.getRefLockWaitTimeConsumer(); + this.indexEventConsumer = cfg.getIndexEventConsumer(); + } + + @Override + public List<BlockCacheStats> getBlockCacheStats() { + return List.of(dfsBlockCacheStats); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean hasBlock0(DfsStreamKey key) { + HashEntry e1 = table.get(slot(key, 0)); + DfsBlock v = scan(e1, key, 0); + return v != null && v.contains(key, 0); + } + + @Override + public DfsBlock getOrLoad(BlockBasedFile file, long position, DfsReader ctx, + ReadableChannelSupplier fileChannel) throws IOException { + final long requestedPosition = position; + position = file.alignToBlock(position); + + DfsStreamKey key = file.key; + int slot = slot(key, position); + HashEntry e1 = table.get(slot); + DfsBlock v = scan(e1, key, position); + if (v != null && v.contains(key, requestedPosition)) { + ctx.stats.blockCacheHit++; + dfsBlockCacheStats.incrementHit(key); + return v; + } + + reserveSpace(blockSize, key); + ReentrantLock regionLock = lockFor(key, position); + regionLock.lock(); + try { + HashEntry e2 = table.get(slot); + if (e2 != e1) { + v = scan(e2, key, position); + if (v != null) { + ctx.stats.blockCacheHit++; + dfsBlockCacheStats.incrementHit(key); + creditSpace(blockSize, key); + return v; + } + } + + dfsBlockCacheStats.incrementMiss(key); + boolean credit = true; + try { + v = file.readOneBlock(position, ctx, fileChannel.get()); + credit = false; + } finally { + if (credit) { + creditSpace(blockSize, key); + } + } + if (position != v.start) { + // The file discovered its blockSize and adjusted. + position = v.start; + slot = slot(key, position); + e2 = table.get(slot); + } + + Ref<DfsBlock> ref = new Ref<>(key, position, v.size(), v); + ref.markHotter(); + for (;;) { + HashEntry n = new HashEntry(HashEntry.clean(e2), ref); + if (table.compareAndSet(slot, e2, n)) { + break; + } + e2 = table.get(slot); + } + addToClock(ref, blockSize - v.size()); + } finally { + regionLock.unlock(); + } + + // If the block size changed from the default, it is possible the + // block + // that was loaded is the wrong block for the requested position. + if (v.contains(file.key, requestedPosition)) { + return v; + } + return getOrLoad(file, requestedPosition, ctx, fileChannel); + } + + @Override + public <T> Ref<T> getOrLoadRef(DfsStreamKey key, long position, + RefLoader<T> loader) throws IOException { + long start = System.nanoTime(); + int slot = slot(key, position); + HashEntry e1 = table.get(slot); + Ref<T> ref = scanRef(e1, key, position); + if (ref != null) { + dfsBlockCacheStats.incrementHit(key); + reportIndexRequested(ref, true /* cacheHit= */, start); + return ref; + } + + ReentrantLock regionLock = lockForRef(key); + long lockStart = System.currentTimeMillis(); + regionLock.lock(); + try { + HashEntry e2 = table.get(slot); + if (e2 != e1) { + ref = scanRef(e2, key, position); + if (ref != null) { + dfsBlockCacheStats.incrementHit(key); + reportIndexRequested(ref, true /* cacheHit= */, start); + return ref; + } + } + + if (refLockWaitTime != null) { + refLockWaitTime.accept( + Long.valueOf(System.currentTimeMillis() - lockStart)); + } + dfsBlockCacheStats.incrementMiss(key); + ref = loader.load(); + ref.markHotter(); + // Reserve after loading to get the size of the object + reserveSpace(ref.size, key); + for (;;) { + HashEntry n = new HashEntry(HashEntry.clean(e2), ref); + if (table.compareAndSet(slot, e2, n)) { + break; + } + e2 = table.get(slot); + } + addToClock(ref, 0); + } finally { + regionLock.unlock(); + } + reportIndexRequested(ref, /* cacheHit= */ false, start); + return ref; + } + + @Override + public void put(DfsBlock v) { + put(v.stream, v.start, v.size(), v); + } + + @Override + public <T> Ref<T> put(DfsStreamKey key, long pos, long size, T v) { + int slot = slot(key, pos); + HashEntry e1 = table.get(slot); + Ref<T> ref = scanRef(e1, key, pos); + if (ref != null) { + return ref; + } + + reserveSpace(size, key); + ReentrantLock regionLock = lockFor(key, pos); + regionLock.lock(); + try { + HashEntry e2 = table.get(slot); + if (e2 != e1) { + ref = scanRef(e2, key, pos); + if (ref != null) { + creditSpace(size, key); + return ref; + } + } + + ref = new Ref<>(key, pos, size, v); + ref.markHotter(); + for (;;) { + HashEntry n = new HashEntry(HashEntry.clean(e2), ref); + if (table.compareAndSet(slot, e2, n)) { + break; + } + e2 = table.get(slot); + } + addToClock(ref, 0); + } finally { + regionLock.unlock(); + } + return ref; + } + + @Override + public <T> Ref<T> putRef(DfsStreamKey key, long size, T v) { + return put(key, 0, size, v); + } + + @Override + public boolean contains(DfsStreamKey key, long position) { + return scan(table.get(slot(key, position)), key, position) != null; + } + + @SuppressWarnings("unchecked") + @Override + public <T> T get(DfsStreamKey key, long position) { + T val = (T) scan(table.get(slot(key, position)), key, position); + if (val == null) { + dfsBlockCacheStats.incrementMiss(key); + } else { + dfsBlockCacheStats.incrementHit(key); + } + return val; + } + + private int slot(DfsStreamKey key, long position) { + return (hash.hash(key.hash, position) >>> 1) % tableSize; + } + + @SuppressWarnings("unchecked") + private void reserveSpace(long reserve, DfsStreamKey key) { + clockLock.lock(); + try { + long live = LongStream.of(dfsBlockCacheStats.getCurrentSize()).sum() + + reserve; + if (maxBytes < live) { + Ref prev = clockHand; + Ref hand = clockHand.next; + do { + if (hand.isHot()) { + // Value was recently touched. Cache is still hot so + // give it another chance, but cool it down a bit. + hand.markColder(); + prev = hand; + hand = hand.next; + continue; + } else if (prev == hand) { + break; + } + + // No recent access since last scan, kill + // value and remove from clock. + Ref dead = hand; + hand = hand.next; + prev.next = hand; + dead.next = null; + dead.value = null; + live -= dead.size; + dfsBlockCacheStats.addToLiveBytes(dead.key, -dead.size); + dfsBlockCacheStats.incrementEvict(dead.key); + reportIndexEvicted(dead); + } while (maxBytes < live); + clockHand = prev; + } + dfsBlockCacheStats.addToLiveBytes(key, reserve); + } finally { + clockLock.unlock(); + } + } + + private void creditSpace(long credit, DfsStreamKey key) { + clockLock.lock(); + try { + dfsBlockCacheStats.addToLiveBytes(key, -credit); + } finally { + clockLock.unlock(); + } + } + + @SuppressWarnings("unchecked") + private void addToClock(Ref ref, long credit) { + clockLock.lock(); + try { + if (credit != 0) { + dfsBlockCacheStats.addToLiveBytes(ref.key, -credit); + } + Ref ptr = clockHand; + ref.next = ptr.next; + ptr.next = ref; + clockHand = ref; + } finally { + clockLock.unlock(); + } + } + + private <T> T scan(HashEntry n, DfsStreamKey key, long position) { + Ref<T> r = scanRef(n, key, position); + return r != null ? r.get() : null; + } + + @SuppressWarnings("unchecked") + private <T> Ref<T> scanRef(HashEntry n, DfsStreamKey key, long position) { + for (; n != null; n = n.next) { + Ref<T> r = n.ref; + if (r.position == position && r.key.equals(key)) { + return r.get() != null ? r : null; + } + } + return null; + } + + private ReentrantLock lockFor(DfsStreamKey key, long position) { + return loadLocks[(hash.hash(key.hash, position) >>> 1) + % loadLocks.length]; + } + + private ReentrantLock lockForRef(DfsStreamKey key) { + int slot = (key.hash >>> 1) % refLocks[key.packExtPos].length; + return refLocks[key.packExtPos][slot]; + } + + private void reportIndexRequested(Ref<?> ref, boolean cacheHit, + long start) { + if (indexEventConsumer == null || !isIndexExtPos(ref.key.packExtPos)) { + return; + } + EvictKey evictKey = createEvictKey(ref); + Long prevEvictedTime = indexEvictionMap.get(evictKey); + long now = System.nanoTime(); + long sinceLastEvictionNanos = prevEvictedTime == null ? 0L + : now - prevEvictedTime.longValue(); + indexEventConsumer.acceptRequestedEvent(ref.key.packExtPos, cacheHit, + (now - start) / 1000L /* micros */, ref.size, + Duration.ofNanos(sinceLastEvictionNanos)); + } + + private void reportIndexEvicted(Ref<?> dead) { + if (indexEventConsumer == null + || !indexEventConsumer.shouldReportEvictedEvent() + || !isIndexExtPos(dead.key.packExtPos)) { + return; + } + EvictKey evictKey = createEvictKey(dead); + Long prevEvictedTime = indexEvictionMap.get(evictKey); + long now = System.nanoTime(); + long sinceLastEvictionNanos = prevEvictedTime == null ? 0L + : now - prevEvictedTime.longValue(); + indexEvictionMap.put(evictKey, Long.valueOf(now)); + indexEventConsumer.acceptEvictedEvent(dead.key.packExtPos, dead.size, + dead.getTotalHitCount(), + Duration.ofNanos(sinceLastEvictionNanos)); + } + + private static final class HashEntry { + /** Next entry in the hash table's chain list. */ + final HashEntry next; + + /** The referenced object. */ + final Ref ref; + + HashEntry(HashEntry n, Ref r) { + next = n; + ref = r; + } + + private static HashEntry clean(HashEntry top) { + while (top != null && top.ref.next == null) { + top = top.next; + } + if (top == null) { + return null; + } + HashEntry n = clean(top.next); + return n == top.next ? top : new HashEntry(n, top.ref); + } + } + + private EvictKey createEvictKey(Ref<?> ref) { + return new EvictKey(hash, ref); + } + + private static boolean isIndexExtPos(int packExtPos) { + return packExtPos == PackExt.INDEX.getPosition() + || packExtPos == PackExt.REVERSE_INDEX.getPosition() + || packExtPos == PackExt.BITMAP_INDEX.getPosition(); + } + + private static int tableSize(DfsBlockCacheConfig cfg) { + final int wsz = cfg.getBlockSize(); + final long limit = cfg.getBlockLimit(); + if (wsz <= 0) { + throw new IllegalArgumentException( + JGitText.get().invalidWindowSize); + } + if (limit < wsz) { + throw new IllegalArgumentException( + JGitText.get().windowSizeMustBeLesserThanLimit); + } + return (int) Math.min(5 * (limit / wsz) / 2, Integer.MAX_VALUE); + } + + private static final class Hash { + /** + * As {@link #blockSize} is a power of 2, bits to shift for a / + * blockSize. + */ + private final int blockSizeShift; + + Hash(int blockSizeShift) { + this.blockSizeShift = blockSizeShift; + } + + int hash(int packHash, long off) { + return packHash + (int) (off >>> blockSizeShift); + } + } + + private static final class EvictKey { + /** + * Provides the hash function to be used for this key's hashCode method. + */ + private final Hash hash; + + private final int keyHash; + + private final int packExtPos; + + private final long position; + + EvictKey(Hash hash, Ref<?> ref) { + this.hash = hash; + keyHash = ref.key.hash; + packExtPos = ref.key.packExtPos; + position = ref.position; + } + + @Override + public boolean equals(Object object) { + if (object instanceof EvictKey) { + EvictKey other = (EvictKey) object; + return keyHash == other.keyHash + && packExtPos == other.packExtPos + && position == other.position; + } + return false; + } + + @Override + public int hashCode() { + return hash.hash(keyHash, position); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java index 0a02180d7d..f8e0831e1f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java @@ -11,19 +11,13 @@ package org.eclipse.jgit.internal.storage.dfs; +import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats; + import java.io.IOException; -import java.time.Duration; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.atomic.AtomicReferenceArray; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Consumer; import java.util.stream.LongStream; -import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.PackExt; /** @@ -36,31 +30,14 @@ import org.eclipse.jgit.internal.storage.pack.PackExt; * reads of only tiny slices of a file, the DfsBlockCache tries to smooth out * these tiny reads into larger block-sized IO operations. * <p> - * Whenever a cache miss occurs, loading is invoked by exactly one thread for - * the given <code>(DfsStreamKey,position)</code> key tuple. This is ensured by - * an array of locks, with the tuple hashed to a lock instance. - * <p> * Its too expensive during object access to be 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. This - * cache implements a clock replacement algorithm, giving each block at least - * one chance to have been accessed during a sweep of the cache to save itself - * from eviction. The number of swipe chances is configurable per pack - * extension. - * <p> - * Entities created by the cache are held under hard references, preventing the - * Java VM from clearing anything. Blocks are discarded by the replacement - * algorithm when adding a new block would cause the cache to exceed its - * configured maximum size. + * typically doesn't yield a corresponding benefit to the application. * <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> - * 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. */ public final class DfsBlockCache { private static volatile DfsBlockCache cache; @@ -94,24 +71,7 @@ public final class DfsBlockCache { return cache; } - /** Number of entries in {@link #table}. */ - private final int tableSize; - - /** Hash bucket directory; entries are chained below. */ - private final AtomicReferenceArray<HashEntry> table; - - /** - * Locks to prevent concurrent loads for same (PackFile,position) block. The - * number of locks is {@link DfsBlockCacheConfig#getConcurrencyLevel()} to - * cap the overall concurrent block loads. - */ - private final ReentrantLock[] loadLocks; - - /** - * A separate pool of locks per pack extension to prevent concurrent loads - * for same index or bitmap from PackFile. - */ - private final ReentrantLock[][] refLocks; + private final DfsBlockCacheTable dfsBlockCacheTable; /** Maximum number of bytes the cache should hold. */ private final long maxBytes; @@ -131,89 +91,21 @@ public final class DfsBlockCache { */ private final int blockSize; - /** As {@link #blockSize} is a power of 2, bits to shift for a / blockSize. */ - private final int blockSizeShift; - - /** - * Number of times a block was found in the cache, per pack file extension. - */ - private final AtomicReference<AtomicLong[]> statHit; - - /** - * Number of times a block was not found, and had to be loaded, per pack - * file extension. - */ - private final AtomicReference<AtomicLong[]> statMiss; - - /** - * Number of blocks evicted due to cache being full, per pack file - * extension. - */ - private final AtomicReference<AtomicLong[]> statEvict; - - /** - * Number of bytes currently loaded in the cache, per pack file extension. - */ - private final AtomicReference<AtomicLong[]> liveBytes; - - /** Protects the clock and its related data. */ - private final ReentrantLock clockLock; - - /** - * A consumer of object reference lock wait time milliseconds. May be used to build a metric. - */ - private final Consumer<Long> refLockWaitTime; - - /** Current position of the clock. */ - private Ref clockHand; - /** Limits of cache hot count per pack file extension. */ private final int[] cacheHotLimits = new int[PackExt.values().length]; - /** Consumer of loading and eviction events of indexes. */ - private final DfsBlockCacheConfig.IndexEventConsumer indexEventConsumer; - - /** Stores timestamps of the last eviction of indexes. */ - private final Map<EvictKey, Long> indexEvictionMap = new ConcurrentHashMap<>(); - - @SuppressWarnings("unchecked") private DfsBlockCache(DfsBlockCacheConfig cfg) { - tableSize = tableSize(cfg); - if (tableSize < 1) { - throw new IllegalArgumentException(JGitText.get().tSizeMustBeGreaterOrEqual1); - } - - table = new AtomicReferenceArray<>(tableSize); - int concurrencyLevel = cfg.getConcurrencyLevel(); - loadLocks = new ReentrantLock[concurrencyLevel]; - for (int i = 0; i < loadLocks.length; i++) { - loadLocks[i] = new ReentrantLock(true /* fair */); - } - refLocks = new ReentrantLock[PackExt.values().length][concurrencyLevel]; - for (int i = 0; i < PackExt.values().length; i++) { - for (int j = 0; j < concurrencyLevel; ++j) { - refLocks[i][j] = new ReentrantLock(true /* fair */); - } - } - maxBytes = cfg.getBlockLimit(); - maxStreamThroughCache = (long) (maxBytes * cfg.getStreamRatio()); blockSize = cfg.getBlockSize(); - blockSizeShift = Integer.numberOfTrailingZeros(blockSize); + double streamRatio = cfg.getStreamRatio(); + maxStreamThroughCache = (long) (maxBytes * streamRatio); - clockLock = new ReentrantLock(true /* fair */); - String none = ""; //$NON-NLS-1$ - clockHand = new Ref<>( - DfsStreamKey.of(new DfsRepositoryDescription(none), none, null), - -1, 0, null); - clockHand.next = clockHand; - - statHit = new AtomicReference<>(newCounters()); - statMiss = new AtomicReference<>(newCounters()); - statEvict = new AtomicReference<>(newCounters()); - liveBytes = new AtomicReference<>(newCounters()); - - refLockWaitTime = cfg.getRefLockWaitTimeConsumer(); + if (!cfg.getPackExtCacheConfigurations().isEmpty()) { + dfsBlockCacheTable = PackExtBlockCacheTable + .fromBlockCacheConfigs(cfg); + } else { + dfsBlockCacheTable = new ClockBlockCacheTable(cfg); + } for (int i = 0; i < PackExt.values().length; ++i) { Integer limit = cfg.getCacheHotMap().get(PackExt.values()[i]); @@ -223,7 +115,6 @@ public final class DfsBlockCache { cacheHotLimits[i] = DfsBlockCacheConfig.DEFAULT_CACHE_HOT_MAX; } } - indexEventConsumer = cfg.getIndexEventConsumer(); } boolean shouldCopyThroughCache(long length) { @@ -236,7 +127,7 @@ public final class DfsBlockCache { * @return total number of bytes in the cache, per pack file extension. */ public long[] getCurrentSize() { - return getStatVals(liveBytes); + return getAggregatedBlockCacheStats().getCurrentSize(); } /** @@ -255,7 +146,7 @@ public final class DfsBlockCache { * extension. */ public long[] getHitCount() { - return getStatVals(statHit); + return getAggregatedBlockCacheStats().getHitCount(); } /** @@ -266,7 +157,7 @@ public final class DfsBlockCache { * extension. */ public long[] getMissCount() { - return getStatVals(statMiss); + return getAggregatedBlockCacheStats().getMissCount(); } /** @@ -275,16 +166,7 @@ public final class DfsBlockCache { * @return total number of requests (hit + miss), per pack file extension. */ public long[] getTotalRequestCount() { - AtomicLong[] hit = statHit.get(); - AtomicLong[] miss = statMiss.get(); - long[] cnt = new long[Math.max(hit.length, miss.length)]; - for (int i = 0; i < hit.length; i++) { - cnt[i] += hit[i].get(); - } - for (int i = 0; i < miss.length; i++) { - cnt[i] += miss[i].get(); - } - return cnt; + return getAggregatedBlockCacheStats().getTotalRequestCount(); } /** @@ -293,22 +175,7 @@ public final class DfsBlockCache { * @return hit ratios */ public long[] getHitRatio() { - AtomicLong[] hit = statHit.get(); - AtomicLong[] miss = statMiss.get(); - long[] ratio = new long[Math.max(hit.length, miss.length)]; - for (int i = 0; i < ratio.length; i++) { - if (i >= hit.length) { - ratio[i] = 0; - } else if (i >= miss.length) { - ratio[i] = 100; - } else { - long hitVal = hit[i].get(); - long missVal = miss[i].get(); - long total = hitVal + missVal; - ratio[i] = total == 0 ? 0 : hitVal * 100 / total; - } - } - return ratio; + return getAggregatedBlockCacheStats().getHitRatio(); } /** @@ -319,7 +186,18 @@ public final class DfsBlockCache { * file extension. */ public long[] getEvictions() { - return getStatVals(statEvict); + return getAggregatedBlockCacheStats().getEvictions(); + } + + /** + * Get the list of {@link BlockCacheStats} for all underlying caches. + * <p> + * Useful in monitoring caches with breakdown. + * + * @return the list of {@link BlockCacheStats} for all underlying caches. + */ + public List<BlockCacheStats> getAllBlockCacheStats() { + return dfsBlockCacheTable.getBlockCacheStats(); } /** @@ -334,31 +212,13 @@ public final class DfsBlockCache { * @return true if block 0 (the first block) is in the cache. */ public boolean hasBlock0(DfsStreamKey key) { - HashEntry e1 = table.get(slot(key, 0)); - DfsBlock v = scan(e1, key, 0); - return v != null && v.contains(key, 0); - } - - private int hash(int packHash, long off) { - return packHash + (int) (off >>> blockSizeShift); + return dfsBlockCacheTable.hasBlock0(key); } int getBlockSize() { return blockSize; } - private static int tableSize(DfsBlockCacheConfig cfg) { - final int wsz = cfg.getBlockSize(); - final long limit = cfg.getBlockLimit(); - if (wsz <= 0) { - throw new IllegalArgumentException(JGitText.get().invalidWindowSize); - } - if (limit < wsz) { - throw new IllegalArgumentException(JGitText.get().windowSizeMustBeLesserThanLimit); - } - return (int) Math.min(5 * (limit / wsz) / 2, Integer.MAX_VALUE); - } - /** * Look up a cached object, creating and loading it if it doesn't exist. * @@ -376,139 +236,11 @@ public final class DfsBlockCache { */ DfsBlock getOrLoad(BlockBasedFile file, long position, DfsReader ctx, ReadableChannelSupplier fileChannel) throws IOException { - final long requestedPosition = position; - position = file.alignToBlock(position); - - DfsStreamKey key = file.key; - int slot = slot(key, position); - HashEntry e1 = table.get(slot); - DfsBlock v = scan(e1, key, position); - if (v != null && v.contains(key, requestedPosition)) { - ctx.stats.blockCacheHit++; - getStat(statHit, key).incrementAndGet(); - return v; - } - - reserveSpace(blockSize, key); - ReentrantLock regionLock = lockFor(key, position); - regionLock.lock(); - try { - HashEntry e2 = table.get(slot); - if (e2 != e1) { - v = scan(e2, key, position); - if (v != null) { - ctx.stats.blockCacheHit++; - getStat(statHit, key).incrementAndGet(); - creditSpace(blockSize, key); - return v; - } - } - - getStat(statMiss, key).incrementAndGet(); - boolean credit = true; - try { - v = file.readOneBlock(position, ctx, fileChannel.get()); - credit = false; - } finally { - if (credit) { - creditSpace(blockSize, key); - } - } - if (position != v.start) { - // The file discovered its blockSize and adjusted. - position = v.start; - slot = slot(key, position); - e2 = table.get(slot); - } - - Ref<DfsBlock> ref = new Ref<>(key, position, v.size(), v); - ref.markHotter(); - for (;;) { - HashEntry n = new HashEntry(clean(e2), ref); - if (table.compareAndSet(slot, e2, n)) { - break; - } - e2 = table.get(slot); - } - addToClock(ref, blockSize - v.size()); - } finally { - regionLock.unlock(); - } - - // If the block size changed from the default, it is possible the block - // that was loaded is the wrong block for the requested position. - if (v.contains(file.key, requestedPosition)) { - return v; - } - return getOrLoad(file, requestedPosition, ctx, fileChannel); - } - - @SuppressWarnings("unchecked") - private void reserveSpace(long reserve, DfsStreamKey key) { - clockLock.lock(); - try { - long live = LongStream.of(getCurrentSize()).sum() + reserve; - if (maxBytes < live) { - Ref prev = clockHand; - Ref hand = clockHand.next; - do { - if (hand.isHot()) { - // Value was recently touched. Cache is still hot so - // give it another chance, but cool it down a bit. - hand.markColder(); - prev = hand; - hand = hand.next; - continue; - } else if (prev == hand) - break; - - // No recent access since last scan, kill - // value and remove from clock. - Ref dead = hand; - hand = hand.next; - prev.next = hand; - dead.next = null; - dead.value = null; - live -= dead.size; - getStat(liveBytes, dead.key).addAndGet(-dead.size); - getStat(statEvict, dead.key).incrementAndGet(); - reportIndexEvicted(dead); - } while (maxBytes < live); - clockHand = prev; - } - getStat(liveBytes, key).addAndGet(reserve); - } finally { - clockLock.unlock(); - } - } - - private void creditSpace(long credit, DfsStreamKey key) { - clockLock.lock(); - try { - getStat(liveBytes, key).addAndGet(-credit); - } finally { - clockLock.unlock(); - } - } - - @SuppressWarnings("unchecked") - private void addToClock(Ref ref, long credit) { - clockLock.lock(); - try { - if (credit != 0) { - getStat(liveBytes, ref.key).addAndGet(-credit); - } - Ref ptr = clockHand; - ref.next = ptr.next; - ptr.next = ref; - clockHand = ref; - } finally { - clockLock.unlock(); - } + return dfsBlockCacheTable.getOrLoad(file, position, ctx, fileChannel); } void put(DfsBlock v) { - put(v.stream, v.start, v.size(), v); + dfsBlockCacheTable.put(v); } /** @@ -524,252 +256,46 @@ public final class DfsBlockCache { * @throws IOException * the reference was not in the cache and could not be loaded. */ - <T> Ref<T> getOrLoadRef( - DfsStreamKey key, long position, RefLoader<T> loader) - throws IOException { - long start = System.nanoTime(); - int slot = slot(key, position); - HashEntry e1 = table.get(slot); - Ref<T> ref = scanRef(e1, key, position); - if (ref != null) { - getStat(statHit, key).incrementAndGet(); - reportIndexRequested(ref, true /* cacheHit */, start); - return ref; - } - - ReentrantLock regionLock = lockForRef(key); - long lockStart = System.currentTimeMillis(); - regionLock.lock(); - try { - HashEntry e2 = table.get(slot); - if (e2 != e1) { - ref = scanRef(e2, key, position); - if (ref != null) { - getStat(statHit, key).incrementAndGet(); - reportIndexRequested(ref, true /* cacheHit */, - start); - return ref; - } - } - - if (refLockWaitTime != null) { - refLockWaitTime.accept( - Long.valueOf(System.currentTimeMillis() - lockStart)); - } - getStat(statMiss, key).incrementAndGet(); - ref = loader.load(); - ref.markHotter(); - // Reserve after loading to get the size of the object - reserveSpace(ref.size, key); - for (;;) { - HashEntry n = new HashEntry(clean(e2), ref); - if (table.compareAndSet(slot, e2, n)) { - break; - } - e2 = table.get(slot); - } - addToClock(ref, 0); - } finally { - regionLock.unlock(); - } - reportIndexRequested(ref, false /* cacheHit */, start); - return ref; + <T> Ref<T> getOrLoadRef(DfsStreamKey key, long position, + RefLoader<T> loader) throws IOException { + return dfsBlockCacheTable.getOrLoadRef(key, position, loader); } <T> Ref<T> putRef(DfsStreamKey key, long size, T v) { - return put(key, 0, size, v); + return dfsBlockCacheTable.putRef(key, size, v); } <T> Ref<T> put(DfsStreamKey key, long pos, long size, T v) { - int slot = slot(key, pos); - HashEntry e1 = table.get(slot); - Ref<T> ref = scanRef(e1, key, pos); - if (ref != null) { - return ref; - } - - reserveSpace(size, key); - ReentrantLock regionLock = lockFor(key, pos); - regionLock.lock(); - try { - HashEntry e2 = table.get(slot); - if (e2 != e1) { - ref = scanRef(e2, key, pos); - if (ref != null) { - creditSpace(size, key); - return ref; - } - } - - ref = new Ref<>(key, pos, size, v); - ref.markHotter(); - for (;;) { - HashEntry n = new HashEntry(clean(e2), ref); - if (table.compareAndSet(slot, e2, n)) { - break; - } - e2 = table.get(slot); - } - addToClock(ref, 0); - } finally { - regionLock.unlock(); - } - return ref; + return dfsBlockCacheTable.put(key, pos, size, v); } boolean contains(DfsStreamKey key, long position) { - return scan(table.get(slot(key, position)), key, position) != null; + return dfsBlockCacheTable.contains(key, position); } - @SuppressWarnings("unchecked") <T> T get(DfsStreamKey key, long position) { - T val = (T) scan(table.get(slot(key, position)), key, position); - if (val == null) { - getStat(statMiss, key).incrementAndGet(); - } else { - getStat(statHit, key).incrementAndGet(); - } - return val; - } - - private <T> T scan(HashEntry n, DfsStreamKey key, long position) { - Ref<T> r = scanRef(n, key, position); - return r != null ? r.get() : null; - } - - @SuppressWarnings("unchecked") - private <T> Ref<T> scanRef(HashEntry n, DfsStreamKey key, long position) { - for (; n != null; n = n.next) { - Ref<T> r = n.ref; - if (r.position == position && r.key.equals(key)) { - return r.get() != null ? r : null; - } - } - return null; - } - - private int slot(DfsStreamKey key, long position) { - return (hash(key.hash, position) >>> 1) % tableSize; - } - - private ReentrantLock lockFor(DfsStreamKey key, long position) { - return loadLocks[(hash(key.hash, position) >>> 1) % loadLocks.length]; - } - - private ReentrantLock lockForRef(DfsStreamKey key) { - int slot = (key.hash >>> 1) % refLocks[key.packExtPos].length; - return refLocks[key.packExtPos][slot]; - } - - private static AtomicLong[] newCounters() { - AtomicLong[] ret = new AtomicLong[PackExt.values().length]; - for (int i = 0; i < ret.length; i++) { - ret[i] = new AtomicLong(); - } - return ret; - } - - private static AtomicLong getStat(AtomicReference<AtomicLong[]> stats, - DfsStreamKey key) { - int pos = key.packExtPos; - while (true) { - AtomicLong[] vals = stats.get(); - if (pos < vals.length) { - return vals[pos]; - } - AtomicLong[] expect = vals; - vals = new AtomicLong[Math.max(pos + 1, PackExt.values().length)]; - System.arraycopy(expect, 0, vals, 0, expect.length); - for (int i = expect.length; i < vals.length; i++) { - vals[i] = new AtomicLong(); - } - if (stats.compareAndSet(expect, vals)) { - return vals[pos]; - } - } + return dfsBlockCacheTable.get(key, position); } - private static long[] getStatVals(AtomicReference<AtomicLong[]> stat) { - AtomicLong[] stats = stat.get(); - long[] cnt = new long[stats.length]; - for (int i = 0; i < stats.length; i++) { - cnt[i] = stats[i].get(); - } - return cnt; - } - - private static HashEntry clean(HashEntry top) { - while (top != null && top.ref.next == null) { - top = top.next; - } - if (top == null) { - return null; - } - HashEntry n = clean(top.next); - return n == top.next ? top : new HashEntry(n, top.ref); - } - - private void reportIndexRequested(Ref<?> ref, boolean cacheHit, - long start) { - if (indexEventConsumer == null - || !isIndexExtPos(ref.key.packExtPos)) { - return; - } - EvictKey evictKey = new EvictKey(ref); - Long prevEvictedTime = indexEvictionMap.get(evictKey); - long now = System.nanoTime(); - long sinceLastEvictionNanos = prevEvictedTime == null ? 0L - : now - prevEvictedTime.longValue(); - indexEventConsumer.acceptRequestedEvent(ref.key.packExtPos, cacheHit, - (now - start) / 1000L /* micros */, ref.size, - Duration.ofNanos(sinceLastEvictionNanos)); - } - - private void reportIndexEvicted(Ref<?> dead) { - if (indexEventConsumer == null - || !indexEventConsumer.shouldReportEvictedEvent() - || !isIndexExtPos(dead.key.packExtPos)) { - return; - } - EvictKey evictKey = new EvictKey(dead); - Long prevEvictedTime = indexEvictionMap.get(evictKey); - long now = System.nanoTime(); - long sinceLastEvictionNanos = prevEvictedTime == null ? 0L - : now - prevEvictedTime.longValue(); - indexEvictionMap.put(evictKey, Long.valueOf(now)); - indexEventConsumer.acceptEvictedEvent(dead.key.packExtPos, dead.size, - dead.totalHitCount.get(), - Duration.ofNanos(sinceLastEvictionNanos)); - } - - private static boolean isIndexExtPos(int packExtPos) { - return packExtPos == PackExt.INDEX.getPosition() - || packExtPos == PackExt.REVERSE_INDEX.getPosition() - || packExtPos == PackExt.BITMAP_INDEX.getPosition(); - } - - private static final class HashEntry { - /** Next entry in the hash table's chain list. */ - final HashEntry next; - - /** The referenced object. */ - final Ref ref; - - HashEntry(HashEntry n, Ref r) { - next = n; - ref = r; - } + private BlockCacheStats getAggregatedBlockCacheStats() { + return AggregatedBlockCacheStats + .fromStatsList(dfsBlockCacheTable.getBlockCacheStats()); } static final class Ref<T> { final DfsStreamKey key; + final long position; + final long size; + volatile T value; + Ref next; private volatile int hotCount; - private AtomicInteger totalHitCount = new AtomicInteger(); + + private final AtomicInteger totalHitCount = new AtomicInteger(); Ref(DfsStreamKey key, long position, long size, T v) { this.key = key; @@ -804,33 +330,9 @@ public final class DfsBlockCache { boolean isHot() { return hotCount > 0; } - } - - private static final class EvictKey { - private final int keyHash; - private final int packExtPos; - private final long position; - - EvictKey(Ref<?> ref) { - keyHash = ref.key.hash; - packExtPos = ref.key.packExtPos; - position = ref.position; - } - - @Override - public boolean equals(Object object) { - if (object instanceof EvictKey) { - EvictKey other = (EvictKey) object; - return keyHash == other.keyHash - && packExtPos == other.packExtPos - && position == other.position; - } - return false; - } - @Override - public int hashCode() { - return DfsBlockCache.getInstance().hash(keyHash, position); + int getTotalHitCount() { + return totalHitCount.get(); } } @@ -845,8 +347,11 @@ public final class DfsBlockCache { @FunctionalInterface interface ReadableChannelSupplier { /** + * Get ReadableChannel + * * @return ReadableChannel * @throws IOException + * if an IO error occurred */ ReadableChannel get() throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java index 69a37058bf..17bf51876d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java @@ -11,17 +11,27 @@ package org.eclipse.jgit.internal.storage.dfs; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_CACHE_PREFIX; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_LIMIT; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_SIZE; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CONCURRENCY_LEVEL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACK_EXTENSIONS; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO; +import java.io.PrintWriter; import java.text.MessageFormat; import java.time.Duration; +import java.util.ArrayList; import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.PackExt; @@ -41,25 +51,103 @@ public class DfsBlockCacheConfig { /** Default number of max cache hits. */ public static final int DEFAULT_CACHE_HOT_MAX = 1; + static final String DEFAULT_NAME = "<default>"; //$NON-NLS-1$ + + private String name; + private long blockLimit; + private int blockSize; + private double streamRatio; + private int concurrencyLevel; private Consumer<Long> refLock; + private Map<PackExt, Integer> cacheHotMap; private IndexEventConsumer indexEventConsumer; + private List<DfsBlockCachePackExtConfig> packExtCacheConfigurations; + /** * Create a default configuration. */ public DfsBlockCacheConfig() { + name = DEFAULT_NAME; setBlockLimit(32 * MB); setBlockSize(64 * KB); setStreamRatio(0.30); setConcurrencyLevel(32); cacheHotMap = Collections.emptyMap(); + packExtCacheConfigurations = Collections.emptyList(); + } + + /** + * Print the current cache configuration to the given {@link PrintWriter}. + * + * @param writer + * {@link PrintWriter} to write the cache's configuration to. + */ + public void print(PrintWriter writer) { + print(/* linePrefix= */ "", /* pad= */ " ", writer); //$NON-NLS-1$//$NON-NLS-2$ + } + + /** + * Print the current cache configuration to the given {@link PrintWriter}. + * + * @param linePrefix + * prefix to prepend all writen lines with. Ex a string of 0 or + * more " " entries. + * @param pad + * filler used to extend linePrefix. Ex a multiple of " ". + * @param writer + * {@link PrintWriter} to write the cache's configuration to. + */ + @SuppressWarnings("nls") + private void print(String linePrefix, String pad, PrintWriter writer) { + String currentPrefixLevel = linePrefix; + if (!name.isEmpty() || !packExtCacheConfigurations.isEmpty()) { + writer.println(linePrefix + "Name: " + + (name.isEmpty() ? DEFAULT_NAME : this.name)); + currentPrefixLevel += pad; + } + writer.println(currentPrefixLevel + "BlockLimit: " + blockLimit); + writer.println(currentPrefixLevel + "BlockSize: " + blockSize); + writer.println(currentPrefixLevel + "StreamRatio: " + streamRatio); + writer.println( + currentPrefixLevel + "ConcurrencyLevel: " + concurrencyLevel); + for (Map.Entry<PackExt, Integer> entry : cacheHotMap.entrySet()) { + writer.println(currentPrefixLevel + "CacheHotMapEntry: " + + entry.getKey() + " : " + entry.getValue()); + } + for (DfsBlockCachePackExtConfig extConfig : packExtCacheConfigurations) { + extConfig.print(currentPrefixLevel, pad, writer); + } + } + + /** + * Get the name for the block cache configured by this cache config. + * + * @return the name for the block cache configured by this cache config. + */ + public String getName() { + return name; + } + + /** + * Set the name for the block cache configured by this cache config. + * <p> + * Made visible for testing. + * + * @param name + * the name for the block cache configured by this cache config. + * @return {@code this} + */ + DfsBlockCacheConfig setName(String name) { + this.name = name; + return this; } /** @@ -77,10 +165,10 @@ public class DfsBlockCacheConfig { * Set maximum number bytes of heap memory to dedicate to caching pack file * data. * <p> - * It is strongly recommended to set the block limit to be an integer multiple - * of the block size. This constraint is not enforced by this method (since - * it may be called before {@link #setBlockSize(int)}), but it is enforced by - * {@link #fromConfig(Config)}. + * It is strongly recommended to set the block limit to be an integer + * multiple of the block size. This constraint is not enforced by this + * method (since it may be called before {@link #setBlockSize(int)}), but it + * is enforced by {@link #fromConfig(Config)}. * * @param newLimit * maximum number bytes of heap memory to dedicate to caching @@ -89,9 +177,9 @@ public class DfsBlockCacheConfig { */ public DfsBlockCacheConfig setBlockLimit(long newLimit) { if (newLimit <= 0) { - throw new IllegalArgumentException(MessageFormat.format( - JGitText.get().blockLimitNotPositive, - Long.valueOf(newLimit))); + throw new IllegalArgumentException( + MessageFormat.format(JGitText.get().blockLimitNotPositive, + Long.valueOf(newLimit))); } blockLimit = newLimit; return this; @@ -211,12 +299,24 @@ public class DfsBlockCacheConfig { * map of hot count per pack extension for {@code DfsBlockCache}. * @return {@code this} */ + /* + * TODO The cache HotMap configuration should be set as a config option and + * not passed in through a setter. + */ public DfsBlockCacheConfig setCacheHotMap( Map<PackExt, Integer> cacheHotMap) { this.cacheHotMap = Collections.unmodifiableMap(cacheHotMap); + setCacheHotMapToPackExtConfigs(this.cacheHotMap); return this; } + private void setCacheHotMapToPackExtConfigs( + Map<PackExt, Integer> cacheHotMap) { + for (DfsBlockCachePackExtConfig packExtConfig : packExtCacheConfigurations) { + packExtConfig.setCacheHotMap(cacheHotMap); + } + } + /** * Get the consumer of cache index events. * @@ -240,61 +340,121 @@ public class DfsBlockCacheConfig { } /** + * Get the list of pack ext cache configs. + * + * @return the list of pack ext cache configs. + */ + List<DfsBlockCachePackExtConfig> getPackExtCacheConfigurations() { + return packExtCacheConfigurations; + } + + /** + * Set the list of pack ext cache configs. + * + * Made visible for testing. + * + * @param packExtCacheConfigurations + * the list of pack ext cache configs to set. + * @return {@code this} + */ + DfsBlockCacheConfig setPackExtCacheConfigurations( + List<DfsBlockCachePackExtConfig> packExtCacheConfigurations) { + this.packExtCacheConfigurations = packExtCacheConfigurations; + return this; + } + + /** * Update properties by setting fields from the configuration. * <p> * If a property is not defined in the configuration, then it is left * unmodified. * <p> - * Enforces certain constraints on the combination of settings in the config, - * for example that the block limit is a multiple of the block size. + * Enforces certain constraints on the combination of settings in the + * config, for example that the block limit is a multiple of the block size. * * @param rc * configuration to read properties from. * @return {@code this} */ public DfsBlockCacheConfig fromConfig(Config rc) { - long cfgBlockLimit = rc.getLong( - CONFIG_CORE_SECTION, - CONFIG_DFS_SECTION, - CONFIG_KEY_BLOCK_LIMIT, - getBlockLimit()); - int cfgBlockSize = rc.getInt( - CONFIG_CORE_SECTION, - CONFIG_DFS_SECTION, - CONFIG_KEY_BLOCK_SIZE, + fromConfig(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, rc); + loadPackExtConfigs(rc); + return this; + } + + private void fromConfig(String section, String subSection, Config rc) { + long cfgBlockLimit = rc.getLong(section, subSection, + CONFIG_KEY_BLOCK_LIMIT, getBlockLimit()); + int cfgBlockSize = rc.getInt(section, subSection, CONFIG_KEY_BLOCK_SIZE, getBlockSize()); if (cfgBlockLimit % cfgBlockSize != 0) { throw new IllegalArgumentException(MessageFormat.format( JGitText.get().blockLimitNotMultipleOfBlockSize, - Long.valueOf(cfgBlockLimit), - Long.valueOf(cfgBlockSize))); + Long.valueOf(cfgBlockLimit), Long.valueOf(cfgBlockSize))); } + // Set name only if `core dfs` is configured, otherwise fall back to the + // default. + if (rc.getSubsections(section).contains(subSection)) { + this.name = subSection; + } setBlockLimit(cfgBlockLimit); setBlockSize(cfgBlockSize); - setConcurrencyLevel(rc.getInt( - CONFIG_CORE_SECTION, - CONFIG_DFS_SECTION, - CONFIG_KEY_CONCURRENCY_LEVEL, - getConcurrencyLevel())); + setConcurrencyLevel(rc.getInt(section, subSection, + CONFIG_KEY_CONCURRENCY_LEVEL, getConcurrencyLevel())); - String v = rc.getString( - CONFIG_CORE_SECTION, - CONFIG_DFS_SECTION, - CONFIG_KEY_STREAM_RATIO); + String v = rc.getString(section, subSection, CONFIG_KEY_STREAM_RATIO); if (v != null) { try { setStreamRatio(Double.parseDouble(v)); } catch (NumberFormatException e) { throw new IllegalArgumentException(MessageFormat.format( - JGitText.get().enumValueNotSupported3, - CONFIG_CORE_SECTION, - CONFIG_DFS_SECTION, - CONFIG_KEY_STREAM_RATIO, v), e); + JGitText.get().enumValueNotSupported3, section, + subSection, CONFIG_KEY_STREAM_RATIO, v), e); } } - return this; + } + + private void loadPackExtConfigs(Config config) { + List<String> subSections = config.getSubsections(CONFIG_CORE_SECTION) + .stream() + .filter(section -> section.startsWith(CONFIG_DFS_CACHE_PREFIX)) + .collect(Collectors.toList()); + if (subSections.size() == 0) { + return; + } + ArrayList<DfsBlockCachePackExtConfig> cacheConfigs = new ArrayList<>(); + Set<PackExt> extensionsSeen = new HashSet<>(); + for (String subSection : subSections) { + var cacheConfig = DfsBlockCachePackExtConfig.fromConfig(config, + CONFIG_CORE_SECTION, subSection); + Set<PackExt> packExtsDuplicates = intersection(extensionsSeen, + cacheConfig.packExts); + if (packExtsDuplicates.size() > 0) { + String duplicatePackExts = packExtsDuplicates.stream() + .map(PackExt::toString) + .collect(Collectors.joining(",")); //$NON-NLS-1$ + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().duplicatePackExtensionsSet, + CONFIG_CORE_SECTION, subSection, + CONFIG_KEY_PACK_EXTENSIONS, duplicatePackExts)); + } + extensionsSeen.addAll(cacheConfig.packExts); + cacheConfigs.add(cacheConfig); + } + packExtCacheConfigurations = cacheConfigs; + setCacheHotMapToPackExtConfigs(this.cacheHotMap); + } + + private static <T> Set<T> intersection(Set<T> first, Set<T> second) { + Set<T> ret = new HashSet<>(); + for (T entry : second) { + if (first.contains(entry)) { + ret.add(entry); + } + } + return ret; } /** Consumer of DfsBlockCache loading and eviction events for indexes. */ @@ -338,10 +498,110 @@ public class DfsBlockCacheConfig { } /** + * Whether evicted events should be reported + * * @return true if reporting evicted events is enabled. */ default boolean shouldReportEvictedEvent() { return false; } } -}
\ No newline at end of file + + /** + * A configuration for a single cache table storing 1 or more Pack + * extensions. + * <p> + * The current pack ext cache tables implementation supports the same + * parameters the ClockBlockCacheTable (current default implementation). + * <p> + * Configuration falls back to the defaults coded values defined in the + * {@link DfsBlockCacheConfig} when not set on each cache table + * configuration and NOT the values of the basic dfs section. + * <p> + * <code> + * + * Format: + * [core "dfs.packCache"] + * packExtensions = "PACK" + * blockSize = 512 + * blockLimit = 100 + * concurrencyLevel = 5 + * + * [core "dfs.multipleExtensionCache"] + * packExtensions = "INDEX REFTABLE BITMAP_INDEX" + * blockSize = 512 + * blockLimit = 100 + * concurrencyLevel = 5 + * </code> + */ + static class DfsBlockCachePackExtConfig { + // Set of pack extensions that will map to the cache instance. + private final EnumSet<PackExt> packExts; + + // Configuration for the cache instance. + private final DfsBlockCacheConfig packExtCacheConfiguration; + + /** + * Made visible for testing. + * + * @param packExts + * Set of {@link PackExt}s associated to this cache config. + * @param packExtCacheConfiguration + * {@link DfsBlockCacheConfig} for this cache config. + */ + DfsBlockCachePackExtConfig(EnumSet<PackExt> packExts, + DfsBlockCacheConfig packExtCacheConfiguration) { + this.packExts = packExts; + this.packExtCacheConfiguration = packExtCacheConfiguration; + } + + Set<PackExt> getPackExts() { + return packExts; + } + + DfsBlockCacheConfig getPackExtCacheConfiguration() { + return packExtCacheConfiguration; + } + + void setCacheHotMap(Map<PackExt, Integer> cacheHotMap) { + Map<PackExt, Integer> packExtHotMap = packExts.stream() + .filter(cacheHotMap::containsKey) + .collect(Collectors.toUnmodifiableMap(Function.identity(), + cacheHotMap::get)); + packExtCacheConfiguration.setCacheHotMap(packExtHotMap); + } + + private static DfsBlockCachePackExtConfig fromConfig(Config config, + String section, String subSection) { + String packExtensions = config.getString(section, subSection, + CONFIG_KEY_PACK_EXTENSIONS); + if (packExtensions == null) { + throw new IllegalArgumentException( + JGitText.get().noPackExtGivenForConfiguration); + } + String[] extensions = packExtensions.split(" ", -1); //$NON-NLS-1$ + Set<PackExt> packExts = new HashSet<>(extensions.length); + for (String extension : extensions) { + try { + packExts.add(PackExt.valueOf(extension)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().unknownPackExtension, section, + subSection, CONFIG_KEY_PACK_EXTENSIONS, extension), + e); + } + } + + DfsBlockCacheConfig dfsBlockCacheConfig = new DfsBlockCacheConfig(); + dfsBlockCacheConfig.fromConfig(section, subSection, config); + return new DfsBlockCachePackExtConfig(EnumSet.copyOf(packExts), + dfsBlockCacheConfig); + } + + void print(String linePrefix, String pad, PrintWriter writer) { + packExtCacheConfiguration.print(linePrefix, pad, writer); + writer.println(linePrefix + pad + "PackExts: " //$NON-NLS-1$ + + packExts.stream().sorted().collect(Collectors.toList())); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheStats.java new file mode 100644 index 0000000000..436f57437e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheStats.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2024, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jgit.internal.storage.pack.PackExt; + +/** + * Keeps track of stats for a Block Cache table. + */ +class DfsBlockCacheStats implements BlockCacheStats { + private final String name; + + /** + * Number of times a block was found in the cache, per pack file extension. + */ + private final AtomicReference<AtomicLong[]> statHit; + + /** + * Number of times a block was not found, and had to be loaded, per pack + * file extension. + */ + private final AtomicReference<AtomicLong[]> statMiss; + + /** + * Number of blocks evicted due to cache being full, per pack file + * extension. + */ + private final AtomicReference<AtomicLong[]> statEvict; + + /** + * Number of bytes currently loaded in the cache, per pack file extension. + */ + private final AtomicReference<AtomicLong[]> liveBytes; + + DfsBlockCacheStats() { + this(""); //$NON-NLS-1$ + } + + DfsBlockCacheStats(String name) { + this.name = name; + statHit = new AtomicReference<>(newCounters()); + statMiss = new AtomicReference<>(newCounters()); + statEvict = new AtomicReference<>(newCounters()); + liveBytes = new AtomicReference<>(newCounters()); + } + + @Override + public String getName() { + return name; + } + + /** + * Increment the {@code statHit} count. + * + * @param key + * key identifying which liveBytes entry to update. + */ + void incrementHit(DfsStreamKey key) { + getStat(statHit, key).incrementAndGet(); + } + + /** + * Increment the {@code statMiss} count. + * + * @param key + * key identifying which liveBytes entry to update. + */ + void incrementMiss(DfsStreamKey key) { + getStat(statMiss, key).incrementAndGet(); + } + + /** + * Increment the {@code statEvict} count. + * + * @param key + * key identifying which liveBytes entry to update. + */ + void incrementEvict(DfsStreamKey key) { + getStat(statEvict, key).incrementAndGet(); + } + + /** + * Add {@code size} to the {@code liveBytes} count. + * + * @param key + * key identifying which liveBytes entry to update. + * @param size + * amount to increment the count by. + */ + void addToLiveBytes(DfsStreamKey key, long size) { + getStat(liveBytes, key).addAndGet(size); + } + + @Override + public long[] getCurrentSize() { + return getStatVals(liveBytes); + } + + @Override + public long[] getHitCount() { + return getStatVals(statHit); + } + + @Override + public long[] getMissCount() { + return getStatVals(statMiss); + } + + @Override + public long[] getTotalRequestCount() { + AtomicLong[] hit = statHit.get(); + AtomicLong[] miss = statMiss.get(); + long[] cnt = new long[Math.max(hit.length, miss.length)]; + for (int i = 0; i < hit.length; i++) { + cnt[i] += hit[i].get(); + } + for (int i = 0; i < miss.length; i++) { + cnt[i] += miss[i].get(); + } + return cnt; + } + + @Override + public long[] getHitRatio() { + AtomicLong[] hit = statHit.get(); + AtomicLong[] miss = statMiss.get(); + long[] ratio = new long[Math.max(hit.length, miss.length)]; + for (int i = 0; i < ratio.length; i++) { + if (i >= hit.length) { + ratio[i] = 0; + } else if (i >= miss.length) { + ratio[i] = 100; + } else { + long hitVal = hit[i].get(); + long missVal = miss[i].get(); + long total = hitVal + missVal; + ratio[i] = total == 0 ? 0 : hitVal * 100 / total; + } + } + return ratio; + } + + @Override + public long[] getEvictions() { + return getStatVals(statEvict); + } + + private static AtomicLong[] newCounters() { + AtomicLong[] ret = new AtomicLong[PackExt.values().length]; + for (int i = 0; i < ret.length; i++) { + ret[i] = new AtomicLong(); + } + return ret; + } + + private static long[] getStatVals(AtomicReference<AtomicLong[]> stat) { + AtomicLong[] stats = stat.get(); + long[] cnt = new long[stats.length]; + for (int i = 0; i < stats.length; i++) { + cnt[i] = stats[i].get(); + } + return cnt; + } + + private static AtomicLong getStat(AtomicReference<AtomicLong[]> stats, + DfsStreamKey key) { + int pos = key.packExtPos; + while (true) { + AtomicLong[] vals = stats.get(); + if (pos < vals.length) { + return vals[pos]; + } + AtomicLong[] expect = vals; + vals = new AtomicLong[Math.max(pos + 1, PackExt.values().length)]; + System.arraycopy(expect, 0, vals, 0, expect.length); + for (int i = expect.length; i < vals.length; i++) { + vals[i] = new AtomicLong(); + } + if (stats.compareAndSet(expect, vals)) { + return vals[pos]; + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java new file mode 100644 index 0000000000..c3fd07b7bb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2024, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import java.io.IOException; +import java.util.List; + +/** + * Block cache table. + */ +public interface DfsBlockCacheTable { + /** + * Quickly check if the cache contains block 0 of the given stream. + * <p> + * This can be useful for sophisticated pre-read algorithms to quickly + * determine if a file is likely already in cache, especially small + * reftables which may be smaller than a typical DFS block size. + * + * @param key + * the file to check. + * @return true if block 0 (the first block) is in the cache. + */ + boolean hasBlock0(DfsStreamKey key); + + /** + * Look up a cached object, creating and loading it if it doesn't exist. + * + * @param file + * the pack that "contains" the cached object. + * @param position + * offset within <code>pack</code> of the object. + * @param dfsReader + * current thread's reader. + * @param fileChannel + * supplier for channel to read {@code pack}. + * @return the object reference. + * @throws IOException + * the reference was not in the cache and could not be loaded. + */ + DfsBlock getOrLoad(BlockBasedFile file, long position, DfsReader dfsReader, + DfsBlockCache.ReadableChannelSupplier fileChannel) + throws IOException; + + /** + * Look up a cached object, creating and loading it if it doesn't exist. + * + * @param key + * the stream key of the pack. + * @param position + * the position in the key. The default should be 0. + * @param loader + * the function to load the reference. + * @return the object reference. + * @throws IOException + * the reference was not in the cache and could not be loaded. + */ + <T> DfsBlockCache.Ref<T> getOrLoadRef(DfsStreamKey key, long position, + DfsBlockCache.RefLoader<T> loader) throws IOException; + + /** + * Put a block in the block cache. + * + * @param v + * the block to put in the cache. + */ + void put(DfsBlock v); + + /** + * Put a block in the block cache. + * + * @param key + * the stream key of the pack. + * @param pos + * the position in the key. + * @param size + * the size of the object. + * @param v + * the object to put in the block cache. + * @return the object reference. + */ + <T> DfsBlockCache.Ref<T> put(DfsStreamKey key, long pos, long size, T v); + + /** + * Put an object in the block cache. + * + * @param key + * the stream key of the pack. + * @param size + * the size of the object. + * @param v + * the object to put in the block cache. + * @return the object reference. + */ + <T> DfsBlockCache.Ref<T> putRef(DfsStreamKey key, long size, T v); + + /** + * Check if the block cache contains an object identified by (key, + * position). + * + * @param key + * the stream key of the pack. + * @param position + * the position in the key. + * @return if the block cache contains the object identified by (key, + * position). + */ + boolean contains(DfsStreamKey key, long position); + + /** + * Get the object identified by (key, position) from the block cache. + * + * @param key + * the stream key of the pack. + * @param position + * the position in the key. + * @return the object identified by (key, position). + */ + <T> T get(DfsStreamKey key, long position); + + /** + * Get the list of {@link BlockCacheStats} held by this cache. + * <p> + * The returned list has a {@link BlockCacheStats} per configured cache + * table, with a minimum of 1 {@link BlockCacheStats} object returned. + * + * Use {@link AggregatedBlockCacheStats} to combine the results of the stats + * in the list for an aggregated view of the cache's stats. + * + * @return the list of {@link BlockCacheStats} held by this cache. + */ + List<BlockCacheStats> getBlockCacheStats(); + + /** + * Get the name of the table. + * + * @return this table's name. + */ + String getName(); + + /** + * Provides methods used with Block Cache statistics. + */ + interface BlockCacheStats { + + /** + * Get the name of the block cache generating this instance. + * + * @return this cache's name. + */ + String getName(); + + /** + * Get total number of bytes in the cache, per pack file extension. + * + * @return total number of bytes in the cache, per pack file extension. + */ + long[] getCurrentSize(); + + /** + * Get number of requests for items in the cache, per pack file + * extension. + * + * @return the number of requests for items in the cache, per pack file + * extension. + */ + long[] getHitCount(); + + /** + * Get number of requests for items not in the cache, per pack file + * extension. + * + * @return the number of requests for items not in the cache, per pack + * file extension. + */ + long[] getMissCount(); + + /** + * Get total number of requests (hit + miss), per pack file extension. + * + * @return total number of requests (hit + miss), per pack file + * extension. + */ + long[] getTotalRequestCount(); + + /** + * Get hit ratios. + * + * @return hit ratios. + */ + long[] getHitRatio(); + + /** + * Get number of evictions performed due to cache being full, per pack + * file extension. + * + * @return the number of evictions performed due to cache being full, + * per pack file extension. + */ + long[] getEvictions(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java index 22959e9f6b..af8cd46d01 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java @@ -28,6 +28,8 @@ public class DfsCachedPack extends CachedPack { } /** + * Get pack file + * * @return the pack passed to the constructor */ public DfsPackFile getPackFile() { @@ -43,19 +45,16 @@ public class DfsCachedPack extends CachedPack { return pack.getPackDescription(); } - /** {@inheritDoc} */ @Override public long getObjectCount() throws IOException { return getPackDescription().getObjectCount(); } - /** {@inheritDoc} */ @Override public long getDeltaCount() throws IOException { return getPackDescription().getDeltaCount(); } - /** {@inheritDoc} */ @Override public boolean hasObject(ObjectToPack obj, StoredObjectRepresentation rep) { return ((DfsObjectRepresentation) rep).pack == pack; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsConfig.java index 2ee23f8627..4f0e4a55d4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsConfig.java @@ -22,13 +22,11 @@ import org.eclipse.jgit.lib.StoredConfig; * simply clears the config, and saving does nothing. */ public final class DfsConfig extends StoredConfig { - /** {@inheritDoc} */ @Override public void load() throws IOException, ConfigInvalidException { clear(); } - /** {@inheritDoc} */ @Override public void save() throws IOException { // TODO actually store this configuration. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java index 66bcf73987..199481cf33 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java @@ -17,14 +17,14 @@ import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.IN import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.RECEIVE; import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE; import static org.eclipse.jgit.internal.storage.dfs.DfsPackCompactor.configureReftable; -import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.COMMIT_GRAPH; -import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; +import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE; import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -35,7 +35,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.commitgraph.CommitGraphWriter; @@ -80,6 +79,8 @@ public class DfsGarbageCollector { private ReftableConfig reftableConfig; private boolean convertToReftable = true; private boolean writeCommitGraph; + + private boolean writeBloomFilter; private boolean includeDeletes; private long reftableInitialMinUpdateIndex = 1; private long reftableInitialMaxUpdateIndex = 1; @@ -89,7 +90,7 @@ public class DfsGarbageCollector { private long coalesceGarbageLimit = 50 << 20; private long garbageTtlMillis = TimeUnit.DAYS.toMillis(1); - private long startTimeMillis; + private Instant startTime; private List<DfsPackFile> packsBefore; private List<DfsReftable> reftablesBefore; private List<DfsPackFile> expiredGarbagePacks; @@ -99,6 +100,7 @@ public class DfsGarbageCollector { private Set<ObjectId> allTags; private Set<ObjectId> nonHeads; private Set<ObjectId> tagTargets; + private Instant refLogExpire; /** * Initialize a garbage collector. @@ -199,6 +201,22 @@ public class DfsGarbageCollector { return this; } + + /** + * Set time limit to the reflog history. + * <p> + * Garbage Collector prunes entries from reflog history older than {@code refLogExpire} + * <p> + * + * @param refLogExpire + * instant in time which defines refLog expiration + * @return {@code this} + */ + public DfsGarbageCollector setRefLogExpire(Instant refLogExpire) { + this.refLogExpire = refLogExpire; + return this; + } + /** * Set maxUpdateIndex for the initial reftable created during conversion. * @@ -298,6 +316,20 @@ public class DfsGarbageCollector { } /** + * Toggle bloom filter generation. + * <p> + * False by default. + * + * @param enable + * Whether bloom filter generation is enabled + * @return {@code this} + */ + public DfsGarbageCollector setWriteBloomFilter(boolean enable) { + writeBloomFilter = enable; + return this; + } + + /** * Create a single new pack file containing all of the live objects. * <p> * This method safely decides which packs can be expired after the new pack @@ -320,7 +352,7 @@ public class DfsGarbageCollector { throw new IllegalStateException( JGitText.get().supportOnlyPackIndexVersion2); - startTimeMillis = SystemReader.getInstance().getCurrentTime(); + startTime = SystemReader.getInstance().now(); ctx = objdb.newReader(); try { refdb.refresh(); @@ -403,7 +435,7 @@ public class DfsGarbageCollector { packsBefore = new ArrayList<>(packs.length); expiredGarbagePacks = new ArrayList<>(packs.length); - long now = SystemReader.getInstance().getCurrentTime(); + long now = SystemReader.getInstance().now().toEpochMilli(); for (DfsPackFile p : packs) { DfsPackDescription d = p.getPackDescription(); if (d.getPackSource() != UNREACHABLE_GARBAGE) { @@ -577,6 +609,7 @@ public class DfsGarbageCollector { cfg.setReuseObjects(true); cfg.setDeltaCompress(false); cfg.setBuildBitmaps(false); + cfg.setWriteReverseIndex(false); try (PackWriter pw = new PackWriter(cfg, ctx); RevWalk pool = new RevWalk(ctx)) { @@ -671,28 +704,26 @@ public class DfsGarbageCollector { pack.setBlockSize(PACK, out.blockSize()); } - try (DfsOutputStream out = objdb.writeFile(pack, INDEX)) { - CountingOutputStream cnt = new CountingOutputStream(out); - pw.writeIndex(cnt); - pack.addFileExt(INDEX); - pack.setFileSize(INDEX, cnt.getCount()); - pack.setBlockSize(INDEX, out.blockSize()); - pack.setIndexVersion(pw.getIndexVersion()); - } + pw.writeIndex(objdb.getPackIndexWriter(pack, pw.getIndexVersion())); - if (pw.prepareBitmapIndex(pm)) { - try (DfsOutputStream out = objdb.writeFile(pack, BITMAP_INDEX)) { + if (source != UNREACHABLE_GARBAGE && packConfig.getMinBytesForObjSizeIndex() >= 0) { + try (DfsOutputStream out = objdb.writeFile(pack, + OBJECT_SIZE_INDEX)) { CountingOutputStream cnt = new CountingOutputStream(out); - pw.writeBitmapIndex(cnt); - pack.addFileExt(BITMAP_INDEX); - pack.setFileSize(BITMAP_INDEX, cnt.getCount()); - pack.setBlockSize(BITMAP_INDEX, out.blockSize()); + pw.writeObjectSizeIndex(cnt); + pack.addFileExt(OBJECT_SIZE_INDEX); + pack.setFileSize(OBJECT_SIZE_INDEX, cnt.getCount()); + pack.setBlockSize(OBJECT_SIZE_INDEX, out.blockSize()); } } + if (pw.prepareBitmapIndex(pm)) { + pw.writeBitmapIndex(objdb.getPackBitmapIndexWriter(pack)); + } + PackStatistics stats = pw.getStatistics(); pack.setPackStats(stats); - pack.setLastModified(startTimeMillis); + pack.setLastModified(startTime.toEpochMilli()); newPackDesc.add(pack); newPackStats.add(stats); newPackObj.add(pw.getObjectSet()); @@ -720,6 +751,10 @@ public class DfsGarbageCollector { compact.addAll(stack.readers()); compact.setIncludeDeletes(includeDeletes); compact.setConfig(configureReftable(reftableConfig, out)); + if(refLogExpire != null ){ + compact.setReflogExpireOldestReflogTimeMillis( + refLogExpire.toEpochMilli()); + } compact.compact(); pack.addFileExt(REFTABLE); pack.setReftableStats(compact.getStats()); @@ -754,18 +789,17 @@ public class DfsGarbageCollector { return; } - Set<ObjectId> allTips = refsBefore.stream().map(Ref::getObjectId) - .collect(Collectors.toUnmodifiableSet()); - try (DfsOutputStream out = objdb.writeFile(pack, COMMIT_GRAPH); RevWalk pool = new RevWalk(ctx)) { - GraphCommits gcs = GraphCommits.fromWalk(pm, allTips, pool); + GraphCommits gcs = GraphCommits.fromWalk(pm, allHeadsAndTags, pool); CountingOutputStream cnt = new CountingOutputStream(out); - CommitGraphWriter writer = new CommitGraphWriter(gcs); - writer.write(pm, cnt); + CommitGraphWriter writer = new CommitGraphWriter(gcs, + writeBloomFilter); + CommitGraphWriter.Stats stats = writer.write(pm, cnt); pack.addFileExt(COMMIT_GRAPH); pack.setFileSize(COMMIT_GRAPH, cnt.getCount()); pack.setBlockSize(COMMIT_GRAPH, out.blockSize()); + pack.setCommitGraphStats(stats); } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java index a2fb67ff0b..dd9e4b96a4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java @@ -11,6 +11,7 @@ package org.eclipse.jgit.internal.storage.dfs; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; +import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import static org.eclipse.jgit.lib.Constants.OBJ_OFS_DELTA; import static org.eclipse.jgit.lib.Constants.OBJ_REF_DELTA; @@ -40,11 +41,13 @@ import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter; import org.eclipse.jgit.internal.storage.file.PackIndex; -import org.eclipse.jgit.internal.storage.file.PackIndexWriter; +import org.eclipse.jgit.internal.storage.file.PackObjectSizeIndexWriter; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdOwnerMap; @@ -68,6 +71,8 @@ public class DfsInserter extends ObjectInserter { private static final int INDEX_VERSION = 2; final DfsObjDatabase db; + + private final int minBytesForObjectSizeIndex; int compression = Deflater.BEST_COMPRESSION; List<PackedObjectInfo> objectList; @@ -78,7 +83,6 @@ public class DfsInserter extends ObjectInserter { DfsPackDescription packDsc; PackStream packOut; private boolean rollback; - private boolean checkExisting = true; /** * Initialize a new inserter. @@ -88,37 +92,24 @@ public class DfsInserter extends ObjectInserter { */ protected DfsInserter(DfsObjDatabase db) { this.db = db; - } - - /** - * Check existence - * - * @param check - * if {@code false}, will write out possibly-duplicate objects - * without first checking whether they exist in the repo; default - * is true. - */ - public void checkExisting(boolean check) { - checkExisting = check; + this.minBytesForObjectSizeIndex = db.getRepository().getConfig().getInt( + ConfigConstants.CONFIG_PACK_SECTION, + ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, -1); } void setCompressionLevel(int compression) { this.compression = compression; } - - /** {@inheritDoc} */ @Override public DfsPackParser newPackParser(InputStream in) throws IOException { return new DfsPackParser(db, this, in); } - /** {@inheritDoc} */ @Override public ObjectReader newReader() { return new Reader(); } - /** {@inheritDoc} */ @Override public ObjectId insert(int type, byte[] data, int off, int len) throws IOException { @@ -126,16 +117,16 @@ public class DfsInserter extends ObjectInserter { if (objectMap != null && objectMap.contains(id)) return id; // Ignore unreachable (garbage) objects here. - if (checkExisting && db.has(id, true)) + if (db.has(id, true)) { return id; + } long offset = beginObject(type, len); packOut.compress.write(data, off, len); packOut.compress.finish(); - return endObject(id, offset); + return endObject(id, offset, len, type); } - /** {@inheritDoc} */ @Override public ObjectId insert(int type, long len, InputStream in) throws IOException { @@ -152,16 +143,17 @@ public class DfsInserter extends ObjectInserter { md.update(Constants.encodeASCII(len)); md.update((byte) 0); - while (0 < len) { - int n = in.read(buf, 0, (int) Math.min(buf.length, len)); + long inLength = len; + while (0 < inLength) { + int n = in.read(buf, 0, (int) Math.min(buf.length, inLength)); if (n <= 0) throw new EOFException(); md.update(buf, 0, n); packOut.compress.write(buf, 0, n); - len -= n; + inLength -= n; } packOut.compress.finish(); - return endObject(md.toObjectId(), offset); + return endObject(md.toObjectId(), offset, len, type); } private byte[] insertBuffer(long len) { @@ -178,7 +170,6 @@ public class DfsInserter extends ObjectInserter { return buf; } - /** {@inheritDoc} */ @Override public void flush() throws IOException { if (packDsc == null) @@ -196,17 +187,17 @@ public class DfsInserter extends ObjectInserter { sortObjectsById(); PackIndex index = writePackIndex(packDsc, packHash, objectList); + writeObjectSizeIndex(packDsc, objectList); db.commitPack(Collections.singletonList(packDsc), null); rollback = false; - DfsPackFile p = new DfsPackFile(cache, packDsc); + DfsPackFile p = db.createDfsPackFile(cache, packDsc); if (index != null) p.setPackIndex(index); db.addPack(p); clear(); } - /** {@inheritDoc} */ @Override public void close() { if (packOut != null) { @@ -244,10 +235,12 @@ public class DfsInserter extends ObjectInserter { return offset; } - private ObjectId endObject(ObjectId id, long offset) { + private ObjectId endObject(ObjectId id, long offset, long inflatedSize, int type) { PackedObjectInfo obj = new PackedObjectInfo(id); + obj.setType(type); obj.setOffset(offset); obj.setCRC((int) packOut.crc32.getValue()); + obj.setFullSize(inflatedSize); objectList.add(obj); objectMap.addIfAbsent(obj); return id; @@ -314,7 +307,23 @@ public class DfsInserter extends ObjectInserter { private static void index(OutputStream out, byte[] packHash, List<PackedObjectInfo> list) throws IOException { - PackIndexWriter.createVersion(out, INDEX_VERSION).write(list, packHash); + BasePackIndexWriter.createVersion(out, INDEX_VERSION).write(list, packHash); + } + + void writeObjectSizeIndex(DfsPackDescription pack, + List<PackedObjectInfo> packedObjs) throws IOException { + if (minBytesForObjectSizeIndex < 0) { + return; + } + try (DfsOutputStream os = db.writeFile(pack, PackExt.OBJECT_SIZE_INDEX); + CountingOutputStream cnt = new CountingOutputStream(os)) { + PackObjectSizeIndexWriter + .createWriter(os, minBytesForObjectSizeIndex) + .write(packedObjs); + pack.addFileExt(OBJECT_SIZE_INDEX); + pack.setBlockSize(OBJECT_SIZE_INDEX, os.blockSize()); + pack.setFileSize(OBJECT_SIZE_INDEX, cnt.getCount()); + } } private class PackStream extends OutputStream { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java index 46ec87df54..1a873d1204 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java @@ -11,6 +11,8 @@ package org.eclipse.jgit.internal.storage.dfs; import static java.util.stream.Collectors.joining; +import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX; +import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; import java.io.FileNotFoundException; import java.io.IOException; @@ -26,11 +28,16 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter; +import org.eclipse.jgit.internal.storage.file.PackBitmapIndexWriterV1; +import org.eclipse.jgit.internal.storage.pack.PackIndexWriter; +import org.eclipse.jgit.internal.storage.pack.PackBitmapIndexWriter; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectDatabase; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.util.io.CountingOutputStream; /** * Manages objects stored in @@ -45,16 +52,6 @@ public abstract class DfsObjDatabase extends ObjectDatabase { boolean dirty() { return true; } - - @Override - void clearDirty() { - // Always dirty. - } - - @Override - public void markDirty() { - // Always dirty. - } }; /** @@ -241,13 +238,11 @@ public abstract class DfsObjDatabase extends ObjectDatabase { this.packComparator = packComparator; } - /** {@inheritDoc} */ @Override public DfsReader newReader() { return new DfsReader(this); } - /** {@inheritDoc} */ @Override public ObjectInserter newInserter() { return new DfsInserter(this); @@ -372,7 +367,7 @@ public abstract class DfsObjDatabase extends ObjectDatabase { * Default implementation of this method would be equivalent to * {@code newPack(source).setEstimatedPackSize(estimatedPackSize)}. But the * clients can override this method to use the given - * {@code estomatedPackSize} value more efficiently in the process of + * {@code estimatedPackSize} value more efficiently in the process of * creating a new * {@link org.eclipse.jgit.internal.storage.dfs.DfsPackDescription} object. * @@ -529,7 +524,7 @@ public abstract class DfsObjDatabase extends ObjectDatabase { DfsPackFile[] packs = new DfsPackFile[1 + o.packs.length]; packs[0] = newPack; System.arraycopy(o.packs, 0, packs, 1, o.packs.length); - n = new PackListImpl(packs, o.reftables); + n = new PackList(packs, o.reftables); } while (!packList.compareAndSet(o, n)); } @@ -554,7 +549,7 @@ public abstract class DfsObjDatabase extends ObjectDatabase { } } tables.add(new DfsReftable(add)); - n = new PackListImpl(o.packs, tables.toArray(new DfsReftable[0])); + n = new PackList(o.packs, tables.toArray(new DfsReftable[0])); } while (!packList.compareAndSet(o, n)); } @@ -594,7 +589,7 @@ public abstract class DfsObjDatabase extends ObjectDatabase { if (oldPack != null) { newPacks.add(oldPack); } else if (dsc.hasFileExt(PackExt.PACK)) { - newPacks.add(new DfsPackFile(cache, dsc)); + newPacks.add(createDfsPackFile(cache, dsc)); foundNew = true; } @@ -608,17 +603,33 @@ public abstract class DfsObjDatabase extends ObjectDatabase { } if (newPacks.isEmpty() && newReftables.isEmpty()) - return new PackListImpl(NO_PACKS.packs, NO_PACKS.reftables); + return new PackList(NO_PACKS.packs, NO_PACKS.reftables); if (!foundNew) { - old.clearDirty(); return old; } Collections.sort(newReftables, reftableComparator()); - return new PackListImpl( + return new PackList( newPacks.toArray(new DfsPackFile[0]), newReftables.toArray(new DfsReftable[0])); } + /** + * Create instances of DfsPackFile + * + * Implementors can decide to construct or wrap DfsPackFile in different + * ways. + * + * @param cache + * block cache + * @param dsc + * pack description + * @return the dfs packfile + */ + protected DfsPackFile createDfsPackFile(DfsBlockCache cache, + DfsPackDescription dsc) { + return new DfsPackFile(cache, dsc); + } + private static Map<DfsPackDescription, DfsPackFile> packMap(PackList old) { Map<DfsPackDescription, DfsPackFile> forReuse = new HashMap<>(); for (DfsPackFile p : old.packs) { @@ -657,14 +668,13 @@ public abstract class DfsObjDatabase extends ObjectDatabase { packList.set(NO_PACKS); } - /** {@inheritDoc} */ @Override public void close() { packList.set(NO_PACKS); } /** Snapshot of packs scanned in a single pass. */ - public abstract static class PackList { + public static class PackList { /** All known packs, sorted. */ public final DfsPackFile[] packs; @@ -678,7 +688,11 @@ public abstract class DfsObjDatabase extends ObjectDatabase { this.reftables = reftables; } - /** @return last modified time of all packs, in milliseconds. */ + /** + * Get last modified time of all packs + * + * @return last modified time of all packs, in milliseconds. + */ public long getLastModified() { if (lastModified < 0) { long max = 0; @@ -690,39 +704,63 @@ public abstract class DfsObjDatabase extends ObjectDatabase { return lastModified; } - abstract boolean dirty(); - abstract void clearDirty(); - - /** - * Mark pack list as dirty. - * <p> - * Used when the caller knows that new data might have been written to the - * repository that could invalidate open readers depending on this pack list, - * for example if refs are newly scanned. - */ - public abstract void markDirty(); - } - - private static final class PackListImpl extends PackList { - private volatile boolean dirty; - - PackListImpl(DfsPackFile[] packs, DfsReftable[] reftables) { - super(packs, reftables); - } - - @Override boolean dirty() { - return dirty; + return false; } + } - @Override - void clearDirty() { - dirty = false; - } + /** + * Returns a writer to store the bitmap index in this object database. + * + * @param pack + * Pack file to which the bitmaps are associated. + * @return a writer to store bitmaps associated with the pack + * @throws IOException + * when some I/O problem occurs while creating or writing to + * output stream + */ + public PackBitmapIndexWriter getPackBitmapIndexWriter( + DfsPackDescription pack) throws IOException { + return (bitmaps, packDataChecksum) -> { + try (DfsOutputStream out = writeFile(pack, BITMAP_INDEX)) { + CountingOutputStream cnt = new CountingOutputStream(out); + PackBitmapIndexWriterV1 iw = new PackBitmapIndexWriterV1(cnt); + iw.write(bitmaps, packDataChecksum); + pack.addFileExt(BITMAP_INDEX); + pack.setFileSize(BITMAP_INDEX, cnt.getCount()); + pack.setBlockSize(BITMAP_INDEX, out.blockSize()); + } + }; + } - @Override - public void markDirty() { - dirty = true; - } + /** + * Returns a writer to store the pack index in this object database. + * + * @param pack + * Pack file to which the index is associated. + * @param indexVersion + * which version of the index to write + * @return a writer to store the index associated with the pack + * @throws IOException + * when some I/O problem occurs while creating or writing to + * output stream + */ + public PackIndexWriter getPackIndexWriter( + DfsPackDescription pack, int indexVersion) + throws IOException { + return (objectsToStore, packDataChecksum) -> { + try (DfsOutputStream out = writeFile(pack, INDEX); + CountingOutputStream cnt = new CountingOutputStream(out)) { + final PackIndexWriter iw = BasePackIndexWriter + .createVersion(cnt, + indexVersion); + iw.write(objectsToStore, packDataChecksum); + pack.addFileExt(INDEX); + pack.setFileSize(INDEX, cnt.getCount()); + pack.setBlockSize(INDEX, out.blockSize()); + pack.setIndexVersion(indexVersion); + } + }; } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectRepresentation.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectRepresentation.java index 8e124e3c20..885582fae2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectRepresentation.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectRepresentation.java @@ -24,25 +24,21 @@ class DfsObjectRepresentation extends StoredObjectRepresentation { this.pack = pack; } - /** {@inheritDoc} */ @Override public int getFormat() { return format; } - /** {@inheritDoc} */ @Override public int getWeight() { return (int) Math.min(length, Integer.MAX_VALUE); } - /** {@inheritDoc} */ @Override public ObjectId getDeltaBase() { return baseId; } - /** {@inheritDoc} */ @Override public boolean wasDeltaAttempted() { switch (pack.getPackDescription().getPackSource()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectToPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectToPack.java index 10fa2365bd..c2feab8cd3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectToPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectToPack.java @@ -39,14 +39,12 @@ class DfsObjectToPack extends ObjectToPack { setExtendedFlag(FLAG_FOUND); } - /** {@inheritDoc} */ @Override protected void clearReuseAsIs() { super.clearReuseAsIs(); pack = null; } - /** {@inheritDoc} */ @Override public void select(StoredObjectRepresentation ref) { DfsObjectRepresentation ptr = (DfsObjectRepresentation) ref; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsOutputStream.java index 7cf91a4882..c74a1e1d7d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsOutputStream.java @@ -39,13 +39,11 @@ public abstract class DfsOutputStream extends OutputStream { return 0; } - /** {@inheritDoc} */ @Override public void write(int b) throws IOException { write(new byte[] { (byte) b }); } - /** {@inheritDoc} */ @Override public abstract void write(byte[] buf, int off, int len) throws IOException; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java index 86144b389c..6339b0326a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java @@ -12,7 +12,7 @@ package org.eclipse.jgit.internal.storage.dfs; import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.COMPACT; import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC; -import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; +import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE; import static org.eclipse.jgit.internal.storage.pack.StoredObjectRepresentation.PACK_DELTA; @@ -249,6 +249,7 @@ public class DfsPackCompactor { try { writePack(objdb, outDesc, pw, pm); writeIndex(objdb, outDesc, pw); + writeObjectSizeIndex(objdb, outDesc, pw); PackStatistics stats = pw.getStatistics(); @@ -404,7 +405,7 @@ public class DfsPackCompactor { pw.addObject(obj); obj.add(added); - src.representation(rep, id.offset, ctx, rev); + src.fillRepresentation(rep, id.offset, ctx, rev); if (rep.getFormat() != PACK_DELTA) continue; @@ -458,13 +459,20 @@ public class DfsPackCompactor { private static void writeIndex(DfsObjDatabase objdb, DfsPackDescription pack, PackWriter pw) throws IOException { - try (DfsOutputStream out = objdb.writeFile(pack, INDEX)) { + pw.writeIndex(objdb.getPackIndexWriter(pack, pw.getIndexVersion())); + } + + private static void writeObjectSizeIndex(DfsObjDatabase objdb, + DfsPackDescription pack, + PackWriter pw) throws IOException { + try (DfsOutputStream out = objdb.writeFile(pack, OBJECT_SIZE_INDEX)) { CountingOutputStream cnt = new CountingOutputStream(out); - pw.writeIndex(cnt); - pack.addFileExt(INDEX); - pack.setFileSize(INDEX, cnt.getCount()); - pack.setBlockSize(INDEX, out.blockSize()); - pack.setIndexVersion(pw.getIndexVersion()); + pw.writeObjectSizeIndex(cnt); + if (cnt.getCount() > 0) { + pack.addFileExt(OBJECT_SIZE_INDEX); + pack.setFileSize(OBJECT_SIZE_INDEX, cnt.getCount()); + pack.setBlockSize(OBJECT_SIZE_INDEX, out.blockSize()); + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java index 4f418ab4db..663190a233 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Comparator; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.internal.storage.commitgraph.CommitGraphWriter; import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; @@ -144,6 +145,7 @@ public class DfsPackDescription { private PackStatistics packStats; private ReftableWriter.Stats refStats; + private CommitGraphWriter.Stats commitGraphStats; private int extensions; private int indexVersion; private long estimatedPackSize; @@ -481,6 +483,19 @@ public class DfsPackDescription { } /** + * Get stats from the sibling commit graph, if created. + * + * @return stats from the sibling commit graph, if created. + */ + public CommitGraphWriter.Stats getCommitGraphStats() { + return commitGraphStats; + } + + void setCommitGraphStats(CommitGraphWriter.Stats stats) { + this.commitGraphStats = stats; + } + + /** * Discard the pack statistics, if it was populated. * * @return {@code this} @@ -512,13 +527,11 @@ public class DfsPackDescription { return this; } - /** {@inheritDoc} */ @Override public int hashCode() { return packName.hashCode(); } - /** {@inheritDoc} */ @Override public boolean equals(Object b) { if (b instanceof DfsPackDescription) { @@ -539,7 +552,6 @@ public class DfsPackDescription { } } - /** {@inheritDoc} */ @Override public String toString() { return getFileName(PackExt.PACK); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java index c745b8e6ee..05b63eaca1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java @@ -16,6 +16,7 @@ import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UN import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.COMMIT_GRAPH; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; +import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import static org.eclipse.jgit.internal.storage.pack.PackExt.REVERSE_INDEX; @@ -26,8 +27,12 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.text.MessageFormat; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.zip.CRC32; import java.util.zip.DataFormatException; import java.util.zip.Inflater; @@ -42,16 +47,23 @@ import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph; import org.eclipse.jgit.internal.storage.commitgraph.CommitGraphLoader; import org.eclipse.jgit.internal.storage.file.PackBitmapIndex; import org.eclipse.jgit.internal.storage.file.PackIndex; +import org.eclipse.jgit.internal.storage.file.PackObjectSizeIndex; +import org.eclipse.jgit.internal.storage.file.PackObjectSizeIndexLoader; import org.eclipse.jgit.internal.storage.file.PackReverseIndex; +import org.eclipse.jgit.internal.storage.file.PackReverseIndexFactory; import org.eclipse.jgit.internal.storage.pack.BinaryDelta; +import org.eclipse.jgit.internal.storage.pack.ObjectToPack; import org.eclipse.jgit.internal.storage.pack.PackOutputStream; import org.eclipse.jgit.internal.storage.pack.StoredObjectRepresentation; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.util.BlockList; import org.eclipse.jgit.util.LongList; /** @@ -61,8 +73,18 @@ import org.eclipse.jgit.util.LongList; */ public final class DfsPackFile extends BlockBasedFile { private static final int REC_SIZE = Constants.OBJECT_ID_LENGTH + 8; + private static final long REF_POSITION = 0; + private static final Comparator<DfsObjectToPack> OFFSET_SORT = ( + DfsObjectToPack a, + DfsObjectToPack b) -> Long.signum(a.getOffset() - b.getOffset()); + + /** + * Loader for the default file-based {@link PackBitmapIndex} implementation. + */ + public static final PackBitmapIndexLoader DEFAULT_BITMAP_LOADER = new StreamPackBitmapIndexLoader(); + /** Index mapping {@link ObjectId} to position within the pack stream. */ private volatile PackIndex index; @@ -75,6 +97,13 @@ public final class DfsPackFile extends BlockBasedFile { /** Index of compressed commit graph mapping entire object graph. */ private volatile CommitGraph commitGraph; + private final PackBitmapIndexLoader bitmapLoader; + + /** Index by size */ + private boolean objectSizeIndexLoadAttempted; + + private volatile PackObjectSizeIndex objectSizeIndex; + /** * Objects we have tried to read, and discovered to be corrupt. * <p> @@ -87,6 +116,57 @@ public final class DfsPackFile extends BlockBasedFile { /** Lock for {@link #corruptObjects}. */ private final Object corruptObjectsLock = new Object(); + private final IndexFactory indexFactory; + + /** + * Returns the indexes for this pack. + * <p> + * We define indexes in different sub interfaces to allow implementing the + * indexes over different combinations of backends. + * <p> + * Implementations decide if/how to cache the indexes. The calling + * DfsPackFile will keep the reference to the index as long as it needs it. + */ + public interface IndexFactory { + /** + * Take care of loading the primary and reverse indexes for this pack. + */ + interface PackIndexes { + /** + * Load the primary index for the pack. + * + * @param ctx + * reader to find the raw bytes + * @return a primary index + * @throws IOException + * a problem finding/parsing the index + */ + PackIndex index(DfsReader ctx) throws IOException; + + /** + * Load the reverse index of the pack + * + * @param ctx + * reader to find the raw bytes + * @param idx + * primary index for this reverse index (probably loaded + * via #index(DfsReader)) + * @return the reverse index of the pack + * @throws IOException + * a problem finding/parsing the reverse index + */ + PackReverseIndex reverseIndex(DfsReader ctx, PackIndex idx) + throws IOException; + } + + /** + * Returns a provider of the primary and reverse indexes of this pack + * + * @return an implementation of the {@link PackIndexes} interface + */ + PackIndexes getPackIndexes(); + } + /** * Construct a reader for an existing, packfile. * @@ -96,6 +176,24 @@ public final class DfsPackFile extends BlockBasedFile { * description of the pack within the DFS. */ DfsPackFile(DfsBlockCache cache, DfsPackDescription desc) { + this(cache, desc, DEFAULT_BITMAP_LOADER, + new CachedStreamIndexFactory(cache, desc)); + } + + /** + * Create an instance of DfsPackFile with a custom bitmap loader + * + * @param cache + * cache that owns the pack data + * @param desc + * description of the pack within the DFS + * @param bitmapLoader + * loader to get the bitmaps of this pack (if any) + * @param indexFactory + * an IndexFactory to get references to the indexes of this pack + */ + public DfsPackFile(DfsBlockCache cache, DfsPackDescription desc, + PackBitmapIndexLoader bitmapLoader, IndexFactory indexFactory) { super(cache, desc, PACK); int bs = desc.getBlockSize(PACK); @@ -105,6 +203,9 @@ public final class DfsPackFile extends BlockBasedFile { long sz = desc.getFileSize(PACK); length = sz > 0 ? sz : -1; + + this.bitmapLoader = bitmapLoader; + this.indexFactory = indexFactory; } /** @@ -159,20 +260,12 @@ public final class DfsPackFile extends BlockBasedFile { Repository.getGlobalListenerList() .dispatch(new BeforeDfsPackIndexLoadedEvent(this)); try { - DfsStreamKey idxKey = desc.getStreamKey(INDEX); - AtomicBoolean cacheHit = new AtomicBoolean(true); - DfsBlockCache.Ref<PackIndex> idxref = cache.getOrLoadRef(idxKey, - REF_POSITION, () -> { - cacheHit.set(false); - return loadPackIndex(ctx, idxKey); - }); - if (cacheHit.get()) { - ctx.stats.idxCacheHit++; - } - PackIndex idx = idxref.get(); - if (index == null && idx != null) { - index = idx; + index = indexFactory.getPackIndexes().index(ctx); + if (index == null) { + throw new IOException( + "Couldn't get a reference to the primary index"); //$NON-NLS-1$ } + ctx.emitIndexLoad(desc, INDEX, index); return index; } catch (IOException e) { invalid = true; @@ -196,7 +289,7 @@ public final class DfsPackFile extends BlockBasedFile { * the bitmap index is not available, or is corrupt. */ public PackBitmapIndex getBitmapIndex(DfsReader ctx) throws IOException { - if (invalid || isGarbage() || !desc.hasFileExt(BITMAP_INDEX)) { + if (invalid || isGarbage() || !bitmapLoader.hasBitmaps(desc)) { return null; } @@ -204,6 +297,15 @@ public final class DfsPackFile extends BlockBasedFile { return bitmapIndex; } + if (!bitmapLoader.keepInDfs(desc)) { + PackBitmapIndexLoader.LoadResult result = bitmapLoader + .loadPackBitmapIndex(ctx, this); + if (bitmapIndex == null && result.bitmapIndex != null) { + bitmapIndex = result.bitmapIndex; + } + return bitmapIndex; + } + DfsStreamKey bitmapKey = desc.getStreamKey(BITMAP_INDEX); AtomicBoolean cacheHit = new AtomicBoolean(true); DfsBlockCache.Ref<PackBitmapIndex> idxref = cache @@ -218,6 +320,7 @@ public final class DfsPackFile extends BlockBasedFile { if (bitmapIndex == null && bmidx != null) { bitmapIndex = bmidx; } + ctx.emitIndexLoad(desc, BITMAP_INDEX, bitmapIndex); return bitmapIndex; } @@ -255,30 +358,71 @@ public final class DfsPackFile extends BlockBasedFile { if (commitGraph == null && cg != null) { commitGraph = cg; } + ctx.emitIndexLoad(desc, COMMIT_GRAPH, commitGraph); return commitGraph; } - PackReverseIndex getReverseIdx(DfsReader ctx) throws IOException { + /** + * Get the PackReverseIndex for this PackFile. + * + * @param ctx + * reader context to support reading from the backing store if + * the index is not already loaded in memory + * @return the PackReverseIndex + * @throws java.io.IOException + * the pack index is not available, or is corrupt + */ + public PackReverseIndex getReverseIdx(DfsReader ctx) throws IOException { if (reverseIndex != null) { return reverseIndex; } - PackIndex idx = idx(ctx); - DfsStreamKey revKey = desc.getStreamKey(REVERSE_INDEX); + reverseIndex = indexFactory.getPackIndexes().reverseIndex(ctx, idx(ctx)); + if (reverseIndex == null) { + throw new IOException( + "Couldn't get a reference to the reverse index"); //$NON-NLS-1$ + } + ctx.emitIndexLoad(desc, REVERSE_INDEX, reverseIndex); + return reverseIndex; + } + + private PackObjectSizeIndex getObjectSizeIndex(DfsReader ctx) + throws IOException { + if (objectSizeIndex != null) { + return objectSizeIndex; + } + + if (objectSizeIndexLoadAttempted + || !ctx.getOptions().shouldUseObjectSizeIndex() + || !desc.hasFileExt(OBJECT_SIZE_INDEX)) { + // Pack doesn't have object size index + return null; + } + + DfsStreamKey objSizeKey = desc.getStreamKey(OBJECT_SIZE_INDEX); AtomicBoolean cacheHit = new AtomicBoolean(true); - DfsBlockCache.Ref<PackReverseIndex> revref = cache.getOrLoadRef(revKey, - REF_POSITION, () -> { - cacheHit.set(false); - return loadReverseIdx(ctx, revKey, idx); - }); - if (cacheHit.get()) { - ctx.stats.ridxCacheHit++; + try { + DfsBlockCache.Ref<PackObjectSizeIndex> sizeIdxRef = cache + .getOrLoadRef(objSizeKey, REF_POSITION, () -> { + cacheHit.set(false); + return loadObjectSizeIndex(ctx, objSizeKey); + }); + if (cacheHit.get()) { + ctx.stats.objectSizeIndexCacheHit++; + } + PackObjectSizeIndex sizeIdx = sizeIdxRef.get(); + if (objectSizeIndex == null && sizeIdx != null) { + objectSizeIndex = sizeIdx; + } + } finally { + objectSizeIndexLoadAttempted = true; } - PackReverseIndex revidx = revref.get(); - if (reverseIndex == null && revidx != null) { - reverseIndex = revidx; + + // Object size index is optional, it can be null and that's fine + if (objectSizeIndex != null) { + ctx.emitIndexLoad(desc, OBJECT_SIZE_INDEX, objectSizeIndex); } - return reverseIndex; + return objectSizeIndex; } /** @@ -298,6 +442,10 @@ public final class DfsPackFile extends BlockBasedFile { return 0 < offset && !isCorrupt(offset); } + int findIdxPosition(DfsReader ctx, AnyObjectId id) throws IOException { + return idx(ctx).findPosition(id); + } + /** * Get an object from this pack. * @@ -320,23 +468,43 @@ public final class DfsPackFile extends BlockBasedFile { return idx(ctx).findOffset(id); } - void resolve(DfsReader ctx, Set<ObjectId> matches, AbbreviatedObjectId id, - int matchLimit) throws IOException { - idx(ctx).resolve(matches, id, matchLimit); - } - /** - * Obtain the total number of objects available in this pack. This method - * relies on pack index, giving number of effectively available objects. + * Return objects in the list available in this pack, sorted in (pack, + * offset) order. * * @param ctx - * current reader for the calling thread. - * @return number of objects in index of this pack, likewise in this pack + * a reader + * @param objects + * objects we are looking for + * @param skipFound + * ignore objects already found. + * @return list of objects with pack and offset set. * @throws IOException - * the index file cannot be loaded into memory. + * an error occurred */ - long getObjectCount(DfsReader ctx) throws IOException { - return idx(ctx).getObjectCount(); + List<DfsObjectToPack> findAllFromPack(DfsReader ctx, + Iterable<ObjectToPack> objects, boolean skipFound) + throws IOException { + List<DfsObjectToPack> tmp = new BlockList<>(); + for (ObjectToPack obj : objects) { + DfsObjectToPack otp = (DfsObjectToPack) obj; + if (skipFound && otp.isFound()) { + continue; + } + long p = idx(ctx).findOffset(otp); + if (p <= 0 || isCorrupt(p)) { + continue; + } + otp.setOffset(p); + tmp.add(otp); + } + Collections.sort(tmp, OFFSET_SORT); + return tmp; + } + + void resolve(DfsReader ctx, Set<ObjectId> matches, AbbreviatedObjectId id, + int matchLimit) throws IOException { + idx(ctx).resolve(matches, id, matchLimit); } private byte[] decompress(long position, int sz, DfsReader ctx) @@ -374,6 +542,7 @@ public final class DfsPackFile extends BlockBasedFile { if (sz > 0) { rc.setReadAheadBytes(sz); } + //TODO(ifrade): report ctx.emitBlockLoaded for this copy if (cache.shouldCopyThroughCache(length)) { copyPackThroughCache(out, ctx, rc); } else { @@ -399,6 +568,7 @@ public final class DfsPackFile extends BlockBasedFile { } } + @SuppressWarnings("ByteBufferBackingArray") private long copyPackBypassCache(PackOutputStream out, ReadableChannel rc) throws IOException { ByteBuffer buf = newCopyBuffer(out, rc); @@ -957,12 +1127,120 @@ public final class DfsPackFile extends BlockBasedFile { } } - void representation(DfsObjectRepresentation r, final long pos, + /** + * Return if this packfile has an object size index attached. + * + * This loads the index if it is not already in memory. + * + * @param ctx + * reader context to support reading from the backing store if + * the object size index is not already loaded in memory. + * @return true if the packfile has an object size index. + * @throws IOException + * a problem accessing storage while looking for the index + */ + boolean hasObjectSizeIndex(DfsReader ctx) throws IOException { + return getObjectSizeIndex(ctx) != null; + } + + /** + * Return minimum size for an object to be included in the object size + * index. + * + * Caller must make sure the pack has an object size index with + * {@link #hasObjectSizeIndex} before calling this method. + * + * @param ctx + * reader context to support reading from the backing store if + * the object size index is not already loaded in memory. + * @return minimum size for indexing in bytes + * @throws IOException + * no object size index or a problem loading it. + */ + int getObjectSizeIndexThreshold(DfsReader ctx) throws IOException { + PackObjectSizeIndex idx = getObjectSizeIndex(ctx); + if (idx == null) { + throw new IOException("Asking threshold of non-existing obj-size"); //$NON-NLS-1$ + } + return idx.getThreshold(); + } + + /** + * Return the size of the object from the object-size index. The object + * should be a blob. Any other type is not indexed and returns -1. + * <p> + * Caller MUST pass a valid index position, as returned by + * {@link #findIdxPosition(DfsReader, AnyObjectId)} and verify the pack has + * object size index (e.g. with {@link #hasObjectSizeIndex(DfsReader)}) + * before asking the indexed size. + * + * @param ctx + * reader context to support reading from the backing store if + * the object size index is not already loaded in memory. + * @param idxPosition + * position in the primary index of the object we are looking + * for, as returned by findIdxPosition + * @return size of the object from the index. Negative if object is not in + * the index (below threshold or not a blob) + * @throws IOException + * could not read the object size index. IO problem or the pack + * doesn't have it. + */ + long getIndexedObjectSize(DfsReader ctx, int idxPosition) + throws IOException { + if (idxPosition < 0) { + throw new IllegalArgumentException("Invalid index position"); //$NON-NLS-1$ + } + PackObjectSizeIndex sizeIdx = getObjectSizeIndex(ctx); + if (sizeIdx == null) { + throw new IllegalStateException( + "Asking indexed size from a pack without object size index"); //$NON-NLS-1$ + } + + return sizeIdx.getSize(idxPosition); + } + + /** + * Populates the representation object with the details of how the object at + * "pos" is stored in this pack (e.g. whole or deltified, its packed + * length). + * + * @param r + * represention object to carry data + * @param offset + * offset in this pack of the object + * @param ctx + * a reader + * @throws IOException + * an error reading the object from disk + */ + void fillRepresentation(DfsObjectRepresentation r, long offset, + DfsReader ctx) throws IOException { + fillRepresentation(r, offset, ctx, getReverseIdx(ctx)); + } + + /** + * Populates the representation object with the details of how the object at + * "pos" is stored in this pack (e.g. whole or deltified, its packed + * length). + * + * @param r + * represention object to carry data + * @param offset + * offset in this pack of the object + * @param ctx + * a reader + * @param rev + * reverse index of this pack + * @throws IOException + * an error reading the object from disk + */ + void fillRepresentation(DfsObjectRepresentation r, long offset, DfsReader ctx, PackReverseIndex rev) throws IOException { - r.offset = pos; + r.offset = offset; final byte[] ib = ctx.tempId; - readFully(pos, ib, 0, 20, ctx); + readFully(offset, ib, 0, 20, ctx); int c = ib[0] & 0xff; int p = 1; final int typeCode = (c >> 4) & 7; @@ -970,7 +1248,7 @@ public final class DfsPackFile extends BlockBasedFile { c = ib[p++] & 0xff; } - long len = rev.findNextOffset(pos, length - 20) - pos; + long len = rev.findNextOffset(offset, length - 20) - offset; switch (typeCode) { case Constants.OBJ_COMMIT: case Constants.OBJ_TREE: @@ -991,13 +1269,13 @@ public final class DfsPackFile extends BlockBasedFile { ofs += (c & 127); } r.format = StoredObjectRepresentation.PACK_DELTA; - r.baseId = rev.findObject(pos - ofs); + r.baseId = rev.findObject(offset - ofs); r.length = len - p; return; } case Constants.OBJ_REF_DELTA: { - readFully(pos + p, ib, 0, 20, ctx); + readFully(offset + p, ib, 0, 20, ctx); r.format = StoredObjectRepresentation.PACK_DELTA; r.baseId = ObjectId.fromRaw(ib); r.length = len - p - 20; @@ -1036,87 +1314,66 @@ public final class DfsPackFile extends BlockBasedFile { } } - private DfsBlockCache.Ref<PackIndex> loadPackIndex( - DfsReader ctx, DfsStreamKey idxKey) throws IOException { - try { - ctx.stats.readIdx++; - long start = System.nanoTime(); - try (ReadableChannel rc = ctx.db.openFile(desc, INDEX)) { - PackIndex idx = PackIndex.read(alignTo8kBlocks(rc)); - ctx.stats.readIdxBytes += rc.position(); - index = idx; - return new DfsBlockCache.Ref<>( - idxKey, - REF_POSITION, - idx.getObjectCount() * REC_SIZE, - idx); - } finally { - ctx.stats.readIdxMicros += elapsedMicros(start); + private DfsBlockCache.Ref<PackObjectSizeIndex> loadObjectSizeIndex( + DfsReader ctx, DfsStreamKey objectSizeIndexKey) throws IOException { + ctx.stats.readObjectSizeIndex++; + long start = System.nanoTime(); + long size = 0; + Exception parsingError = null; + try (ReadableChannel rc = ctx.db.openFile(desc, OBJECT_SIZE_INDEX)) { + try { + objectSizeIndex = PackObjectSizeIndexLoader + .load(Channels.newInputStream(rc)); + size = rc.position(); + } catch (IOException e) { + parsingError = e; } - } catch (EOFException e) { - throw new IOException(MessageFormat.format( - DfsText.get().shortReadOfIndex, - desc.getFileName(INDEX)), e); } catch (IOException e) { - throw new IOException(MessageFormat.format( - DfsText.get().cannotReadIndex, - desc.getFileName(INDEX)), e); + // No object size index in this pack + return new DfsBlockCache.Ref<>(objectSizeIndexKey, REF_POSITION, 0, + null); } - } - private DfsBlockCache.Ref<PackReverseIndex> loadReverseIdx( - DfsReader ctx, DfsStreamKey revKey, PackIndex idx) { - ctx.stats.readReverseIdx++; - long start = System.nanoTime(); - PackReverseIndex revidx = new PackReverseIndex(idx); - reverseIndex = revidx; - ctx.stats.readReverseIdxMicros += elapsedMicros(start); - return new DfsBlockCache.Ref<>( - revKey, - REF_POSITION, - idx.getObjectCount() * 8, - revidx); + if (parsingError != null) { + throw new IOException( + MessageFormat.format(DfsText.get().shortReadOfIndex, + desc.getFileName(OBJECT_SIZE_INDEX)), + parsingError); + } + + ctx.stats.readObjectSizeIndexBytes += size; + ctx.stats.readObjectSizeIndexMicros += elapsedMicros(start); + return new DfsBlockCache.Ref<>(objectSizeIndexKey, REF_POSITION, size, + objectSizeIndex); } private DfsBlockCache.Ref<PackBitmapIndex> loadBitmapIndex(DfsReader ctx, DfsStreamKey bitmapKey) throws IOException { ctx.stats.readBitmap++; long start = System.nanoTime(); - try (ReadableChannel rc = ctx.db.openFile(desc, BITMAP_INDEX)) { - long size; - PackBitmapIndex bmidx; - try { - bmidx = PackBitmapIndex.read(alignTo8kBlocks(rc), - () -> idx(ctx), () -> getReverseIdx(ctx), - ctx.getOptions().shouldLoadRevIndexInParallel()); - } finally { - size = rc.position(); - ctx.stats.readBitmapIdxBytes += size; - ctx.stats.readBitmapIdxMicros += elapsedMicros(start); - } - bitmapIndex = bmidx; - return new DfsBlockCache.Ref<>( - bitmapKey, REF_POSITION, size, bmidx); - } catch (EOFException e) { - throw new IOException(MessageFormat.format( - DfsText.get().shortReadOfIndex, - desc.getFileName(BITMAP_INDEX)), e); - } catch (IOException e) { - throw new IOException(MessageFormat.format( - DfsText.get().cannotReadIndex, - desc.getFileName(BITMAP_INDEX)), e); - } + PackBitmapIndexLoader.LoadResult result = bitmapLoader + .loadPackBitmapIndex(ctx, this); + bitmapIndex = result.bitmapIndex; + ctx.stats.readBitmapIdxBytes += result.bytesRead; + ctx.stats.readBitmapIdxMicros += elapsedMicros(start); + return new DfsBlockCache.Ref<>(bitmapKey, REF_POSITION, + result.bytesRead, result.bitmapIndex); } private DfsBlockCache.Ref<CommitGraph> loadCommitGraph(DfsReader ctx, DfsStreamKey cgkey) throws IOException { ctx.stats.readCommitGraph++; long start = System.nanoTime(); + StoredConfig repoConfig = ctx.db.getRepository().getConfig(); + boolean readChangedPathFilters = repoConfig.getBoolean( + ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION, + ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS, false); try (ReadableChannel rc = ctx.db.openFile(desc, COMMIT_GRAPH)) { long size; CommitGraph cg; try { - cg = CommitGraphLoader.read(alignTo8kBlocks(rc)); + cg = CommitGraphLoader.read(alignTo8kBlocks(rc), + readChangedPathFilters); } finally { size = rc.position(); ctx.stats.readCommitGraphBytes += size; @@ -1146,4 +1403,252 @@ public final class DfsPackFile extends BlockBasedFile { } return new BufferedInputStream(in, bs); } + + /** + * Loads the PackBitmapIndex associated with this packfile + */ + public interface PackBitmapIndexLoader { + /** + * Does this pack has bitmaps associated? + * + * @param desc + * the pack + * @return true if the pack has bitmaps + */ + boolean hasBitmaps(DfsPackDescription desc); + + /** + * If the returned instance must be kept in DFS cache + * + * It should be true when the instance is expensive to load and can be + * reused. + * + * @param desc + * the pack + * @return true if the returned bitmap index should be kept in DFS + */ + boolean keepInDfs(DfsPackDescription desc); + + /** + * Returns a PackBitmapIndex for this pack, if the pack has bitmaps + * associated. + * + * @param ctx + * the reader + * @param pack + * the pack + * @return the pack bitmap index and bytes size (when applicable) + * @throws IOException + * error accessing storage + */ + LoadResult loadPackBitmapIndex(DfsReader ctx, DfsPackFile pack) + throws IOException; + + /** + * The instance of the pack bitmap index and the amount of bytes loaded. + * + * The bytes can be 0, if the implementation doesn't do any initial + * loading. + */ + public class LoadResult { + /** The loaded {@link PackBitmapIndex}. */ + public final PackBitmapIndex bitmapIndex; + + /** The bytes read upon initial load (may be 0). */ + public final long bytesRead; + + /** + * Constructs the LoadResult. + * + * @param packBitmapIndex + * the loaded index. + * @param bytesRead + * the bytes read upon loading. + */ + public LoadResult(PackBitmapIndex packBitmapIndex, long bytesRead) { + this.bitmapIndex = packBitmapIndex; + this.bytesRead = bytesRead; + } + } + } + + /** + * Load the packbitmapindex from the BITMAP_INDEX pack extension + */ + private static final class StreamPackBitmapIndexLoader implements PackBitmapIndexLoader { + @Override + public boolean hasBitmaps(DfsPackDescription desc) { + return desc.hasFileExt(BITMAP_INDEX); + } + + @Override + public boolean keepInDfs(DfsPackDescription desc) { + return true; + } + + @Override + public LoadResult loadPackBitmapIndex(DfsReader ctx, DfsPackFile pack) + throws IOException { + DfsPackDescription desc = pack.getPackDescription(); + try (ReadableChannel rc = ctx.db.openFile(desc, BITMAP_INDEX)) { + long size; + PackBitmapIndex bmidx; + try { + bmidx = PackBitmapIndex.read(alignTo8kBlocks(rc), + () -> pack.idx(ctx), () -> pack.getReverseIdx(ctx), + ctx.getOptions().shouldLoadRevIndexInParallel()); + } finally { + size = rc.position(); + } + return new LoadResult(bmidx, size); + } catch (EOFException e) { + throw new IOException( + MessageFormat.format(DfsText.get().shortReadOfIndex, + desc.getFileName(BITMAP_INDEX)), + e); + } catch (IOException e) { + throw new IOException( + MessageFormat.format(DfsText.get().cannotReadIndex, + desc.getFileName(BITMAP_INDEX)), + e); + } + } + } + + /** + * An index factory backed by Dfs streams and references cached in + * DfsBlockCache + */ + public static final class CachedStreamIndexFactory implements IndexFactory { + private final CachedStreamPackIndexes indexes; + + /** + * An index factory + * + * @param cache + * DFS block cache to use for the references + * @param desc + * This factory loads indexes for this package + */ + public CachedStreamIndexFactory(DfsBlockCache cache, + DfsPackDescription desc) { + this.indexes = new CachedStreamPackIndexes(cache, desc); + } + + @Override + public PackIndexes getPackIndexes() { + return indexes; + } + } + + /** + * Load primary and reverse index from Dfs streams and cache the references + * in DfsBlockCache. + */ + public static final class CachedStreamPackIndexes implements IndexFactory.PackIndexes { + private final DfsBlockCache cache; + + private final DfsPackDescription desc; + + /** + * An index factory + * + * @param cache + * DFS block cache to use for the references + * @param desc This factory loads indexes for this package + */ + public CachedStreamPackIndexes(DfsBlockCache cache, + DfsPackDescription desc) { + this.cache = cache; + this.desc = desc; + } + + @Override + public PackIndex index(DfsReader ctx) throws IOException { + DfsStreamKey idxKey = desc.getStreamKey(INDEX); + // Keep the value parsed in the loader, in case the Ref<> is + // nullified in ClockBlockCacheTable#reserveSpace + // before we read its value. + AtomicReference<PackIndex> loadedRef = new AtomicReference<>(null); + DfsBlockCache.Ref<PackIndex> cachedRef = cache.getOrLoadRef(idxKey, + REF_POSITION, () -> { + RefWithSize<PackIndex> idx = loadPackIndex(ctx, desc); + loadedRef.set(idx.ref); + return new DfsBlockCache.Ref<>(idxKey, REF_POSITION, + idx.size, idx.ref); + }); + if (loadedRef.get() == null) { + ctx.stats.idxCacheHit++; + } + return cachedRef.get() != null ? cachedRef.get() : loadedRef.get(); + } + + private static RefWithSize<PackIndex> loadPackIndex(DfsReader ctx, + DfsPackDescription desc) throws IOException { + try { + ctx.stats.readIdx++; + long start = System.nanoTime(); + try (ReadableChannel rc = ctx.db.openFile(desc, INDEX)) { + PackIndex idx = PackIndex.read(alignTo8kBlocks(rc)); + ctx.stats.readIdxBytes += rc.position(); + return new RefWithSize<>(idx, + idx.getObjectCount() * REC_SIZE); + } finally { + ctx.stats.readIdxMicros += elapsedMicros(start); + } + } catch (EOFException e) { + throw new IOException( + MessageFormat.format(DfsText.get().shortReadOfIndex, + desc.getFileName(INDEX)), + e); + } catch (IOException e) { + throw new IOException( + MessageFormat.format(DfsText.get().cannotReadIndex, + desc.getFileName(INDEX)), + e); + } + } + + @Override + public PackReverseIndex reverseIndex(DfsReader ctx, PackIndex idx) + throws IOException { + DfsStreamKey revKey = desc.getStreamKey(REVERSE_INDEX); + // Keep the value parsed in the loader, in case the Ref<> is + // nullified in ClockBlockCacheTable#reserveSpace + // before we read its value. + AtomicReference<PackReverseIndex> loadedRef = new AtomicReference<>( + null); + DfsBlockCache.Ref<PackReverseIndex> cachedRef = cache + .getOrLoadRef(revKey, REF_POSITION, () -> { + RefWithSize<PackReverseIndex> ridx = loadReverseIdx(ctx, + idx); + loadedRef.set(ridx.ref); + return new DfsBlockCache.Ref<>(revKey, REF_POSITION, + ridx.size, ridx.ref); + }); + if (loadedRef.get() == null) { + ctx.stats.ridxCacheHit++; + } + return cachedRef.get() != null ? cachedRef.get() : loadedRef.get(); + } + + private static RefWithSize<PackReverseIndex> loadReverseIdx( + DfsReader ctx, PackIndex idx) { + ctx.stats.readReverseIdx++; + long start = System.nanoTime(); + PackReverseIndex revidx = PackReverseIndexFactory + .computeFromIndex(idx); + ctx.stats.readReverseIdxMicros += elapsedMicros(start); + return new RefWithSize<>(revidx, idx.getObjectCount() * 8); + } + } + + private static final class RefWithSize<V> { + final V ref; + final long size; + RefWithSize(V ref, long size) { + this.ref = ref; + this.size = size; + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java index d8e191c4e0..b19f65c69b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java @@ -101,7 +101,6 @@ public class DfsPackParser extends PackParser { this.packDigest = Constants.newMessageDigest(); } - /** {@inheritDoc} */ @Override public PackLock parse(ProgressMonitor receiving, ProgressMonitor resolving) throws IOException { @@ -123,10 +122,11 @@ public class DfsPackParser extends PackParser { packDsc.setBlockSize(PACK, blockSize); writePackIndex(); + objins.writeObjectSizeIndex(packDsc, getSortedObjectList(null)); objdb.commitPack(Collections.singletonList(packDsc), null); rollback = false; - DfsPackFile p = new DfsPackFile(blockCache, packDsc); + DfsPackFile p = objdb.createDfsPackFile(blockCache, packDsc); p.setBlockSize(blockSize); if (packIndex != null) p.setPackIndex(packIndex); @@ -172,7 +172,6 @@ public class DfsPackParser extends PackParser { return packDsc; } - /** {@inheritDoc} */ @Override protected void onPackHeader(long objectCount) throws IOException { if (objectCount == 0) { @@ -194,34 +193,29 @@ public class DfsPackParser extends PackParser { currBuf = new byte[blockSize]; } - /** {@inheritDoc} */ @Override protected void onBeginWholeObject(long streamPosition, int type, long inflatedSize) throws IOException { crc.reset(); } - /** {@inheritDoc} */ @Override protected void onEndWholeObject(PackedObjectInfo info) throws IOException { info.setCRC((int) crc.getValue()); } - /** {@inheritDoc} */ @Override protected void onBeginOfsDelta(long streamPosition, long baseStreamPosition, long inflatedSize) throws IOException { crc.reset(); } - /** {@inheritDoc} */ @Override protected void onBeginRefDelta(long streamPosition, AnyObjectId baseId, long inflatedSize) throws IOException { crc.reset(); } - /** {@inheritDoc} */ @Override protected UnresolvedDelta onEndDelta() throws IOException { UnresolvedDelta delta = new UnresolvedDelta(); @@ -229,28 +223,24 @@ public class DfsPackParser extends PackParser { return delta; } - /** {@inheritDoc} */ @Override protected void onInflatedObjectData(PackedObjectInfo obj, int typeCode, byte[] data) throws IOException { // DfsPackParser ignores this event. } - /** {@inheritDoc} */ @Override protected void onObjectHeader(Source src, byte[] raw, int pos, int len) throws IOException { crc.update(raw, pos, len); } - /** {@inheritDoc} */ @Override protected void onObjectData(Source src, byte[] raw, int pos, int len) throws IOException { crc.update(raw, pos, len); } - /** {@inheritDoc} */ @Override protected void onStoreStream(byte[] raw, int pos, int len) throws IOException { @@ -297,7 +287,6 @@ public class DfsPackParser extends PackParser { return v; } - /** {@inheritDoc} */ @Override protected void onPackFooter(byte[] hash) throws IOException { // The base class will validate the original hash matches @@ -307,7 +296,6 @@ public class DfsPackParser extends PackParser { packHash = hash; } - /** {@inheritDoc} */ @Override protected ObjectTypeAndSize seekDatabase(PackedObjectInfo obj, ObjectTypeAndSize info) throws IOException { @@ -316,7 +304,6 @@ public class DfsPackParser extends PackParser { return readObjectHeader(info); } - /** {@inheritDoc} */ @Override protected ObjectTypeAndSize seekDatabase(UnresolvedDelta delta, ObjectTypeAndSize info) throws IOException { @@ -325,7 +312,6 @@ public class DfsPackParser extends PackParser { return readObjectHeader(info); } - /** {@inheritDoc} */ @Override protected int readDatabase(byte[] dst, int pos, int cnt) throws IOException { if (cnt == 0) @@ -381,13 +367,11 @@ public class DfsPackParser extends PackParser { return (pos / blockSize) * blockSize; } - /** {@inheritDoc} */ @Override protected boolean checkCRC(int oldCRC) { return oldCRC == (int) crc.getValue(); } - /** {@inheritDoc} */ @Override protected boolean onAppendBase(final int typeCode, final byte[] data, final PackedObjectInfo info) throws IOException { @@ -427,7 +411,6 @@ public class DfsPackParser extends PackParser { return true; } - /** {@inheritDoc} */ @Override protected void onEndThinPack() throws IOException { // Normally when a thin pack is closed the pack header gets diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPacksChangedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPacksChangedEvent.java index 750c118268..f4e863d02d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPacksChangedEvent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPacksChangedEvent.java @@ -18,13 +18,11 @@ import org.eclipse.jgit.events.RepositoryEvent; */ public class DfsPacksChangedEvent extends RepositoryEvent<DfsPacksChangedListener> { - /** {@inheritDoc} */ @Override public Class<DfsPacksChangedListener> getListenerType() { return DfsPacksChangedListener.class; } - /** {@inheritDoc} */ @Override public void dispatch(DfsPacksChangedListener listener) { listener.onPacksChanged(this); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java index 8d8a766b0f..f50cd597e5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java @@ -21,26 +21,27 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.zip.DataFormatException; import java.util.zip.Inflater; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.StoredObjectRepresentationNotAvailableException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph; import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackList; +import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; +import org.eclipse.jgit.internal.storage.dfs.DfsReader.PackLoadListener.DfsBlockData; import org.eclipse.jgit.internal.storage.file.BitmapIndexImpl; import org.eclipse.jgit.internal.storage.file.PackBitmapIndex; -import org.eclipse.jgit.internal.storage.file.PackIndex; -import org.eclipse.jgit.internal.storage.file.PackReverseIndex; import org.eclipse.jgit.internal.storage.pack.CachedPack; import org.eclipse.jgit.internal.storage.pack.ObjectReuseAsIs; import org.eclipse.jgit.internal.storage.pack.ObjectToPack; +import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.pack.PackOutputStream; import org.eclipse.jgit.internal.storage.pack.PackWriter; import org.eclipse.jgit.lib.AbbreviatedObjectId; @@ -49,12 +50,12 @@ import org.eclipse.jgit.lib.AsyncObjectLoaderQueue; import org.eclipse.jgit.lib.AsyncObjectSizeQueue; import org.eclipse.jgit.lib.BitmapIndex; import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.InflaterCache; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ProgressMonitor; -import org.eclipse.jgit.util.BlockList; /** * Reader to access repository content through. @@ -78,6 +79,7 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { private DeltaBaseCache baseCache; private DfsPackFile last; private boolean avoidUnreachable; + private List<PackLoadListener> packLoadListeners = new ArrayList<>(); /** * Initialize a new DfsReader @@ -100,30 +102,38 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { return baseCache; } - /** {@inheritDoc} */ @Override public ObjectReader newReader() { return db.newReader(); } - /** {@inheritDoc} */ @Override public void setAvoidUnreachableObjects(boolean avoid) { avoidUnreachable = avoid; } - /** {@inheritDoc} */ @Override public BitmapIndex getBitmapIndex() throws IOException { for (DfsPackFile pack : db.getPacks()) { PackBitmapIndex bitmapIndex = pack.getBitmapIndex(this); if (bitmapIndex != null) - return new BitmapIndexImpl(bitmapIndex); + return createBitmapIndex(bitmapIndex); } return null; } - /** {@inheritDoc} */ + /** + * Give subclasses a chance to record pack index stats + * + * @param packBitmapIndex + * packBitmapIndex found in a pack (never null) + * @return an instance of BitmapIndex + */ + protected BitmapIndex createBitmapIndex( + @NonNull PackBitmapIndex packBitmapIndex) { + return new BitmapIndexImpl(packBitmapIndex); + } + @Override public Optional<CommitGraph> getCommitGraph() throws IOException { for (DfsPackFile pack : db.getPacks()) { @@ -135,7 +145,6 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { return Optional.empty(); } - /** {@inheritDoc} */ @Override public Collection<CachedPack> getCachedPacksAndUpdate( BitmapBuilder needBitmap) throws IOException { @@ -148,7 +157,6 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { return Collections.emptyList(); } - /** {@inheritDoc} */ @Override public Collection<ObjectId> resolve(AbbreviatedObjectId id) throws IOException { @@ -177,37 +185,48 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { } } - /** {@inheritDoc} */ @Override public boolean has(AnyObjectId objectId) throws IOException { + return findPack(objectId) >= 0; + } + + private int findPack(AnyObjectId objectId) throws IOException { if (last != null - && !skipGarbagePack(last) - && last.hasObject(this, objectId)) - return true; + && !skipGarbagePack(last)) { + int idxPos = last.findIdxPosition(this, objectId); + if (idxPos >= 0) { + return idxPos; + } + } + PackList packList = db.getPackList(); - if (hasImpl(packList, objectId)) { - return true; + int idxPos = findInPackList(packList, objectId); + if (idxPos >= 0) { + return idxPos; } else if (packList.dirty()) { stats.scanPacks++; - return hasImpl(db.scanPacks(packList), objectId); + idxPos = findInPackList(db.scanPacks(packList), objectId); + return idxPos; } - return false; + return -1; } - private boolean hasImpl(PackList packList, AnyObjectId objectId) + // Leave "last" pointing to the pack and return the idx position of the + // object (-1 if not found) + private int findInPackList(PackList packList, AnyObjectId objectId) throws IOException { for (DfsPackFile pack : packList.packs) { if (pack == last || skipGarbagePack(pack)) continue; - if (pack.hasObject(this, objectId)) { + int idxPos = pack.findIdxPosition(this, objectId); + if (idxPos >= 0) { last = pack; - return true; + return idxPos; } } - return false; + return -1; } - /** {@inheritDoc} */ @Override public ObjectLoader open(AnyObjectId objectId, int typeHint) throws MissingObjectException, IncorrectObjectTypeException, @@ -262,7 +281,6 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { return null; } - /** {@inheritDoc} */ @Override public Set<ObjectId> getShallowCommits() { return Collections.emptySet(); @@ -299,7 +317,7 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { private <T extends ObjectId> Iterable<FoundObject<T>> findAll( Iterable<T> objectIds) throws IOException { - Collection<T> pending = new LinkedList<>(); + HashSet<T> pending = new HashSet<>(); for (T id : objectIds) { pending.add(id); } @@ -319,22 +337,21 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { } private <T extends ObjectId> void findAllImpl(PackList packList, - Collection<T> pending, List<FoundObject<T>> r) { + HashSet<T> pending, List<FoundObject<T>> r) { DfsPackFile[] packs = packList.packs; if (packs.length == 0) { return; } int lastIdx = 0; DfsPackFile lastPack = packs[lastIdx]; - - OBJECT_SCAN: for (Iterator<T> it = pending.iterator(); it.hasNext();) { - T t = it.next(); + HashSet<T> toRemove = new HashSet<>(); + OBJECT_SCAN: for (T t : pending) { if (!skipGarbagePack(lastPack)) { try { long p = lastPack.findOffset(this, t); if (0 < p) { r.add(new FoundObject<>(t, lastIdx, lastPack, p)); - it.remove(); + toRemove.add(t); continue; } } catch (IOException e) { @@ -352,7 +369,7 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { long p = pack.findOffset(this, t); if (0 < p) { r.add(new FoundObject<>(t, i, pack, p)); - it.remove(); + toRemove.add(t); lastIdx = i; lastPack = pack; continue OBJECT_SCAN; @@ -362,6 +379,7 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { } } } + pending.removeAll(toRemove); last = lastPack; } @@ -370,7 +388,6 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { return avoidUnreachable && pack.isGarbage(); } - /** {@inheritDoc} */ @Override public <T extends ObjectId> AsyncObjectLoaderQueue<T> open( Iterable<T> objectIds, final boolean reportMissing) { @@ -430,7 +447,6 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { }; } - /** {@inheritDoc} */ @Override public <T extends ObjectId> AsyncObjectSizeQueue<T> getObjectSize( Iterable<T> objectIds, final boolean reportMissing) { @@ -492,62 +508,102 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { }; } - /** {@inheritDoc} */ @Override public long getObjectSize(AnyObjectId objectId, int typeHint) throws MissingObjectException, IncorrectObjectTypeException, IOException { - if (last != null && !skipGarbagePack(last)) { - long sz = last.getObjectSize(this, objectId); - if (0 <= sz) { - return sz; + int idxPos = findPack(objectId); + if (idxPos < 0) { + if (typeHint == OBJ_ANY) { + throw new MissingObjectException(objectId.copy(), + JGitText.get().unknownObjectType2); } + throw new MissingObjectException(objectId.copy(), typeHint); } - PackList packList = db.getPackList(); - long sz = getObjectSizeImpl(packList, objectId); - if (0 <= sz) { + if (typeHint != Constants.OBJ_BLOB || !safeHasObjectSizeIndex(last)) { + return last.getObjectSize(this, objectId); + } + + long sz = safeGetIndexedObjectSize(last, idxPos); + if (sz >= 0) { return sz; } - if (packList.dirty()) { - sz = getObjectSizeImpl(packList, objectId); - if (0 <= sz) { - return sz; + return last.getObjectSize(this, objectId); + } + + + @Override + public boolean isNotLargerThan(AnyObjectId objectId, int typeHint, + long limit) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + int idxPos = findPack(objectId); + if (idxPos < 0) { + if (typeHint == OBJ_ANY) { + throw new MissingObjectException(objectId.copy(), + JGitText.get().unknownObjectType2); } + throw new MissingObjectException(objectId.copy(), typeHint); } - if (typeHint == OBJ_ANY) { - throw new MissingObjectException(objectId.copy(), - JGitText.get().unknownObjectType2); + stats.isNotLargerThanCallCount += 1; + if (typeHint != Constants.OBJ_BLOB || !safeHasObjectSizeIndex(last)) { + return last.getObjectSize(this, objectId) <= limit; } - throw new MissingObjectException(objectId.copy(), typeHint); + + long sz = safeGetIndexedObjectSize(last, idxPos); + if (sz >= 0) { + return sz <= limit; + } + + if (isLimitInsideIndexThreshold(last, limit)) { + // With threshold T, not-found means object < T + // If limit L > T, then object < T < L + return true; + } + + return last.getObjectSize(this, objectId) <= limit; } - private long getObjectSizeImpl(PackList packList, AnyObjectId objectId) - throws IOException { - for (DfsPackFile pack : packList.packs) { - if (pack == last || skipGarbagePack(pack)) { - continue; - } - long sz = pack.getObjectSize(this, objectId); - if (0 <= sz) { - last = pack; - return sz; - } + private boolean safeHasObjectSizeIndex(DfsPackFile pack) { + try { + return pack.hasObjectSizeIndex(this); + } catch (IOException e) { + return false; + } + } + + private long safeGetIndexedObjectSize(DfsPackFile pack, + int idxPos) { + long sz; + try { + sz = pack.getIndexedObjectSize(this, idxPos); + } catch (IOException e) { + // If there is any error in the index, we should have seen it + // on hasObjectSizeIndex. + throw new IllegalStateException(e); + } + if (sz < 0) { + stats.objectSizeIndexMiss += 1; + } else { + stats.objectSizeIndexHit += 1; + } + return sz; + } + + private boolean isLimitInsideIndexThreshold(DfsPackFile pack, long limit) { + try { + return pack.getObjectSizeIndexThreshold(this) <= limit; + } catch (IOException e) { + return false; } - return -1; } - /** {@inheritDoc} */ @Override public DfsObjectToPack newObjectToPack(AnyObjectId objectId, int type) { return new DfsObjectToPack(objectId, type); } - private static final Comparator<DfsObjectToPack> OFFSET_SORT = ( - DfsObjectToPack a, - DfsObjectToPack b) -> Long.signum(a.getOffset() - b.getOffset()); - @Override public void selectObjectRepresentation(PackWriter packer, ProgressMonitor monitor, Iterable<ObjectToPack> objects) @@ -567,16 +623,15 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { ProgressMonitor monitor, Iterable<ObjectToPack> objects, List<DfsPackFile> packs, boolean skipFound) throws IOException { for (DfsPackFile pack : packs) { - List<DfsObjectToPack> tmp = findAllFromPack(pack, objects, skipFound); - if (tmp.isEmpty()) + List<DfsObjectToPack> inPack = pack.findAllFromPack(this, objects, skipFound); + if (inPack.isEmpty()) continue; - Collections.sort(tmp, OFFSET_SORT); - PackReverseIndex rev = pack.getReverseIdx(this); DfsObjectRepresentation rep = new DfsObjectRepresentation(pack); - for (DfsObjectToPack otp : tmp) { - pack.representation(rep, otp.getOffset(), this, rev); + for (DfsObjectToPack otp : inPack) { + // Populate rep.{offset,length} from the pack + pack.fillRepresentation(rep, otp.getOffset(), this); otp.setOffset(0); - packer.select(otp, rep); + packer.select(otp, rep); // Set otp.offset from rep if (!otp.isFound()) { otp.setFound(); monitor.update(1); @@ -623,26 +678,8 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { return false; } - private List<DfsObjectToPack> findAllFromPack(DfsPackFile pack, - Iterable<ObjectToPack> objects, boolean skipFound) - throws IOException { - List<DfsObjectToPack> tmp = new BlockList<>(); - PackIndex idx = pack.getPackIndex(this); - for (ObjectToPack obj : objects) { - DfsObjectToPack otp = (DfsObjectToPack) obj; - if (skipFound && otp.isFound()) { - continue; - } - long p = idx.findOffset(otp); - if (0 < p && !pack.isCorrupt(p)) { - otp.setOffset(p); - tmp.add(otp); - } - } - return tmp; - } - /** {@inheritDoc} */ + @Override public void copyObjectAsIs(PackOutputStream out, ObjectToPack otp, boolean validate) throws IOException, @@ -651,7 +688,6 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { src.pack.copyAsIs(out, src, validate, this); } - /** {@inheritDoc} */ @Override public void writeObjects(PackOutputStream out, List<ObjectToPack> list) throws IOException { @@ -659,7 +695,6 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { out.writeObject(otp); } - /** {@inheritDoc} */ @Override public void copyPackAsIs(PackOutputStream out, CachedPack pack) throws IOException { @@ -796,6 +831,100 @@ public class DfsReader extends ObjectReader implements ObjectReuseAsIs { return new DfsReaderIoStats(stats); } + /** Announces when data is loaded by reader */ + protected interface PackLoadListener { + /** + * Immutable copy of a DFS block metadata + */ + class DfsBlockData { + private final int identityHash; + private final int size; + + static DfsBlockData of(DfsBlock src) { + return new DfsBlockData(src); + } + + private DfsBlockData(DfsBlock src) { + this.identityHash = System.identityHashCode(src); + this.size = src.size(); + } + + public int getIdentityHash() { + return identityHash; + } + + public int getSize() { + return size; + } + } + + /** + * This is called when an index reference (e.g. primary index, reverse + * index, ...) is set in the reader, regarless if loaded from scratch or + * copied from cache. + * + * During the lifetime of the reader, the reference for an index should + * be set only once. + * + * @param packName + * Name of the pack + * @param src + * Source of the pack (e.g. GC, COMPACT, ...) + * @param ext + * Extension in the pack (e.g. IDX, RIDX, ...) + * @param size + * Size of the data loaded (usually as bytes in disk) + * @param loadedIdx + * reference to the loaded index + */ + void onIndexLoad(String packName, PackSource src, PackExt ext, long size, + Object loadedIdx); + + /** + * This is called when a dfs block is loaded into the reader. + * + * The reader keeps only one block at a time in memory, so during a + * request the same block could be loaded multiple times. + * + * @param packName + * Name of the pack this block belongs to + * @param src + * Source of the pack (e.g. GC, COMPACT, ...) + * @param ext + * Extension in the pack (e.g. PACK or REFTABLE) + * @param position + * Offset in the file requested by caller + * @param dfsBlockData + * Metadata of the block + */ + void onBlockLoad(String packName, PackSource src, PackExt ext, + long position, DfsBlockData dfsBlockData); + } + + void emitIndexLoad(DfsPackDescription packDescription, PackExt ext, + Object loadedIdx) { + packLoadListeners.forEach( + listener -> listener.onIndexLoad(packDescription.getFileName(ext), + packDescription.getPackSource(), ext, + packDescription.getFileSize(ext), loadedIdx)); + } + + void emitBlockLoad(BlockBasedFile file, long position, DfsBlock dfsBlock) { + packLoadListeners + .forEach(listener -> listener.onBlockLoad(file.getFileName(), + file.desc.getPackSource(), file.ext, position, + DfsBlockData.of(dfsBlock))); + } + + /** + * Add listener to record loads by this reader + * + * @param listener a listener + */ + protected void addPackLoadListener(PackLoadListener listener) { + packLoadListeners.add(listener); + } + /** * {@inheritDoc} * <p> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java index 5ac7985e97..fcfa3e0dee 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java @@ -10,12 +10,15 @@ package org.eclipse.jgit.internal.storage.dfs; +import org.eclipse.jgit.lib.AnyObjectId; + /** * IO statistics for a {@link org.eclipse.jgit.internal.storage.dfs.DfsReader}. */ public class DfsReaderIoStats { /** POJO to accumulate IO statistics. */ public static class Accumulator { + /** Number of times the reader explicitly called scanPacks. */ long scanPacks; @@ -31,38 +34,50 @@ public class DfsReaderIoStats { /** Total number of cache hits for commit graphs. */ long commitGraphCacheHit; + /** Total number of cache hits for object size indexes. */ + long objectSizeIndexCacheHit; + /** Total number of complete pack indexes read into memory. */ long readIdx; - /** Total number of complete bitmap indexes read into memory. */ - long readBitmap; - /** Total number of reverse indexes added into memory. */ long readReverseIdx; + /** Total number of complete bitmap indexes read into memory. */ + long readBitmap; + /** Total number of complete commit graphs read into memory. */ long readCommitGraph; + /** Total number of object size indexes added into memory. */ + long readObjectSizeIndex; + /** Total number of bytes read from pack indexes. */ long readIdxBytes; + /** Total number of bytes read from bitmap indexes. */ + long readBitmapIdxBytes; + /** Total number of bytes read from commit graphs. */ long readCommitGraphBytes; + /** Total numer of bytes read from object size index */ + long readObjectSizeIndexBytes; + /** Total microseconds spent reading pack indexes. */ long readIdxMicros; /** Total microseconds spent creating reverse indexes. */ long readReverseIdxMicros; + /** Total microseconds spent reading bitmap indexes. */ + long readBitmapIdxMicros; + /** Total microseconds spent creating commit graphs. */ long readCommitGraphMicros; - /** Total number of bytes read from bitmap indexes. */ - long readBitmapIdxBytes; - - /** Total microseconds spent reading bitmap indexes. */ - long readBitmapIdxMicros; + /** Total microseconds spent creating object size indexes */ + long readObjectSizeIndexMicros; /** Total number of block cache hits. */ long blockCacheHit; @@ -88,6 +103,15 @@ public class DfsReaderIoStats { /** Total microseconds spent inflating compressed bytes. */ long inflationMicros; + /** Count of queries for the size of an object via #isNotLargerThan */ + long isNotLargerThanCallCount; + + /** Object was below threshold in the object size index */ + long objectSizeIndexMiss; + + /** Object size found in the object size index */ + long objectSizeIndexHit; + Accumulator() { } } @@ -144,6 +168,15 @@ public class DfsReaderIoStats { } /** + * Get total number of object size index cache hits. + * + * @return total number of object size index cache hits. + */ + public long getObjectSizeIndexCacheHits() { + return stats.objectSizeIndexCacheHit; + } + + /** * Get total number of complete pack indexes read into memory. * * @return total number of complete pack indexes read into memory. @@ -162,6 +195,15 @@ public class DfsReaderIoStats { } /** + * Get total number of complete bitmap indexes read into memory. + * + * @return total number of complete bitmap indexes read into memory. + */ + public long getReadBitmapIndexCount() { + return stats.readBitmap; + } + + /** * Get total number of times the commit graph read into memory. * * @return total number of commit graph read into memory. @@ -171,12 +213,12 @@ public class DfsReaderIoStats { } /** - * Get total number of complete bitmap indexes read into memory. + * Get total number of complete object size indexes read into memory. * - * @return total number of complete bitmap indexes read into memory. + * @return total number of complete object size indexes read into memory. */ - public long getReadBitmapIndexCount() { - return stats.readBitmap; + public long getReadObjectSizeIndexCount() { + return stats.readObjectSizeIndex; } /** @@ -189,6 +231,15 @@ public class DfsReaderIoStats { } /** + * Get total number of bytes read from bitmap indexes. + * + * @return total number of bytes read from bitmap indexes. + */ + public long getReadBitmapIndexBytes() { + return stats.readBitmapIdxBytes; + } + + /** * Get total number of bytes read from commit graphs. * * @return total number of bytes read from commit graphs. @@ -198,6 +249,15 @@ public class DfsReaderIoStats { } /** + * Get total number of bytes read from object size indexes. + * + * @return total number of bytes read from object size indexes. + */ + public long getObjectSizeIndexBytes() { + return stats.readObjectSizeIndexBytes; + } + + /** * Get total microseconds spent reading pack indexes. * * @return total microseconds spent reading pack indexes. @@ -216,30 +276,30 @@ public class DfsReaderIoStats { } /** - * Get total microseconds spent reading commit graphs. + * Get total microseconds spent reading bitmap indexes. * - * @return total microseconds spent reading commit graphs. + * @return total microseconds spent reading bitmap indexes. */ - public long getReadCommitGraphMicros() { - return stats.readCommitGraphMicros; + public long getReadBitmapIndexMicros() { + return stats.readBitmapIdxMicros; } /** - * Get total number of bytes read from bitmap indexes. + * Get total microseconds spent reading commit graphs. * - * @return total number of bytes read from bitmap indexes. + * @return total microseconds spent reading commit graphs. */ - public long getReadBitmapIndexBytes() { - return stats.readBitmapIdxBytes; + public long getReadCommitGraphMicros() { + return stats.readCommitGraphMicros; } /** - * Get total microseconds spent reading bitmap indexes. + * Get total microseconds spent reading object size indexes. * - * @return total microseconds spent reading bitmap indexes. + * @return total microseconds spent reading object size indexes. */ - public long getReadBitmapIndexMicros() { - return stats.readBitmapIdxMicros; + public long getReadObjectSizeIndexMicros() { + return stats.readObjectSizeIndexMicros; } /** @@ -297,4 +357,41 @@ public class DfsReaderIoStats { public long getInflationMicros() { return stats.inflationMicros; } + + /** + * Get count of invocations to + * {@link DfsReader#isNotLargerThan(AnyObjectId, int, long)} + * <p> + * Each call could use the object-size index or not. + * + * @return how many times the size of an object was checked with + * {@link DfsReader#isNotLargerThan(AnyObjectId, int, long)} + */ + public long getIsNotLargerThanCallCount() { + return stats.isNotLargerThanCallCount; + } + + /** + * Get number of times the size of a blob was found in the object size + * index. + * <p> + * This counts only queries for blobs on packs with object size index. + * + * @return count of object size index hits + */ + public long getObjectSizeIndexHits() { + return stats.objectSizeIndexHit; + } + + /** + * Get number of times the size of an object was not found in the object + * size index. This usually means it was below the threshold. + * <p> + * This counts only queries for blobs on packs with object size index. + * + * @return count of object size index misses. + */ + public long getObjectSizeIndexMisses() { + return stats.objectSizeIndexMiss; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java index 146f76167d..c3974691a1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java @@ -13,8 +13,10 @@ package org.eclipse.jgit.internal.storage.dfs; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_DELTA_BASE_CACHE_LIMIT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_LOAD_REV_INDEX_IN_PARALLEL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_BUFFER; -import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_FILE_TRESHOLD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_FILE_THRESHOLD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_USE_OBJECT_SIZE_INDEX; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.storage.pack.PackConfig; @@ -36,6 +38,8 @@ public class DfsReaderOptions { private boolean loadRevIndexInParallel; + private boolean useObjectSizeIndex; + /** * Create a default reader configuration. */ @@ -137,6 +141,28 @@ public class DfsReaderOptions { } /** + * Use the object size index if available. + * + * @return true if the reader should try to use the object size index. if + * false, the reader ignores that index. + */ + public boolean shouldUseObjectSizeIndex() { + return useObjectSizeIndex; + } + + /** + * Set if the reader should try to use the object size index + * + * @param useObjectSizeIndex true to use it, false to ignore the object size index + * + * @return {@code this} + */ + public DfsReaderOptions setUseObjectSizeIndex(boolean useObjectSizeIndex) { + this.useObjectSizeIndex = useObjectSizeIndex; + return this; + } + + /** * Update properties by setting fields from the configuration. * <p> * If a property is not defined in the configuration, then it is left @@ -157,7 +183,7 @@ public class DfsReaderOptions { long sft = rc.getLong( CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, - CONFIG_KEY_STREAM_FILE_TRESHOLD, + CONFIG_KEY_STREAM_FILE_THRESHOLD, getStreamFileThreshold()); sft = Math.min(sft, maxMem / 4); // don't use more than 1/4 of the heap sft = Math.min(sft, Integer.MAX_VALUE); // cannot exceed array length @@ -168,6 +194,13 @@ public class DfsReaderOptions { CONFIG_DFS_SECTION, CONFIG_KEY_STREAM_BUFFER, getStreamPackBufferSize())); + + setUseObjectSizeIndex(rc.getBoolean(CONFIG_CORE_SECTION, + CONFIG_DFS_SECTION, CONFIG_KEY_USE_OBJECT_SIZE_INDEX, + false)); + setLoadRevIndexInParallel( + rc.getBoolean(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, + CONFIG_KEY_LOAD_REV_INDEX_IN_PARALLEL, false)); return this; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java index ae656707ac..4547bcab5d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java @@ -34,7 +34,6 @@ import org.eclipse.jgit.util.RefMap; /** * Abstract DfsRefDatabase class. - * */ public abstract class DfsRefDatabase extends RefDatabase { private final DfsRepository repository; @@ -65,7 +64,6 @@ public abstract class DfsRefDatabase extends RefDatabase { return 0 < read().size(); } - /** {@inheritDoc} */ @Override public Ref exactRef(String name) throws IOException { RefCache curr = read(); @@ -73,13 +71,11 @@ public abstract class DfsRefDatabase extends RefDatabase { return ref != null ? resolve(ref, 0, curr.ids) : null; } - /** {@inheritDoc} */ @Override public List<Ref> getAdditionalRefs() { return Collections.emptyList(); } - /** {@inheritDoc} */ @Override public Map<String, Ref> getRefs(String prefix) throws IOException { RefCache curr = read(); @@ -126,7 +122,6 @@ public abstract class DfsRefDatabase extends RefDatabase { return new SymbolicRef(ref.getName(), dst); } - /** {@inheritDoc} */ @Override public Ref peel(Ref ref) throws IOException { final Ref oldLeaf = ref.getLeaf(); @@ -176,7 +171,6 @@ public abstract class DfsRefDatabase extends RefDatabase { return leaf; } - /** {@inheritDoc} */ @Override public RefUpdate newUpdate(String refName, boolean detach) throws IOException { @@ -193,7 +187,6 @@ public abstract class DfsRefDatabase extends RefDatabase { return update; } - /** {@inheritDoc} */ @Override public RefRename newRename(String fromName, String toName) throws IOException { @@ -202,7 +195,6 @@ public abstract class DfsRefDatabase extends RefDatabase { return new DfsRefRename(src, dst); } - /** {@inheritDoc} */ @Override public boolean isNameConflicting(String refName) throws IOException { RefList<Ref> all = read().ids; @@ -224,19 +216,16 @@ public abstract class DfsRefDatabase extends RefDatabase { return false; } - /** {@inheritDoc} */ @Override public void create() { // Nothing to do. } - /** {@inheritDoc} */ @Override public void refresh() { clearCache(); } - /** {@inheritDoc} */ @Override public void close() { clearCache(); @@ -369,7 +358,11 @@ public abstract class DfsRefDatabase extends RefDatabase { this(ids, old.sym); } - /** @return number of references in this cache. */ + /** + * Get number of references + * + * @return number of references in this cache. + */ public int size() { return ids.size(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefRename.java index 569803575d..e76b0bc142 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefRename.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefRename.java @@ -22,7 +22,6 @@ final class DfsRefRename extends RefRename { super(src, dst); } - /** {@inheritDoc} */ @Override protected Result doRename() throws IOException { // TODO Correctly handle renaming foo/bar to foo. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefUpdate.java index 28331a08e6..f327034123 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefUpdate.java @@ -33,19 +33,16 @@ final class DfsRefUpdate extends RefUpdate { this.refdb = refdb; } - /** {@inheritDoc} */ @Override protected DfsRefDatabase getRefDatabase() { return refdb; } - /** {@inheritDoc} */ @Override protected DfsRepository getRepository() { return refdb.getRepository(); } - /** {@inheritDoc} */ @Override protected boolean tryLock(boolean deref) throws IOException { dstRef = getRef(); @@ -60,13 +57,11 @@ final class DfsRefUpdate extends RefUpdate { return true; } - /** {@inheritDoc} */ @Override protected void unlock() { // No state is held while "locked". } - /** {@inheritDoc} */ @Override public Result update(RevWalk walk) throws IOException { try { @@ -77,7 +72,6 @@ final class DfsRefUpdate extends RefUpdate { } } - /** {@inheritDoc} */ @Override protected Result doUpdate(Result desiredResult) throws IOException { ObjectIdRef newRef; @@ -102,7 +96,6 @@ final class DfsRefUpdate extends RefUpdate { return Result.LOCK_FAILURE; } - /** {@inheritDoc} */ @Override protected Result doDelete(Result desiredResult) throws IOException { if (getRefDatabase().compareAndRemove(dstRef)) { @@ -112,7 +105,6 @@ final class DfsRefUpdate extends RefUpdate { return Result.LOCK_FAILURE; } - /** {@inheritDoc} */ @Override protected Result doLink(String target) throws IOException { final SymbolicRef newRef = new SymbolicRef( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java index 6c3b056efd..2751cd2969 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java @@ -28,6 +28,7 @@ import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.ReflogReader; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.util.RefList; @@ -75,19 +76,16 @@ public class DfsReftableDatabase extends DfsRefDatabase { stack = null; } - /** {@inheritDoc} */ @Override public boolean hasVersioning() { return true; } - /** {@inheritDoc} */ @Override public boolean performsAtomicTransactions() { return true; } - /** {@inheritDoc} */ @Override public BatchRefUpdate newBatchUpdate() { DfsObjDatabase odb = getRepository().getObjectDatabase(); @@ -151,13 +149,11 @@ public class DfsReftableDatabase extends DfsRefDatabase { return reftableDatabase.isNameConflicting(refName, new TreeSet<>(), new HashSet<>()); } - /** {@inheritDoc} */ @Override public Ref exactRef(String name) throws IOException { return reftableDatabase.exactRef(name); } - /** {@inheritDoc} */ @Override public Map<String, Ref> getRefs(String prefix) throws IOException { List<Ref> refs = reftableDatabase.getRefsByPrefix(prefix); @@ -169,21 +165,23 @@ public class DfsReftableDatabase extends DfsRefDatabase { RefList.emptyList()); } - /** {@inheritDoc} */ @Override public List<Ref> getRefsByPrefix(String prefix) throws IOException { return reftableDatabase.getRefsByPrefix(prefix); } - /** {@inheritDoc} */ @Override public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes) throws IOException { return reftableDatabase.getRefsByPrefixWithExclusions(include, excludes); } - /** {@inheritDoc} */ + @Override + public ReflogReader getReflogReader(Ref ref) throws IOException { + return reftableDatabase.getReflogReader(ref.getName()); + } + @Override public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException { if (!getReftableConfig().isIndexObjects()) { @@ -192,13 +190,11 @@ public class DfsReftableDatabase extends DfsRefDatabase { return reftableDatabase.getTipsWithSha1(id); } - /** {@inheritDoc} */ @Override public boolean hasFastTipsWithSha1() throws IOException { return reftableDatabase.hasFastTipsWithSha1(); } - /** {@inheritDoc} */ @Override public Ref peel(Ref ref) throws IOException { Ref oldLeaf = ref.getLeaf(); @@ -233,7 +229,6 @@ public class DfsReftableDatabase extends DfsRefDatabase { } } - /** {@inheritDoc} */ @Override protected boolean compareAndPut(Ref oldRef, @Nullable Ref newRef) throws IOException { @@ -254,13 +249,11 @@ public class DfsReftableDatabase extends DfsRefDatabase { } } - /** {@inheritDoc} */ @Override protected boolean compareAndRemove(Ref oldRef) throws IOException { return compareAndPut(oldRef, null); } - /** {@inheritDoc} */ @Override protected RefCache scanAllRefs() throws IOException { throw new UnsupportedOperationException(); @@ -276,7 +269,6 @@ public class DfsReftableDatabase extends DfsRefDatabase { // Unnecessary; DfsReftableBatchRefUpdate calls clearCache(). } - /** {@inheritDoc} */ @Override protected void cachePeeledState(Ref oldLeaf, Ref newLeaf) { // Do not cache peeled state in reftable. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableStack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableStack.java index 27b03299e4..97ba4d52eb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableStack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableStack.java @@ -80,7 +80,6 @@ public class DfsReftableStack implements AutoCloseable { return Collections.unmodifiableList(tables); } - /** {@inheritDoc} */ @Override public void close() { for (ReftableReader t : tables) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java index 32c0ccdf4f..218f1e9552 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java @@ -45,7 +45,6 @@ public abstract class DfsRepository extends Repository { this.description = builder.getRepositoryDescription(); } - /** {@inheritDoc} */ @Override public abstract DfsObjDatabase getObjectDatabase(); @@ -72,7 +71,6 @@ public abstract class DfsRepository extends Repository { return true; } - /** {@inheritDoc} */ @Override public void create(boolean bare) throws IOException { if (exists()) @@ -85,39 +83,33 @@ public abstract class DfsRepository extends Repository { throw new IOException(result.name()); } - /** {@inheritDoc} */ @Override public StoredConfig getConfig() { return config; } - /** {@inheritDoc} */ @Override public String getIdentifier() { return getDescription().getRepositoryName(); } - /** {@inheritDoc} */ @Override public void scanForRepoChanges() throws IOException { getRefDatabase().refresh(); getObjectDatabase().clearCache(); } - /** {@inheritDoc} */ @Override public void notifyIndexChanged(boolean internal) { // Do not send notifications. // There is no index, as there is no working tree. } - /** {@inheritDoc} */ @Override public ReflogReader getReflogReader(String refName) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public AttributesNodeProvider createAttributesNodeProvider() { // TODO Check if the implementation used in FileRepository can be used diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepositoryBuilder.java index 943d92684b..ab5f7fe75e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepositoryBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepositoryBuilder.java @@ -72,7 +72,6 @@ public abstract class DfsRepositoryBuilder<B extends DfsRepositoryBuilder, R ext return self(); } - /** {@inheritDoc} */ @Override public B setup() throws IllegalArgumentException, IOException { super.setup(); @@ -97,7 +96,6 @@ public abstract class DfsRepositoryBuilder<B extends DfsRepositoryBuilder, R ext // We don't support local file IO and thus shouldn't permit these to set. - /** {@inheritDoc} */ @Override public B setGitDir(File gitDir) { if (gitDir != null) @@ -105,7 +103,6 @@ public abstract class DfsRepositoryBuilder<B extends DfsRepositoryBuilder, R ext return self(); } - /** {@inheritDoc} */ @Override public B setObjectDirectory(File objectDirectory) { if (objectDirectory != null) @@ -113,14 +110,12 @@ public abstract class DfsRepositoryBuilder<B extends DfsRepositoryBuilder, R ext return self(); } - /** {@inheritDoc} */ @Override public B addAlternateObjectDirectory(File other) { throw new UnsupportedOperationException( JGitText.get().unsupportedAlternates); } - /** {@inheritDoc} */ @Override public B setWorkTree(File workTree) { if (workTree != null) @@ -128,7 +123,6 @@ public abstract class DfsRepositoryBuilder<B extends DfsRepositoryBuilder, R ext return self(); } - /** {@inheritDoc} */ @Override public B setIndexFile(File indexFile) { if (indexFile != null) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepositoryDescription.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepositoryDescription.java index 069956b586..e949085819 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepositoryDescription.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepositoryDescription.java @@ -42,7 +42,6 @@ public class DfsRepositoryDescription { return repositoryName; } - /** {@inheritDoc} */ @Override public int hashCode() { if (getRepositoryName() != null) @@ -50,7 +49,6 @@ public class DfsRepositoryDescription { return System.identityHashCode(this); } - /** {@inheritDoc} */ @Override public boolean equals(Object b) { if (b instanceof DfsRepositoryDescription){ @@ -61,7 +59,6 @@ public class DfsRepositoryDescription { return false; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsStreamKey.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsStreamKey.java index f3f30914f6..8983c2d2eb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsStreamKey.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsStreamKey.java @@ -56,17 +56,14 @@ public abstract class DfsStreamKey { this.packExtPos = ext == null ? 0 : ext.getPosition(); } - /** {@inheritDoc} */ @Override public int hashCode() { return hash; } - /** {@inheritDoc} */ @Override public abstract boolean equals(Object o); - /** {@inheritDoc} */ @SuppressWarnings("boxing") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsText.java index f36ec06d3f..671d93d32d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsText.java @@ -16,6 +16,7 @@ import org.eclipse.jgit.nls.TranslationBundle; /** * Translation bundle for the DFS storage implementation. */ +@SuppressWarnings("MissingSummary") public class DfsText extends TranslationBundle { /** * Get an instance of this translation bundle. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java index 583b8b3f6b..fe8dc17b3d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java @@ -78,13 +78,11 @@ public class InMemoryRepository extends DfsRepository { return new MemRefDatabase(); } - /** {@inheritDoc} */ @Override public MemObjDatabase getObjectDatabase() { return objdb; } - /** {@inheritDoc} */ @Override public RefDatabase getRefDatabase() { return refdb; @@ -102,14 +100,12 @@ public class InMemoryRepository extends DfsRepository { refdb.performsAtomicTransactions = atomic; } - /** {@inheritDoc} */ @Override @Nullable public String getGitwebDescription() { return gitwebDescription; } - /** {@inheritDoc} */ @Override public void setGitwebDescription(@Nullable String d) { gitwebDescription = d; @@ -126,6 +122,8 @@ public class InMemoryRepository extends DfsRepository { } /** + * Set readable channel block size + * * @param blockSize * force a different block size for testing. */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java index cd4f168d86..89a3afbd9b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java @@ -44,31 +44,26 @@ final class LargePackedWholeObject extends ObjectLoader { this.db = db; } - /** {@inheritDoc} */ @Override public int getType() { return type; } - /** {@inheritDoc} */ @Override public long getSize() { return size; } - /** {@inheritDoc} */ @Override public boolean isLarge() { return true; } - /** {@inheritDoc} */ @Override public byte[] getCachedBytes() throws LargeObjectException { throw new LargeObjectException(); } - /** {@inheritDoc} */ @Override public ObjectStream openStream() throws MissingObjectException, IOException { PackInputStream packIn; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java new file mode 100644 index 0000000000..bb44f9397a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2024, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.ReadableChannelSupplier; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.Ref; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.RefLoader; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig; +import org.eclipse.jgit.internal.storage.pack.PackExt; + +/** + * A table that holds multiple cache tables accessed by {@link PackExt} types. + * + * <p> + * Allows the separation of entries from different {@link PackExt} types to + * limit churn in cache caused by entries of differing sizes. + * <p> + * Separating these tables enables the fine-tuning of cache tables per extension + * type. + */ +class PackExtBlockCacheTable implements DfsBlockCacheTable { + /** + * Table name. + */ + private final String name; + + private final DfsBlockCacheTable defaultBlockCacheTable; + + // Holds the unique tables backing the extBlockCacheTables values. + private final List<DfsBlockCacheTable> blockCacheTableList; + + // Holds the mapping of PackExt to DfsBlockCacheTables. + // The relation between the size of extBlockCacheTables entries and + // blockCacheTableList entries is: + // blockCacheTableList.size() <= extBlockCacheTables.size() + private final Map<PackExt, DfsBlockCacheTable> extBlockCacheTables; + + /** + * Builds the PackExtBlockCacheTable from a list of + * {@link DfsBlockCachePackExtConfig}s. + * + * @param cacheConfig + * {@link DfsBlockCacheConfig} containing + * {@link DfsBlockCachePackExtConfig}s used to configure + * PackExtBlockCacheTable. The {@link DfsBlockCacheConfig} holds + * the configuration for the default cache table. + * @return the cache table built from the given configs. + * @throws IllegalArgumentException + * when no {@link DfsBlockCachePackExtConfig} exists in the + * {@link DfsBlockCacheConfig}. + */ + static PackExtBlockCacheTable fromBlockCacheConfigs( + DfsBlockCacheConfig cacheConfig) { + DfsBlockCacheTable defaultTable = new ClockBlockCacheTable(cacheConfig); + Map<PackExt, DfsBlockCacheTable> packExtBlockCacheTables = new HashMap<>(); + List<DfsBlockCachePackExtConfig> packExtConfigs = cacheConfig + .getPackExtCacheConfigurations(); + if (packExtConfigs == null || packExtConfigs.size() == 0) { + throw new IllegalArgumentException( + JGitText.get().noPackExtConfigurationGiven); + } + for (DfsBlockCachePackExtConfig packExtCacheConfig : packExtConfigs) { + DfsBlockCacheTable table = new ClockBlockCacheTable( + packExtCacheConfig.getPackExtCacheConfiguration()); + for (PackExt packExt : packExtCacheConfig.getPackExts()) { + if (packExtBlockCacheTables.containsKey(packExt)) { + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().duplicatePackExtensionsForCacheTables, + packExt)); + } + packExtBlockCacheTables.put(packExt, table); + } + } + return fromCacheTables(defaultTable, packExtBlockCacheTables); + } + + /** + * Creates a new PackExtBlockCacheTable from the combination of a default + * {@link DfsBlockCacheTable} and a map of {@link PackExt}s to + * {@link DfsBlockCacheTable}s. + * <p> + * This method allows for the PackExtBlockCacheTable to handle a mapping of + * {@link PackExt}s to arbitrarily defined {@link DfsBlockCacheTable} + * implementations. This is especially useful for users wishing to implement + * custom cache tables. + * <p> + * This is currently made visible for testing. + * + * @param defaultBlockCacheTable + * the default table used when a handling a {@link PackExt} type + * that does not map to a {@link DfsBlockCacheTable} mapped by + * packExtsCacheTablePairs. + * @param packExtBlockCacheTables + * the mapping of {@link PackExt}s to + * {@link DfsBlockCacheTable}s. A single + * {@link DfsBlockCacheTable} can be defined for multiple + * {@link PackExt}s in a many-to-one relationship. + * @return the PackExtBlockCacheTable created from the + * defaultBlockCacheTable and packExtsCacheTablePairs mapping. + * @throws IllegalArgumentException + * when a {@link PackExt} is defined for multiple + * {@link DfsBlockCacheTable}s. + */ + static PackExtBlockCacheTable fromCacheTables( + DfsBlockCacheTable defaultBlockCacheTable, + Map<PackExt, DfsBlockCacheTable> packExtBlockCacheTables) { + Set<DfsBlockCacheTable> blockCacheTables = new HashSet<>(); + blockCacheTables.add(defaultBlockCacheTable); + blockCacheTables.addAll(packExtBlockCacheTables.values()); + String name = defaultBlockCacheTable.getName() + "," //$NON-NLS-1$ + + packExtBlockCacheTables.values().stream() + .map(DfsBlockCacheTable::getName).sorted() + .collect(Collectors.joining(",")); //$NON-NLS-1$ + return new PackExtBlockCacheTable(name, defaultBlockCacheTable, + List.copyOf(blockCacheTables), packExtBlockCacheTables); + } + + private PackExtBlockCacheTable(String name, + DfsBlockCacheTable defaultBlockCacheTable, + List<DfsBlockCacheTable> blockCacheTableList, + Map<PackExt, DfsBlockCacheTable> extBlockCacheTables) { + this.name = name; + this.defaultBlockCacheTable = defaultBlockCacheTable; + this.blockCacheTableList = blockCacheTableList; + this.extBlockCacheTables = extBlockCacheTables; + } + + @Override + public boolean hasBlock0(DfsStreamKey key) { + return getTable(key).hasBlock0(key); + } + + @Override + public DfsBlock getOrLoad(BlockBasedFile file, long position, + DfsReader dfsReader, ReadableChannelSupplier fileChannel) + throws IOException { + return getTable(file.ext).getOrLoad(file, position, dfsReader, + fileChannel); + } + + @Override + public <T> Ref<T> getOrLoadRef(DfsStreamKey key, long position, + RefLoader<T> loader) throws IOException { + return getTable(key).getOrLoadRef(key, position, loader); + } + + @Override + public void put(DfsBlock v) { + getTable(v.stream).put(v); + } + + @Override + public <T> Ref<T> put(DfsStreamKey key, long pos, long size, T v) { + return getTable(key).put(key, pos, size, v); + } + + @Override + public <T> Ref<T> putRef(DfsStreamKey key, long size, T v) { + return getTable(key).putRef(key, size, v); + } + + @Override + public boolean contains(DfsStreamKey key, long position) { + return getTable(key).contains(key, position); + } + + @Override + public <T> T get(DfsStreamKey key, long position) { + return getTable(key).get(key, position); + } + + @Override + public List<BlockCacheStats> getBlockCacheStats() { + return blockCacheTableList.stream() + .flatMap(cacheTable -> cacheTable.getBlockCacheStats().stream()) + .collect(Collectors.toList()); + } + + @Override + public String getName() { + return name; + } + + private DfsBlockCacheTable getTable(PackExt packExt) { + return extBlockCacheTables.getOrDefault(packExt, + defaultBlockCacheTable); + } + + private DfsBlockCacheTable getTable(DfsStreamKey key) { + return extBlockCacheTables.getOrDefault(getPackExt(key), + defaultBlockCacheTable); + } + + private static PackExt getPackExt(DfsStreamKey key) { + return PackExt.values()[key.packExtPos]; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackInputStream.java index 6ff81646cf..3d07660a60 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackInputStream.java @@ -31,7 +31,6 @@ final class PackInputStream extends InputStream { ctx.pin(pack, pos); } - /** {@inheritDoc} */ @Override public int read(byte[] b, int off, int len) throws IOException { int n = ctx.copy(pack, pos, b, off, len); @@ -39,7 +38,6 @@ final class PackInputStream extends InputStream { return n; } - /** {@inheritDoc} */ @Override public int read() throws IOException { byte[] buf = new byte[1]; @@ -47,7 +45,6 @@ final class PackInputStream extends InputStream { return n == 1 ? buf[0] & 0xff : -1; } - /** {@inheritDoc} */ @Override public void close() { ctx.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndex.java index ec53818b4e..5f979b0daa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndex.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndex.java @@ -18,14 +18,13 @@ import com.googlecode.javaewah.EWAHCompressedBitmap; /** * Base implementation of the PackBitmapIndex. */ -abstract class BasePackBitmapIndex extends PackBitmapIndex { +abstract class BasePackBitmapIndex implements PackBitmapIndex { private final ObjectIdOwnerMap<StoredBitmap> bitmaps; BasePackBitmapIndex(ObjectIdOwnerMap<StoredBitmap> bitmaps) { this.bitmaps = bitmaps; } - /** {@inheritDoc} */ @Override public EWAHCompressedBitmap getBitmap(AnyObjectId objectId) { StoredBitmap sb = bitmaps.get(objectId); @@ -36,6 +35,50 @@ abstract class BasePackBitmapIndex extends PackBitmapIndex { return bitmaps; } + @Override + public int getBaseBitmapCount() { + int bases = 0; + for (StoredBitmap sb : getBitmaps()) { + if (sb.isBase()) { + bases += 1; + } + } + return bases; + } + + @Override + public long getBaseBitmapSizeInBytes() { + long baseSize = 0; + for (StoredBitmap sb : getBitmaps()) { + if (sb.isBase()) { + baseSize += sb.getCurrentSizeInBytes(); + } + } + return baseSize; + } + + @Override + public int getXorBitmapCount() { + int xored = 0; + for (StoredBitmap sb : getBitmaps()) { + if (!sb.isBase()) { + xored += 1; + } + } + return xored; + } + + @Override + public long getXorBitmapSizeInBytes() { + long xorSize = 0; + for (StoredBitmap sb : getBitmaps()) { + if (!sb.isBase()) { + xorSize += sb.getCurrentSizeInBytes(); + } + } + return xorSize; + } + /** * Data representation of the bitmap entry restored from a pack index. The * commit of the bitmap is the map key. @@ -75,8 +118,9 @@ abstract class BasePackBitmapIndex extends PackBitmapIndex { EWAHCompressedBitmap getBitmapWithoutCaching() { // Fast path to immediately return the expanded result. Object r = bitmapContainer; - if (r instanceof EWAHCompressedBitmap) + if (r instanceof EWAHCompressedBitmap) { return (EWAHCompressedBitmap) r; + } // Expand the bitmap but not cache the result. XorCompressedBitmap xb = (XorCompressedBitmap) r; @@ -93,14 +137,46 @@ abstract class BasePackBitmapIndex extends PackBitmapIndex { } } - /** @return the flags associated with the bitmap */ + /** + * Get flags + * + * @return the flags associated with the bitmap + */ int getFlags() { return flags; } + + /** + * This bitmap is (currently) a base or a XOR mask + * + * @return true if this bitmap is a base (a ready map). + */ + boolean isBase() { + return bitmapContainer instanceof EWAHCompressedBitmap; + } + + /** + * Size in bytes of this bitmap in its current representation + * + * If this is a XOR'ed bitmap, size is different before/after + * {@link #getBitmap()}. Before is the byte size of the xor mask, + * afterwards is the size of the "ready" bitmap + * + * @return size in bytes of the bitmap in its current representation + */ + long getCurrentSizeInBytes() { + Object r = bitmapContainer; + if (r instanceof EWAHCompressedBitmap) { + return ((EWAHCompressedBitmap) r).sizeInBytes(); + } + XorCompressedBitmap xor = ((XorCompressedBitmap) r); + return xor.bitmap.sizeInBytes(); + } } private static final class XorCompressedBitmap { final EWAHCompressedBitmap bitmap; + final StoredBitmap xorBitmap; XorCompressedBitmap(EWAHCompressedBitmap b, StoredBitmap xb) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackIndexWriter.java index 87e0b44d46..b89cc1ebf4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackIndexWriter.java @@ -19,6 +19,7 @@ import java.text.MessageFormat; import java.util.List; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.pack.PackIndexWriter; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.transport.PackedObjectInfo; import org.eclipse.jgit.util.NB; @@ -31,7 +32,7 @@ import org.eclipse.jgit.util.NB; * 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 { +public abstract class BasePackIndexWriter implements PackIndexWriter { /** Magic constant indicating post-version 1 format. */ protected static final byte[] TOC = { -1, 't', 'O', 'c' }; @@ -147,7 +148,7 @@ public abstract class PackIndexWriter { * the stream this instance outputs to. If not already buffered * it will be automatically wrapped in a buffered stream. */ - protected PackIndexWriter(OutputStream dst) { + protected BasePackIndexWriter(OutputStream dst) { out = new DigestOutputStream(dst instanceof BufferedOutputStream ? dst : new BufferedOutputStream(dst), Constants.newMessageDigest()); @@ -172,6 +173,7 @@ public abstract class PackIndexWriter { * an error occurred while writing to the output stream, or this * index format cannot store the object data supplied. */ + @Override public void write(final List<? extends PackedObjectInfo> toStore, final byte[] packDataChecksum) throws IOException { entries = toStore; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BitmapIndexImpl.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BitmapIndexImpl.java index 9aa14171c7..ba6929308c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BitmapIndexImpl.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BitmapIndexImpl.java @@ -38,6 +38,8 @@ public class BitmapIndexImpl implements BitmapIndex { final int indexObjectCount; + private BitmapLookupListener listener = BitmapLookupListener.NOOP; + /** * Creates a BitmapIndex that is back by Compressed bitmaps. * @@ -54,21 +56,31 @@ public class BitmapIndexImpl implements BitmapIndex { return packIndex; } - /** {@inheritDoc} */ @Override public CompressedBitmap getBitmap(AnyObjectId objectId) { EWAHCompressedBitmap compressed = packIndex.getBitmap(objectId); - if (compressed == null) + if (compressed == null) { + listener.onBitmapNotFound(objectId); return null; + } + listener.onBitmapFound(objectId); return new CompressedBitmap(compressed, this); } - /** {@inheritDoc} */ @Override public CompressedBitmapBuilder newBitmapBuilder() { return new CompressedBitmapBuilder(this); } + @Override + public void addBitmapLookupListener(BitmapLookupListener l) { + if (l == null) { + throw new IllegalArgumentException( + JGitText.get().bitmapUseNoopNoListener); + } + this.listener = l; + } + int findPosition(AnyObjectId objectId) { int position = packIndex.findPosition(objectId); if (position < 0) { @@ -88,6 +100,11 @@ public class BitmapIndexImpl implements BitmapIndex { return position; } + /** + * A bitset for representing small changes (set/remove individual bits) + * relative to an existing EWAH bitmap. Executing bit-vector operations will + * materialize the changes into a fresh EWAH bitmap + */ private static final class ComboBitset { private InflatingBitSet inflatingBitmap; @@ -123,18 +140,21 @@ public class BitmapIndexImpl implements BitmapIndex { return inflatingBitmap.getBitmap(); } + /* In-place or operation */ void or(EWAHCompressedBitmap inbits) { if (toRemove != null) combine(); inflatingBitmap = inflatingBitmap.or(inbits); } + /* In-place andNot operation */ void andNot(EWAHCompressedBitmap inbits) { if (toAdd != null || toRemove != null) combine(); inflatingBitmap = inflatingBitmap.andNot(inbits); } + /* In-place xor operation. */ void xor(EWAHCompressedBitmap inbits) { if (toAdd != null || toRemove != null) combine(); @@ -291,7 +311,9 @@ public class BitmapIndexImpl implements BitmapIndex { * Construct compressed bitmap for given bitmap and bitmap index * * @param bitmap + * the bitmap * @param bitmapIndex + * the bitmap index */ public CompressedBitmap(EWAHCompressedBitmap bitmap, BitmapIndexImpl bitmapIndex) { this.bitmap = bitmap; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java index 1036535423..ef1392e6e0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java @@ -30,7 +30,6 @@ final class ByteArrayWindow extends ByteWindow { array = b; } - /** {@inheritDoc} */ @Override protected int copy(int p, byte[] b, int o, int n) { n = Math.min(array.length - p, n); @@ -38,7 +37,6 @@ final class ByteArrayWindow extends ByteWindow { return n; } - /** {@inheritDoc} */ @Override protected int setInput(int pos, Inflater inf) throws DataFormatException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java index b6877578c9..4094f130f6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java @@ -32,7 +32,6 @@ final class ByteBufferWindow extends ByteWindow { buffer = b; } - /** {@inheritDoc} */ @Override protected int copy(int p, byte[] b, int o, int n) { final ByteBuffer s = buffer.slice(); @@ -57,7 +56,6 @@ final class ByteBufferWindow extends ByteWindow { } } - /** {@inheritDoc} */ @Override protected int setInput(int pos, Inflater inf) throws DataFormatException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java index 2e19580f5f..129d2e0894 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java @@ -86,13 +86,11 @@ class CachedObjectDirectory extends FileObjectDatabase { return m; } - /** {@inheritDoc} */ @Override public void close() { // Don't close anything. } - /** {@inheritDoc} */ @Override public ObjectDatabase newCachedDatabase() { return this; @@ -153,7 +151,6 @@ class CachedObjectDirectory extends FileObjectDatabase { wrapped.resolve(matches, id); } - /** {@inheritDoc} */ @Override public boolean has(AnyObjectId objectId) throws IOException { return has(objectId, null); @@ -261,7 +258,6 @@ class CachedObjectDirectory extends FileObjectDatabase { return wrapped.getPacks(); } - /** {@inheritDoc} */ @Override public Optional<CommitGraph> getCommitGraph() { return wrapped.getCommitGraph(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CheckoutEntryImpl.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CheckoutEntryImpl.java index 2b8779f6df..d641da59e1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CheckoutEntryImpl.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CheckoutEntryImpl.java @@ -32,13 +32,11 @@ public class CheckoutEntryImpl implements CheckoutEntry { to = comment.substring(p2 + " to ".length(), p3); //$NON-NLS-1$ } - /** {@inheritDoc} */ @Override public String getFromBranch() { return from; } - /** {@inheritDoc} */ @Override public String getToBranch() { return to; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileCommitGraph.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileCommitGraph.java index 44429a7786..5172963e2e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileCommitGraph.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileCommitGraph.java @@ -108,7 +108,8 @@ public class FileCommitGraph { // commit-graph file was not modified return this; } - return new GraphSnapshot(file, FileSnapshot.save(file), open(file)); + return new GraphSnapshot(file, FileSnapshot.save(file), + open(file)); } private static CommitGraph open(File file) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java index aa578d31ba..e8f532fe24 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java @@ -33,13 +33,11 @@ abstract class FileObjectDatabase extends ObjectDatabase { INSERTED, EXISTS_PACKED, EXISTS_LOOSE, FAILURE; } - /** {@inheritDoc} */ @Override public ObjectReader newReader() { return new WindowCursor(this); } - /** {@inheritDoc} */ @Override public ObjectDirectoryInserter newInserter() { return new ObjectDirectoryInserter(this, getConfig()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java index e9e17c0ec5..64f8c9b0e3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java @@ -16,6 +16,7 @@ import static org.eclipse.jgit.lib.Ref.Storage.PACKED; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -28,17 +29,21 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.api.PackRefsCommand; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.events.RefsChangedEvent; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.reftable.MergedReftable; import org.eclipse.jgit.internal.storage.reftable.ReftableBatchRefUpdate; import org.eclipse.jgit.internal.storage.reftable.ReftableDatabase; import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.RefRename; @@ -67,15 +72,15 @@ public class FileReftableDatabase extends RefDatabase { private final FileReftableStack reftableStack; - FileReftableDatabase(FileRepository repo) throws IOException { - this(repo, new File(new File(repo.getDirectory(), Constants.REFTABLE), - Constants.TABLES_LIST)); - } + private volatile boolean autoRefresh; - FileReftableDatabase(FileRepository repo, File refstackName) throws IOException { + FileReftableDatabase(FileRepository repo) throws IOException { this.fileRepository = repo; - this.reftableStack = new FileReftableStack(refstackName, - new File(fileRepository.getDirectory(), Constants.REFTABLE), + this.autoRefresh = repo.getConfig().getBoolean( + ConfigConstants.CONFIG_REFTABLE_SECTION, + ConfigConstants.CONFIG_KEY_AUTOREFRESH, false); + this.reftableStack = new FileReftableStack( + new File(fileRepository.getCommonDirectory(), Constants.REFTABLE), () -> fileRepository.fireEvent(new RefsChangedEvent()), () -> fileRepository.getConfig()); this.reftableDatabase = new ReftableDatabase() { @@ -87,25 +92,49 @@ public class FileReftableDatabase extends RefDatabase { }; } - ReflogReader getReflogReader(String refname) throws IOException { + @Override + public ReflogReader getReflogReader(Ref ref) throws IOException { + return reftableDatabase.getReflogReader(ref.getName()); + } + + @Override + public ReflogReader getReflogReader(String refname) throws IOException { return reftableDatabase.getReflogReader(refname); } /** + * Whether the given repo uses reftable for refdb storage + * * @param repoDir + * the repository's metadata directory * @return whether the given repo uses reftable for refdb storage. */ public static boolean isReftable(File repoDir) { return new File(repoDir, Constants.REFTABLE).isDirectory(); } - /** {@inheritDoc} */ @Override public boolean hasFastTipsWithSha1() throws IOException { return reftableDatabase.hasFastTipsWithSha1(); } /** + * {@inheritDoc} + * + * For Reftable, all the data is compacted into a single table. + */ + @Override + public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs) + throws IOException { + pm.beginTask(JGitText.get().packRefs, 1); + try { + compactFully(); + } finally { + pm.endTask(); + } + } + + /** * Runs a full compaction for GC purposes. * @throws IOException on I/O errors */ @@ -124,20 +153,17 @@ public class FileReftableDatabase extends RefDatabase { return reftableDatabase.getLock(); } - /** {@inheritDoc} */ @Override public boolean performsAtomicTransactions() { return true; } - /** {@inheritDoc} */ @NonNull @Override public BatchRefUpdate newBatchUpdate() { return new FileReftableBatchRefUpdate(this, fileRepository); } - /** {@inheritDoc} */ @Override public RefUpdate newUpdate(String refName, boolean detach) throws IOException { @@ -157,21 +183,20 @@ public class FileReftableDatabase extends RefDatabase { return update; } - /** {@inheritDoc} */ @Override public Ref exactRef(String name) throws IOException { + autoRefresh(); return reftableDatabase.exactRef(name); } - /** {@inheritDoc} */ @Override public List<Ref> getRefs() throws IOException { return super.getRefs(); } - /** {@inheritDoc} */ @Override public Map<String, Ref> getRefs(String prefix) throws IOException { + autoRefresh(); List<Ref> refs = reftableDatabase.getRefsByPrefix(prefix); RefList.Builder<Ref> builder = new RefList.Builder<>(refs.size()); for (Ref r : refs) { @@ -181,20 +206,18 @@ public class FileReftableDatabase extends RefDatabase { RefList.emptyList()); } - /** {@inheritDoc} */ @Override public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes) throws IOException { + autoRefresh(); return reftableDatabase.getRefsByPrefixWithExclusions(include, excludes); } - /** {@inheritDoc} */ @Override public List<Ref> getAdditionalRefs() throws IOException { return Collections.emptyList(); } - /** {@inheritDoc} */ @Override public Ref peel(Ref ref) throws IOException { Ref oldLeaf = ref.getLeaf(); @@ -205,6 +228,56 @@ public class FileReftableDatabase extends RefDatabase { } + /** + * Whether to auto-refresh the reftable stack if it is out of date. + * + * @param autoRefresh + * whether to auto-refresh the reftable stack if it is out of + * date. + */ + public void setAutoRefresh(boolean autoRefresh) { + this.autoRefresh = autoRefresh; + } + + /** + * Whether the reftable stack is auto-refreshed if it is out of date. + * + * @return whether the reftable stack is auto-refreshed if it is out of + * date. + */ + public boolean isAutoRefresh() { + return autoRefresh; + } + + private void autoRefresh() { + if (autoRefresh) { + refresh(); + } + } + + /** + * Check if the reftable stack is up to date, and if not, reload it. + * <p> + * {@inheritDoc} + */ + @Override + public void refresh() { + try { + if (!reftableStack.isUpToDate()) { + ReentrantLock lock = getLock(); + lock.lock(); + try { + reftableDatabase.clearCache(); + reftableStack.reload(); + } finally { + lock.unlock(); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + private Ref doPeel(Ref leaf) throws IOException { try (RevWalk rw = new RevWalk(fileRepository)) { RevObject obj = rw.parseAny(leaf.getObjectId()); @@ -303,7 +376,6 @@ public class FileReftableDatabase extends RefDatabase { } } - /** {@inheritDoc} */ @Override public RefRename newRename(String fromName, String toName) throws IOException { @@ -312,24 +384,21 @@ public class FileReftableDatabase extends RefDatabase { return new FileRefRename(src, dst); } - /** {@inheritDoc} */ @Override public boolean isNameConflicting(String name) throws IOException { return reftableDatabase.isNameConflicting(name, new TreeSet<>(), new HashSet<>()); } - /** {@inheritDoc} */ @Override public void close() { reftableStack.close(); } - /** {@inheritDoc} */ @Override public void create() throws IOException { FileUtils.mkdir( - new File(fileRepository.getDirectory(), Constants.REFTABLE), + new File(fileRepository.getCommonDirectory(), Constants.REFTABLE), true); } @@ -549,9 +618,10 @@ public class FileReftableDatabase extends RefDatabase { boolean writeLogs) throws IOException { int size = 0; List<Ref> refs = repo.getRefDatabase().getRefs(); + RefDatabase refDb = repo.getRefDatabase(); if (writeLogs) { for (Ref r : refs) { - ReflogReader rlr = repo.getReflogReader(r.getName()); + ReflogReader rlr = refDb.getReflogReader(r); if (rlr != null) { size = Math.max(rlr.getReverseEntries().size(), size); } @@ -574,10 +644,7 @@ public class FileReftableDatabase extends RefDatabase { if (writeLogs) { for (Ref r : refs) { long idx = size; - ReflogReader reader = repo.getReflogReader(r.getName()); - if (reader == null) { - continue; - } + ReflogReader reader = refDb.getReflogReader(r); for (ReflogEntry e : reader.getReverseEntries()) { w.writeLog(r.getName(), idx, e.getWho(), e.getOldId(), e.getNewId(), e.getComment()); @@ -611,36 +678,26 @@ public class FileReftableDatabase extends RefDatabase { } /** + * Convert FileRepository to a FileReftableDatabase + * * @param repo * the repository * @param writeLogs * whether to write reflogs - * @return a reftable based RefDB from an existing repository. * @throws IOException * on IO error */ - public static FileReftableDatabase convertFrom(FileRepository repo, - boolean writeLogs) throws IOException { - FileReftableDatabase newDb = null; - File reftableList = null; - try { - File reftableDir = new File(repo.getDirectory(), - Constants.REFTABLE); - reftableList = new File(reftableDir, Constants.TABLES_LIST); - if (!reftableDir.isDirectory()) { - reftableDir.mkdir(); - } + public static void convertFrom(FileRepository repo, boolean writeLogs) + throws IOException { + File reftableDir = new File(repo.getCommonDirectory(), + Constants.REFTABLE); + if (!reftableDir.isDirectory()) { + reftableDir.mkdir(); + } - try (FileReftableStack stack = new FileReftableStack(reftableList, - reftableDir, null, () -> repo.getConfig())) { - stack.addReftable(rw -> writeConvertTable(repo, rw, writeLogs)); - } - reftableList = null; - } finally { - if (reftableList != null) { - reftableList.delete(); - } + try (FileReftableStack stack = new FileReftableStack(reftableDir, null, + () -> repo.getConfig())) { + stack.addReftable(rw -> writeConvertTable(repo, rw, writeLogs)); } - return newDb; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java index 5152367d23..6658575fc5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java @@ -18,8 +18,10 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.StandardCopyOption; import java.security.SecureRandom; import java.util.ArrayList; @@ -27,6 +29,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -39,6 +42,9 @@ import org.eclipse.jgit.internal.storage.reftable.ReftableConfig; import org.eclipse.jgit.internal.storage.reftable.ReftableReader; import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.CoreConfig; +import org.eclipse.jgit.lib.CoreConfig.TrustStat; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; @@ -59,9 +65,12 @@ public class FileReftableStack implements AutoCloseable { private List<StackEntry> stack; + private AtomicReference<FileSnapshot> snapshot = new AtomicReference<>( + FileSnapshot.DIRTY); + private long lastNextUpdateIndex; - private final File stackPath; + private final File tablesListFile; private final File reftableDir; @@ -98,11 +107,11 @@ public class FileReftableStack implements AutoCloseable { private final CompactionStats stats; + private final TrustStat trustTablesListStat; + /** * Creates a stack corresponding to the list of reftables in the argument * - * @param stackPath - * the filename for the stack. * @param reftableDir * the dir holding the tables. * @param onChange @@ -112,10 +121,10 @@ public class FileReftableStack implements AutoCloseable { * @throws IOException * on I/O problems */ - public FileReftableStack(File stackPath, File reftableDir, + public FileReftableStack(File reftableDir, @Nullable Runnable onChange, Supplier<Config> configSupplier) throws IOException { - this.stackPath = stackPath; + this.tablesListFile = new File(reftableDir, Constants.TABLES_LIST); this.reftableDir = reftableDir; this.stack = new ArrayList<>(); this.configSupplier = configSupplier; @@ -126,6 +135,8 @@ public class FileReftableStack implements AutoCloseable { reload(); stats = new CompactionStats(); + trustTablesListStat = configSupplier.get().get(CoreConfig.KEY) + .getTrustTablesListStat(); } CompactionStats getStats() { @@ -232,7 +243,7 @@ public class FileReftableStack implements AutoCloseable { } if (!success) { - throw new LockFailedException(stackPath); + throw new LockFailedException(tablesListFile); } mergedReftable = new MergedReftable(stack.stream() @@ -246,6 +257,8 @@ public class FileReftableStack implements AutoCloseable { } /** + * Get merged reftable + * * @return the merged reftable */ public MergedReftable getMergedReftable() { @@ -264,23 +277,27 @@ public class FileReftableStack implements AutoCloseable { * @param w * writer to use * @throws IOException + * if an IO error occurred */ void call(ReftableWriter w) throws IOException; } private List<String> readTableNames() throws IOException { + FileSnapshot old; List<String> names = new ArrayList<>(stack.size() + 1); - + old = snapshot.get(); try (BufferedReader br = new BufferedReader( - new InputStreamReader(new FileInputStream(stackPath), UTF_8))) { + new InputStreamReader(new FileInputStream(tablesListFile), UTF_8))) { String line; while ((line = br.readLine()) != null) { if (!line.isEmpty()) { names.add(line); } } + snapshot.compareAndSet(old, FileSnapshot.save(tablesListFile)); } catch (FileNotFoundException e) { // file isn't there: empty repository. + snapshot.compareAndSet(old, FileSnapshot.MISSING_FILE); } return names; } @@ -291,9 +308,29 @@ public class FileReftableStack implements AutoCloseable { * on IO problem */ boolean isUpToDate() throws IOException { - // We could use FileSnapshot to avoid reading the file, but the file is - // small so it's probably a minor optimization. try { + switch (trustTablesListStat) { + case NEVER: + break; + case AFTER_OPEN: + try (InputStream stream = Files + .newInputStream(reftableDir.toPath())) { + // open the refs/reftable/ directory to refresh attributes + // of reftable files and the tables.list file listing their + // names (on some NFS clients) + } catch (FileNotFoundException | NoSuchFileException e) { + // ignore + } + //$FALL-THROUGH$ + case ALWAYS: + if (!snapshot.get().isModified(tablesListFile)) { + return true; + } + break; + case INHERIT: + // only used in CoreConfig internally + throw new IllegalStateException(); + } List<String> names = readTableNames(); if (names.size() != stack.size()) { return false; @@ -350,7 +387,7 @@ public class FileReftableStack implements AutoCloseable { */ @SuppressWarnings("nls") public boolean addReftable(Writer w) throws IOException { - LockFile lock = new LockFile(stackPath); + LockFile lock = new LockFile(tablesListFile); try { if (!lock.lockForAppend()) { return false; @@ -361,8 +398,7 @@ public class FileReftableStack implements AutoCloseable { String fn = filename(nextUpdateIndex(), nextUpdateIndex()); - File tmpTable = File.createTempFile(fn + "_", ".ref", - stackPath.getParentFile()); + File tmpTable = File.createTempFile(fn + "_", ".ref", reftableDir); ReftableWriter.Stats s; try (FileOutputStream fos = new FileOutputStream(tmpTable)) { @@ -416,7 +452,7 @@ public class FileReftableStack implements AutoCloseable { String fn = filename(first, last); File tmpTable = File.createTempFile(fn + "_", ".ref", //$NON-NLS-1$//$NON-NLS-2$ - stackPath.getParentFile()); + reftableDir); try (FileOutputStream fos = new FileOutputStream(tmpTable)) { ReftableCompactor c = new ReftableCompactor(fos) .setConfig(reftableConfig()) @@ -460,7 +496,7 @@ public class FileReftableStack implements AutoCloseable { if (first >= last) { return true; } - LockFile lock = new LockFile(stackPath); + LockFile lock = new LockFile(tablesListFile); File tmpTable = null; List<LockFile> subtableLocks = new ArrayList<>(); @@ -489,7 +525,7 @@ public class FileReftableStack implements AutoCloseable { tmpTable = compactLocked(first, last); - lock = new LockFile(stackPath); + lock = new LockFile(tablesListFile); if (!lock.lock()) { return false; } @@ -559,6 +595,7 @@ public class FileReftableStack implements AutoCloseable { * Calculate an approximate log2. * * @param sz + * the number to compute an approximate log2 for * @return log2 */ static int log(long sz) { @@ -615,6 +652,9 @@ public class FileReftableStack implements AutoCloseable { if (other == null) { return false; } + if (!(other instanceof Segment)) { + return false; + } Segment o = (Segment) other; return o.bytes == bytes && o.log == log && o.start == start && o.end == end; @@ -688,6 +728,7 @@ public class FileReftableStack implements AutoCloseable { * shape. * * @throws IOException + * if an IO error occurred */ private void autoCompact() throws IOException { Optional<Segment> cand = autoCompactCandidate(tableSizes()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java index 3e92cddacd..bcf9f1efdf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java @@ -2,7 +2,7 @@ * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> * Copyright (C) 2008-2010, Google Inc. * Copyright (C) 2006-2010, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2006-2024, Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -31,8 +31,8 @@ import java.util.Locale; import java.util.Objects; import java.util.Set; -import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.attributes.AttributesNode; import org.eclipse.jgit.attributes.AttributesNodeProvider; @@ -60,7 +60,6 @@ import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; -import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; @@ -165,7 +164,7 @@ public class FileRepository extends Repository { throw new IOException(e.getMessage(), e); } repoConfig = new FileBasedConfig(userConfig, getFS().resolve( - getDirectory(), Constants.CONFIG), + getCommonDirectory(), Constants.CONFIG), getFS()); loadRepoConfig(); @@ -193,7 +192,7 @@ public class FileRepository extends Repository { options.getObjectDirectory(), // options.getAlternateObjectDirectories(), // getFS(), // - new File(getDirectory(), Constants.SHALLOW)); + new File(getCommonDirectory(), Constants.SHALLOW)); if (objectDatabase.exists()) { if (repositoryFormatVersion > 1) @@ -215,6 +214,16 @@ public class FileRepository extends Repository { } } + private String getRelativeDir(File base, File other) { + File relPath; + try { + relPath = base.toPath().relativize(other.toPath()).toFile(); + } catch (IllegalArgumentException e) { + relPath = other; + } + return FileUtils.pathToString(relPath); + } + /** * {@inheritDoc} * <p> @@ -223,6 +232,22 @@ public class FileRepository extends Repository { */ @Override public void create(boolean bare) throws IOException { + create(bare, false); + } + + /** + * Create a new Git repository initializing the necessary files and + * directories. + * + * @param bare + * if true, a bare repository (a repository without a working + * directory) is created. + * @param relativePaths + * if true, relative paths are used for GIT_DIR and GIT_WORK_TREE + * @throws IOException + * in case of IO problem + */ + public void create(boolean bare, boolean relativePaths) throws IOException { final FileBasedConfig cfg = getConfig(); if (cfg.getFile().exists()) { throw new IllegalStateException(MessageFormat.format( @@ -293,15 +318,25 @@ public class FileRepository extends Repository { if (!bare) { File workTree = getWorkTree(); if (!getDirectory().getParentFile().equals(workTree)) { + String workTreePath; + String gitDirPath; + if (relativePaths) { + File canonGitDir = getDirectory().getCanonicalFile(); + File canonWorkTree = getWorkTree().getCanonicalFile(); + workTreePath = getRelativeDir(canonGitDir, canonWorkTree); + gitDirPath = getRelativeDir(canonWorkTree, canonGitDir); + } else { + workTreePath = getWorkTree().getAbsolutePath(); + gitDirPath = getDirectory().getAbsolutePath(); + } cfg.setString(ConfigConstants.CONFIG_CORE_SECTION, null, - ConfigConstants.CONFIG_KEY_WORKTREE, getWorkTree() - .getAbsolutePath()); + ConfigConstants.CONFIG_KEY_WORKTREE, workTreePath); LockFile dotGitLockFile = new LockFile(new File(workTree, Constants.DOT_GIT)); try { if (dotGitLockFile.lock()) { dotGitLockFile.write(Constants.encode(Constants.GITDIR - + getDirectory().getAbsolutePath())); + + gitDirPath)); dotGitLockFile.commit(); } } finally { @@ -321,19 +356,16 @@ public class FileRepository extends Repository { return objectDatabase.getDirectory(); } - /** {@inheritDoc} */ @Override public ObjectDirectory getObjectDatabase() { return objectDatabase; } - /** {@inheritDoc} */ @Override public RefDatabase getRefDatabase() { return refs; } - /** {@inheritDoc} */ @Override public String getIdentifier() { File directory = getDirectory(); @@ -343,7 +375,6 @@ public class FileRepository extends Repository { throw new IllegalStateException(); } - /** {@inheritDoc} */ @Override public FileBasedConfig getConfig() { try { @@ -357,7 +388,6 @@ public class FileRepository extends Repository { return repoConfig; } - /** {@inheritDoc} */ @Override @Nullable public String getGitwebDescription() throws IOException { @@ -376,7 +406,6 @@ public class FileRepository extends Repository { return d; } - /** {@inheritDoc} */ @Override public void setGitwebDescription(@Nullable String description) throws IOException { @@ -423,6 +452,7 @@ public class FileRepository extends Repository { * client trying to push changes avoid pushing more than it needs to. * * @throws IOException + * if an IO error occurred */ @Override public Set<ObjectId> getAdditionalHaves() throws IOException { @@ -478,7 +508,6 @@ public class FileRepository extends Repository { objectDatabase.openPack(pack); } - /** {@inheritDoc} */ @Override public void scanForRepoChanges() throws IOException { getRefDatabase().getRefs(); // This will look for changes to refs @@ -504,7 +533,6 @@ public class FileRepository extends Repository { notifyIndexChanged(false); } - /** {@inheritDoc} */ @Override public void notifyIndexChanged(boolean internal) { synchronized (snapshotLock) { @@ -513,31 +541,6 @@ public class FileRepository extends Repository { fireEvent(new IndexChangedEvent(internal)); } - /** {@inheritDoc} */ - @Override - public ReflogReader getReflogReader(String refName) throws IOException { - if (refs instanceof FileReftableDatabase) { - // Cannot use findRef: reftable stores log data for deleted or renamed - // branches. - return ((FileReftableDatabase)refs).getReflogReader(refName); - } - - // TODO: use exactRef here, which offers more predictable and therefore preferable - // behavior. - Ref ref = findRef(refName); - if (ref == null) { - return null; - } - return new ReflogReaderImpl(this, ref.getName()); - } - - @Override - public @NonNull ReflogReader getReflogReader(@NonNull Ref ref) - throws IOException { - return new ReflogReaderImpl(this, ref.getName()); - } - - /** {@inheritDoc} */ @Override public AttributesNodeProvider createAttributesNodeProvider() { return new AttributesNodeProviderImpl(this); @@ -600,18 +603,16 @@ public class FileRepository extends Repository { ConfigConstants.CONFIG_KEY_AUTODETACH, true); } - /** {@inheritDoc} */ @SuppressWarnings("FutureReturnValueIgnored") @Override public void autoGC(ProgressMonitor monitor) { GC gc = new GC(this); - gc.setPackConfig(new PackConfig(this)); gc.setProgressMonitor(monitor); gc.setAuto(true); gc.setBackground(shouldAutoDetach()); try { gc.gc(); - } catch (ParseException | IOException e) { + } catch (ParseException | IOException | GitAPIException e) { throw new JGitInternalException(JGitText.get().gcFailed, e); } } @@ -632,16 +633,17 @@ public class FileRepository extends Repository { * on IO problem */ void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException { + File commonDirectory = getCommonDirectory(); List<Ref> all = refs.getRefs(); - File packedRefs = new File(getDirectory(), Constants.PACKED_REFS); + File packedRefs = new File(commonDirectory, Constants.PACKED_REFS); if (packedRefs.exists()) { throw new IOException(MessageFormat.format(JGitText.get().fileAlreadyExists, packedRefs.getName())); } - File refsFile = new File(getDirectory(), "refs"); //$NON-NLS-1$ + File refsFile = new File(commonDirectory, "refs"); //$NON-NLS-1$ File refsHeadsFile = new File(refsFile, "heads");//$NON-NLS-1$ - File headFile = new File(getDirectory(), Constants.HEAD); + File headFile = new File(commonDirectory, Constants.HEAD); FileReftableDatabase oldDb = (FileReftableDatabase) refs; // Remove the dummy files that ensure compatibility with older git @@ -671,8 +673,8 @@ public class FileRepository extends Repository { } if (writeLogs) { - List<ReflogEntry> logs = oldDb.getReflogReader(r.getName()) - .getReverseEntries(); + ReflogReader reflogReader = oldDb.getReflogReader(r); + List<ReflogEntry> logs = reflogReader.getReverseEntries(); Collections.reverse(logs); for (ReflogEntry e : logs) { logWriter.log(r.getName(), e); @@ -711,12 +713,14 @@ public class FileRepository extends Repository { } if (!backup) { - File reftableDir = new File(getDirectory(), Constants.REFTABLE); + File reftableDir = new File(commonDirectory, Constants.REFTABLE); FileUtils.delete(reftableDir, FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS); } repoConfig.unset(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null, ConfigConstants.CONFIG_KEY_REF_STORAGE); + repoConfig.setLong(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0); repoConfig.save(); } @@ -740,8 +744,10 @@ public class FileRepository extends Repository { @SuppressWarnings("nls") void convertToReftable(boolean writeLogs, boolean backup) throws IOException { - File reftableDir = new File(getDirectory(), Constants.REFTABLE); - File headFile = new File(getDirectory(), Constants.HEAD); + File commonDirectory = getCommonDirectory(); + File directory = getDirectory(); + File reftableDir = new File(commonDirectory, Constants.REFTABLE); + File headFile = new File(directory, Constants.HEAD); if (reftableDir.exists() && FileUtils.hasFiles(reftableDir.toPath())) { throw new IOException(JGitText.get().reftableDirExists); } @@ -749,28 +755,28 @@ public class FileRepository extends Repository { // Ignore return value, as it is tied to temporary newRefs file. FileReftableDatabase.convertFrom(this, writeLogs); - File refsFile = new File(getDirectory(), "refs"); + File refsFile = new File(commonDirectory, "refs"); // non-atomic: remove old data. - File packedRefs = new File(getDirectory(), Constants.PACKED_REFS); - File logsDir = new File(getDirectory(), Constants.LOGS); + File packedRefs = new File(commonDirectory, Constants.PACKED_REFS); + File logsDir = new File(commonDirectory, Constants.LOGS); List<String> additional = getRefDatabase().getAdditionalRefs().stream() .map(Ref::getName).collect(toList()); additional.add(Constants.HEAD); if (backup) { - FileUtils.rename(refsFile, new File(getDirectory(), "refs.old")); + FileUtils.rename(refsFile, new File(commonDirectory, "refs.old")); if (packedRefs.exists()) { - FileUtils.rename(packedRefs, new File(getDirectory(), + FileUtils.rename(packedRefs, new File(commonDirectory, Constants.PACKED_REFS + ".old")); } if (logsDir.exists()) { FileUtils.rename(logsDir, - new File(getDirectory(), Constants.LOGS + ".old")); + new File(commonDirectory, Constants.LOGS + ".old")); } for (String r : additional) { - FileUtils.rename(new File(getDirectory(), r), - new File(getDirectory(), r + ".old")); + FileUtils.rename(new File(commonDirectory, r), + new File(commonDirectory, r + ".old")); } } else { FileUtils.delete(packedRefs, FileUtils.SKIP_MISSING); @@ -780,7 +786,7 @@ public class FileRepository extends Repository { FileUtils.delete(refsFile, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); for (String r : additional) { - new File(getDirectory(), r).delete(); + new File(commonDirectory, r).delete(); } } @@ -794,7 +800,7 @@ public class FileRepository extends Repository { // Some tools might write directly into .git/refs/heads/BRANCH. By // putting a file here, this fails spectacularly. - FileUtils.createNewFile(new File(refsFile, "heads")); + FileUtils.createNewFile(new File(refsFile, Constants.HEADS)); repoConfig.setString(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null, ConfigConstants.CONFIG_KEY_REF_STORAGE, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java index 6088c152a1..b6bde6eea0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java @@ -15,6 +15,7 @@ import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_TIMESTAMP_RE import java.io.File; import java.io.IOException; +import java.nio.file.FileSystemException; import java.nio.file.NoSuchFileException; import java.nio.file.attribute.BasicFileAttributes; import java.time.Duration; @@ -22,10 +23,12 @@ import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Locale; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS.FileStoreAttributes; import org.slf4j.Logger; @@ -139,29 +142,6 @@ public class FileSnapshot { * @param modified * the last modification time of the file * @return the snapshot. - * @deprecated use {@link #save(Instant)} instead. - */ - @Deprecated - public static FileSnapshot save(long modified) { - final Instant read = Instant.now(); - return new FileSnapshot(read, Instant.ofEpochMilli(modified), - UNKNOWN_SIZE, FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY); - } - - /** - * Record a snapshot for a file for which the last modification time is - * already known. - * <p> - * This method should be invoked before the file is accessed. - * <p> - * Note that this method cannot rely on measuring file timestamp resolution - * to avoid racy git issues caused by finite file timestamp resolution since - * it's unknown in which filesystem the file is located. Hence the worst - * case fallback for timestamp resolution is used. - * - * @param modified - * the last modification time of the file - * @return the snapshot. */ public static FileSnapshot save(Instant modified) { final Instant read = Instant.now(); @@ -231,14 +211,8 @@ public class FileSnapshot { this.useConfig = useConfig; BasicFileAttributes fileAttributes = null; try { - fileAttributes = FS.DETECTED.fileAttributes(file); - } catch (NoSuchFileException e) { - this.lastModified = Instant.EPOCH; - this.size = 0L; - this.fileKey = MISSING_FILEKEY; - return; - } catch (IOException e) { - LOG.error(e.getMessage(), e); + fileAttributes = getFileAttributes(file); + } catch (NoSuchElementException e) { this.lastModified = Instant.EPOCH; this.size = 0L; this.fileKey = MISSING_FILEKEY; @@ -282,23 +256,14 @@ public class FileSnapshot { * Get time of last snapshot update * * @return time of last snapshot update - * @deprecated use {@link #lastModifiedInstant()} instead - */ - @Deprecated - public long lastModified() { - return lastModified.toEpochMilli(); - } - - /** - * Get time of last snapshot update - * - * @return time of last snapshot update */ public Instant lastModifiedInstant() { return lastModified; } /** + * Get file size in bytes of last snapshot update + * * @return file size in bytes of last snapshot update */ public long size() { @@ -317,16 +282,11 @@ public class FileSnapshot { long currSize; Object currFileKey; try { - BasicFileAttributes fileAttributes = FS.DETECTED.fileAttributes(path); + BasicFileAttributes fileAttributes = getFileAttributes(path); currLastModified = fileAttributes.lastModifiedTime().toInstant(); currSize = fileAttributes.size(); currFileKey = getFileKey(fileAttributes); - } catch (NoSuchFileException e) { - currLastModified = Instant.EPOCH; - currSize = 0L; - currFileKey = MISSING_FILEKEY; - } catch (IOException e) { - LOG.error(e.getMessage(), e); + } catch (NoSuchElementException e) { currLastModified = Instant.EPOCH; currSize = 0L; currFileKey = MISSING_FILEKEY; @@ -404,7 +364,6 @@ public class FileSnapshot { && Objects.equals(fileKey, other.fileKey); } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { @@ -420,45 +379,53 @@ public class FileSnapshot { return equals(other); } - /** {@inheritDoc} */ @Override public int hashCode() { return Objects.hash(lastModified, Long.valueOf(size), fileKey); } /** - * @return {@code true} if FileSnapshot.isModified(File) found the file size - * changed + * Whether #isModified(File) found the file size changed + * + * @return {@code true} if #isModified(File) found the file size changed */ boolean wasSizeChanged() { return sizeChanged; } /** - * @return {@code true} if FileSnapshot.isModified(File) found the file key - * changed + * Whether #isModified(File) found the file key changed + * + * @return {@code true} if #isModified(File) found the file key changed */ boolean wasFileKeyChanged() { return fileKeyChanged; } /** - * @return {@code true} if FileSnapshot.isModified(File) found the file's - * lastModified changed + * Whether #isModified(File) found the file's lastModified changed + * + * @return {@code true} if #isModified(File) found the file's lastModified + * changed */ boolean wasLastModifiedChanged() { return lastModifiedChanged; } /** - * @return {@code true} if FileSnapshot.isModified(File) detected that - * lastModified is racily clean + * Whether #isModified(File) detected that lastModified is racily clean + * + * @return {@code true} if #isModified(File) detected that lastModified is + * racily clean */ boolean wasLastModifiedRacilyClean() { return wasRacyClean; } /** + * Get the delta in nanoseconds between lastModified and lastRead during + * last racy check + * * @return the delta in nanoseconds between lastModified and lastRead during * last racy check */ @@ -467,6 +434,8 @@ public class FileSnapshot { } /** + * Get the racyLimitNanos threshold in nanoseconds during last racy check + * * @return the racyLimitNanos threshold in nanoseconds during last racy * check */ @@ -474,7 +443,6 @@ public class FileSnapshot { return racyThreshold; } - /** {@inheritDoc} */ @SuppressWarnings({ "nls", "ReferenceEquality" }) @Override public String toString() { @@ -576,4 +544,27 @@ public class FileSnapshot { } return fileStoreAttributeCache; } + + private static BasicFileAttributes getFileAttributes(File path) + throws NoSuchElementException { + try { + try { + return FS.DETECTED.fileAttributes(path); + } catch (IOException e) { + if (!FileUtils.isStaleFileHandle(e)) { + throw e; + } + } + } catch (NoSuchFileException e) { + // ignore + } catch (FileSystemException e) { + String msg = e.getMessage(); + if (!msg.endsWith("Not a directory")) { //$NON-NLS-1$ + LOG.error(msg, e); + } + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + throw new NoSuchElementException(path.toString()); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java index be359bbeaa..97473bba2a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java @@ -11,8 +11,10 @@ package org.eclipse.jgit.internal.storage.file; import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX; +import static org.eclipse.jgit.internal.storage.pack.PackExt.COMMIT_GRAPH; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP; +import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import static org.eclipse.jgit.internal.storage.pack.PackExt.REVERSE_INDEX; @@ -48,7 +50,6 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -63,6 +64,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.api.PackRefsCommand; +import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.CancelledException; import org.eclipse.jgit.errors.CorruptObjectException; @@ -74,6 +77,7 @@ import org.eclipse.jgit.internal.storage.commitgraph.CommitGraphWriter; import org.eclipse.jgit.internal.storage.commitgraph.GraphCommits; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.pack.PackWriter; +import org.eclipse.jgit.internal.util.ShutdownHook; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig; @@ -99,7 +103,7 @@ import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS.LockToken; import org.eclipse.jgit.util.FileUtils; -import org.eclipse.jgit.util.GitDateParser; +import org.eclipse.jgit.util.GitTimeParser; import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; @@ -128,12 +132,14 @@ public class GC { private static final Set<PackExt> PARENT_EXTS = Set.of(PACK, KEEP); private static final Set<PackExt> CHILD_EXTS = Set.of(BITMAP_INDEX, INDEX, - REVERSE_INDEX); + REVERSE_INDEX, OBJECT_SIZE_INDEX); private static final int DEFAULT_AUTOPACKLIMIT = 50; private static final int DEFAULT_AUTOLIMIT = 6700; + private static final boolean DEFAULT_WRITE_BLOOM_FILTER = false; + private static final boolean DEFAULT_WRITE_COMMIT_GRAPH = false; private static volatile ExecutorService executor; @@ -155,11 +161,13 @@ public class GC { private long expireAgeMillis = -1; - private Date expire; + private Instant expire; private long packExpireAgeMillis = -1; - private Date packExpire; + private Instant packExpire; + + private Boolean packKeptObjects; private PackConfig pconfig; @@ -176,7 +184,7 @@ public class GC { * prune() to inspect only those reflog entries which have been added since * last repack(). */ - private long lastRepackTime; + private Instant lastRepackTime; /** * Whether gc should do automatic housekeeping @@ -221,15 +229,18 @@ public class GC { * gc.log. * * @return the collection of - * {@link org.eclipse.jgit.internal.storage.file.Pack}'s which - * are newly created + * {@link org.eclipse.jgit.internal.storage.file.Pack}'s which are + * newly created * @throws java.io.IOException + * if an IO error occurred * @throws java.text.ParseException * If the configuration parameter "gc.pruneexpire" couldn't be * parsed + * @throws GitAPIException + * If packing refs failed */ public CompletableFuture<Collection<Pack>> gc() - throws IOException, ParseException { + throws IOException, ParseException, GitAPIException { if (!background) { return CompletableFuture.completedFuture(doGc()); } @@ -248,7 +259,7 @@ public class GC { gcLog.commit(); } return newPacks; - } catch (IOException | ParseException e) { + } catch (IOException | ParseException | GitAPIException e) { try { gcLog.write(e.getMessage()); StringWriter sw = new StringWriter(); @@ -271,7 +282,8 @@ public class GC { return (executor != null) ? executor : WorkQueue.getExecutor(); } - private Collection<Pack> doGc() throws IOException, ParseException { + private Collection<Pack> doGc() + throws IOException, ParseException, GitAPIException { if (automatic && !needGc()) { return Collections.emptyList(); } @@ -280,7 +292,8 @@ public class GC { return Collections.emptyList(); } pm.start(6 /* tasks */); - packRefs(); + new PackRefsCommand(repo).setProgressMonitor(pm).setAll(true) + .call(); // TODO: implement reflog_expire(pm, repo); Collection<Pack> newPacks = repack(); prune(Collections.emptySet()); @@ -297,10 +310,15 @@ public class GC { * pack files. * * @param inserter + * used to insert objects * @param reader + * used to read objects * @param pack + * the pack file to loosen objects for * @param existing + * existing objects * @throws IOException + * if an IO error occurred */ private void loosen(ObjectDirectoryInserter inserter, ObjectReader reader, Pack pack, HashSet<ObjectId> existing) throws IOException { @@ -326,13 +344,17 @@ public class GC { * directory. If an expirationDate is set then pack files which are younger * than the expirationDate will not be deleted nor preserved. * <p> - * If we're not immediately expiring loose objects, loosen any objects - * in the old pack files which aren't in the new pack files. + * If we're not immediately expiring loose objects, loosen any objects in + * the old pack files which aren't in the new pack files. * * @param oldPacks + * old pack files * @param newPacks + * new pack files * @throws ParseException + * if an error occurred during parsing * @throws IOException + * if an IO error occurred */ private void deleteOldPacks(Collection<Pack> oldPacks, Collection<Pack> newPacks) throws ParseException, IOException { @@ -350,6 +372,7 @@ public class GC { prunePreserved(); long packExpireDate = getPackExpireDate(); + List<PackFile> packFilesToPrune = new ArrayList<>(); oldPackLoop: for (Pack oldPack : oldPacks) { checkCancelled(); String oldName = oldPack.getPackName(); @@ -367,9 +390,10 @@ public class GC { loosen(inserter, reader, oldPack, ids); } oldPack.close(); - prunePack(oldPack.getPackFile()); + packFilesToPrune.add(oldPack.getPackFile()); } } + packFilesToPrune.forEach(this::prunePack); // close the complete object database. That's my only chance to force // rescanning and to detect that certain pack files are now deleted. @@ -377,15 +401,22 @@ public class GC { } /** - * Deletes old pack file, unless 'preserve-oldpacks' is set, in which case it - * moves the pack file to the preserved directory + * Deletes old pack file, unless 'preserve-oldpacks' is set, in which case + * it moves the pack file to the preserved directory * * @param packFile + * the packfile to delete * @param deleteOptions + * delete option flags * @throws IOException + * if an IO error occurred */ private void removeOldPack(PackFile packFile, int deleteOptions) throws IOException { + if (!packFile.exists()) { + return; + } + if (pconfig.isPreserveOldPacks()) { File oldPackDir = repo.getObjectDatabase().getPreservedDirectory(); FileUtils.mkdir(oldPackDir, true); @@ -421,6 +452,7 @@ public class GC { * with a ".pack" file without a ".index" file. * * @param packFile + * the pack file to prune files for */ private void prunePack(PackFile packFile) { try { @@ -448,6 +480,7 @@ public class GC { * because the filesystem delete operation fails) this is silently ignored. * * @throws java.io.IOException + * if an IO error occurred */ public void prunePacked() throws IOException { ObjectDirectory objdb = repo.getObjectDatabase(); @@ -506,6 +539,7 @@ public class GC { * @param objectsToKeep * a set of objects which should explicitly not be pruned * @throws java.io.IOException + * if an IO error occurred * @throws java.text.ParseException * If the configuration parameter "gc.pruneexpire" couldn't be * parsed @@ -669,16 +703,18 @@ public class GC { if (expire == null && expireAgeMillis == -1) { String pruneExpireStr = getPruneExpireStr(); - if (pruneExpireStr == null) + if (pruneExpireStr == null) { pruneExpireStr = PRUNE_EXPIRE_DEFAULT; - expire = GitDateParser.parse(pruneExpireStr, null, SystemReader - .getInstance().getLocale()); + } + expire = GitTimeParser.parseInstant(pruneExpireStr); expireAgeMillis = -1; } - if (expire != null) - expireDate = expire.getTime(); - if (expireAgeMillis != -1) + if (expire != null) { + expireDate = expire.toEpochMilli(); + } + if (expireAgeMillis != -1) { expireDate = System.currentTimeMillis() - expireAgeMillis; + } return expireDate; } @@ -695,16 +731,18 @@ public class GC { String prunePackExpireStr = repo.getConfig().getString( ConfigConstants.CONFIG_GC_SECTION, null, ConfigConstants.CONFIG_KEY_PRUNEPACKEXPIRE); - if (prunePackExpireStr == null) + if (prunePackExpireStr == null) { prunePackExpireStr = PRUNE_PACK_EXPIRE_DEFAULT; - packExpire = GitDateParser.parse(prunePackExpireStr, null, - SystemReader.getInstance().getLocale()); + } + packExpire = GitTimeParser.parseInstant(prunePackExpireStr); packExpireAgeMillis = -1; } - if (packExpire != null) - packExpireDate = packExpire.getTime(); - if (packExpireAgeMillis != -1) + if (packExpire != null) { + packExpireDate = packExpire.toEpochMilli(); + } + if (packExpireAgeMillis != -1) { packExpireDate = System.currentTimeMillis() - packExpireAgeMillis; + } return packExpireDate; } @@ -713,10 +751,15 @@ public class GC { * by the given ObjectWalk * * @param id2File + * mapping objectIds to files * @param w + * used to walk objects * @throws MissingObjectException + * if an object is missing * @throws IncorrectObjectTypeException + * if an object has an unexpected tyoe * @throws IOException + * if an IO error occurred */ private void removeReferenced(Map<ObjectId, File> id2File, ObjectWalk w) throws MissingObjectException, @@ -752,42 +795,6 @@ public class GC { } /** - * Pack ref storage. For a RefDirectory database, this packs all - * non-symbolic, loose refs into packed-refs. For Reftable, all of the data - * is compacted into a single table. - * - * @throws java.io.IOException - */ - public void packRefs() throws IOException { - RefDatabase refDb = repo.getRefDatabase(); - if (refDb instanceof FileReftableDatabase) { - // TODO: abstract this more cleanly. - pm.beginTask(JGitText.get().packRefs, 1); - try { - ((FileReftableDatabase) refDb).compactFully(); - } finally { - pm.endTask(); - } - return; - } - - Collection<Ref> refs = refDb.getRefsByPrefix(Constants.R_REFS); - List<String> refsToBePacked = new ArrayList<>(refs.size()); - pm.beginTask(JGitText.get().packRefs, refs.size()); - try { - for (Ref ref : refs) { - checkCancelled(); - if (!ref.isSymbolic() && ref.getStorage().isLoose()) - refsToBePacked.add(ref.getName()); - pm.update(1); - } - ((RefDirectory) repo.getRefDatabase()).pack(refsToBePacked); - } finally { - pm.endTask(); - } - } - - /** * Packs all objects which reachable from any of the heads into one pack * file. Additionally all objects which are not reachable from any head but * which are reachable from any of the other refs (e.g. tags), special refs @@ -804,14 +811,13 @@ public class GC { public Collection<Pack> repack() throws IOException { Collection<Pack> toBeDeleted = repo.getObjectDatabase().getPacks(); - long time = System.currentTimeMillis(); + Instant time = SystemReader.getInstance().now(); Collection<Ref> refsBefore = getAllRefs(); Set<ObjectId> allHeadsAndTags = new HashSet<>(); Set<ObjectId> allHeads = new HashSet<>(); Set<ObjectId> allTags = new HashSet<>(); Set<ObjectId> nonHeads = new HashSet<>(); - Set<ObjectId> txnHeads = new HashSet<>(); Set<ObjectId> tagTargets = new HashSet<>(); Set<ObjectId> indexObjects = listNonHEADIndexObjects(); @@ -821,7 +827,7 @@ public class GC { for (Ref ref : refsBefore) { checkCancelled(); - nonHeads.addAll(listRefLogObjects(ref, 0)); + nonHeads.addAll(listRefLogObjects(ref, Instant.EPOCH)); if (ref.isSymbolic() || ref.getObjectId() == null) { continue; } @@ -837,11 +843,12 @@ public class GC { } } - List<ObjectIdSet> excluded = new LinkedList<>(); + List<ObjectIdSet> excluded = new ArrayList<>(); for (Pack p : repo.getObjectDatabase().getPacks()) { checkCancelled(); - if (p.shouldBeKept()) + if (!shouldPackKeptObjects() && p.shouldBeKept()) { excluded.add(p.getIndex()); + } } // Don't exclude tags that are also branch tips @@ -863,7 +870,7 @@ public class GC { Pack heads = null; if (!allHeadsAndTags.isEmpty()) { heads = writePack(allHeadsAndTags, PackWriter.NONE, allTags, - refsToExcludeFromBitmap, tagTargets, excluded); + refsToExcludeFromBitmap, tagTargets, excluded, true); if (heads != null) { ret.add(heads); excluded.add(0, heads.getIndex()); @@ -871,16 +878,10 @@ public class GC { } if (!nonHeads.isEmpty()) { Pack rest = writePack(nonHeads, allHeadsAndTags, PackWriter.NONE, - PackWriter.NONE, tagTargets, excluded); + PackWriter.NONE, tagTargets, excluded, false); if (rest != null) ret.add(rest); } - if (!txnHeads.isEmpty()) { - Pack txn = writePack(txnHeads, PackWriter.NONE, PackWriter.NONE, - PackWriter.NONE, null, excluded); - if (txn != null) - ret.add(txn); - } try { deleteOldPacks(toBeDeleted, ret); } catch (ParseException e) { @@ -926,6 +927,7 @@ public class GC { * the list of wanted objects, writer walks commits starting at * these. Must not be {@code null}. * @throws IOException + * if an IO error occurred */ void writeCommitGraph(@NonNull Set<? extends ObjectId> wants) throws IOException { @@ -942,8 +944,10 @@ public class GC { File tmpFile = null; try (RevWalk walk = new RevWalk(repo)) { CommitGraphWriter writer = new CommitGraphWriter( - GraphCommits.fromWalk(pm, wants, walk)); - tmpFile = File.createTempFile("commit_", ".graph_tmp", //$NON-NLS-1$//$NON-NLS-2$ + GraphCommits.fromWalk(pm, wants, walk), + shouldWriteBloomFilter()); + tmpFile = File.createTempFile("commit_", //$NON-NLS-1$ + COMMIT_GRAPH.getTmpExtension(), repo.getObjectDatabase().getInfoDirectory()); // write the commit-graph file try (FileOutputStream fos = new FileOutputStream(tmpFile); @@ -993,7 +997,7 @@ public class GC { /** * If {@code true}, will rewrite the commit-graph file when gc is run. * - * @return true if commit-graph should be writen. Default is {@code false}. + * @return true if commit-graph should be written. Default is {@code false}. */ boolean shouldWriteCommitGraphWhenGc() { return repo.getConfig().getBoolean(ConfigConstants.CONFIG_GC_SECTION, @@ -1001,6 +1005,17 @@ public class GC { DEFAULT_WRITE_COMMIT_GRAPH); } + /** + * If {@code true}, generates bloom filter in the commit-graph file. + * + * @return true if bloom filter should be written. Default is {@code false}. + */ + boolean shouldWriteBloomFilter() { + return repo.getConfig().getBoolean(ConfigConstants.CONFIG_GC_SECTION, + ConfigConstants.CONFIG_KEY_WRITE_CHANGED_PATHS, + DEFAULT_WRITE_BLOOM_FILTER); + } + private static boolean isHead(Ref ref) { return ref.getName().startsWith(Constants.R_HEADS); } @@ -1010,7 +1025,7 @@ public class GC { } private void deleteEmptyRefsFolders() throws IOException { - Path refs = repo.getDirectory().toPath().resolve(Constants.R_REFS); + Path refs = repo.getCommonDirectory().toPath().resolve(Constants.R_REFS); // Avoid deleting a folder that was created after the threshold so that concurrent // operations trying to create a reference are not impacted Instant threshold = Instant.now().minus(30, ChronoUnit.SECONDS); @@ -1141,20 +1156,24 @@ public class GC { /** * @param ref * the ref which log should be inspected - * @param minTime only reflog entries not older then this time are processed + * @param minTime + * only reflog entries equal or younger than this time are + * processed * @return the {@link ObjectId}s contained in the reflog * @throws IOException + * if an IO error occurred */ - private Set<ObjectId> listRefLogObjects(Ref ref, long minTime) throws IOException { - ReflogReader reflogReader = repo.getReflogReader(ref); + private Set<ObjectId> listRefLogObjects(Ref ref, Instant minTime) throws IOException { + ReflogReader reflogReader = repo.getRefDatabase().getReflogReader(ref); List<ReflogEntry> rlEntries = reflogReader .getReverseEntries(); if (rlEntries == null || rlEntries.isEmpty()) return Collections.emptySet(); Set<ObjectId> ret = new HashSet<>(); for (ReflogEntry e : rlEntries) { - if (e.getWho().getWhen().getTime() < minTime) + if (e.getWho().getWhenAsInstant().isBefore(minTime)) { break; + } ObjectId newId = e.getNewId(); if (newId != null && !ObjectId.zeroId().equals(newId)) ret.add(newId); @@ -1175,6 +1194,7 @@ public class GC { * * @return a collection of refs pointing to live objects. * @throws IOException + * if an IO error occurred */ private Collection<Ref> getAllRefs() throws IOException { RefDatabase refdb = repo.getRefDatabase(); @@ -1201,8 +1221,11 @@ public class GC { * * @return a set of ObjectIds of changed objects in the index * @throws IOException + * if an IO error occurred * @throws CorruptObjectException + * if an object is corrupt * @throws NoWorkTreeException + * if the repository has no working directory */ private Set<ObjectId> listNonHEADIndexObjects() throws CorruptObjectException, IOException { @@ -1251,7 +1274,7 @@ public class GC { private Pack writePack(@NonNull Set<? extends ObjectId> want, @NonNull Set<? extends ObjectId> have, @NonNull Set<ObjectId> tags, @NonNull Set<ObjectId> excludedRefsTips, - Set<ObjectId> tagTargets, List<ObjectIdSet> excludeObjects) + Set<ObjectId> tagTargets, List<ObjectIdSet> excludeObjects, boolean createBitmap) throws IOException { checkCancelled(); File tmpPack = null; @@ -1282,6 +1305,7 @@ public class GC { if (excludeObjects != null) for (ObjectIdSet idx : excludeObjects) pw.excludeObjects(idx); + pw.setCreateBitmaps(createBitmap); pw.preparePack(pm, want, have, PackWriter.NONE, union(tags, excludedRefsTips)); if (pw.getObjectCount() == 0) @@ -1292,10 +1316,11 @@ public class GC { ObjectId id = pw.computeName(); File packdir = repo.getObjectDatabase().getPackDirectory(); packdir.mkdirs(); - tmpPack = File.createTempFile("gc_", ".pack_tmp", packdir); //$NON-NLS-1$ //$NON-NLS-2$ - final String tmpBase = tmpPack.getName() + tmpPack = File.createTempFile("gc_", //$NON-NLS-1$ + PACK.getTmpExtension(), packdir); + String tmpBase = tmpPack.getName() .substring(0, tmpPack.getName().lastIndexOf('.')); - File tmpIdx = new File(packdir, tmpBase + ".idx_tmp"); //$NON-NLS-1$ + File tmpIdx = new File(packdir, tmpBase + INDEX.getTmpExtension()); tmpExts.put(INDEX, tmpIdx); if (!tmpIdx.createNewFile()) @@ -1320,8 +1345,42 @@ public class GC { idxChannel.force(true); } + + if (pw.isReverseIndexEnabled()) { + File tmpReverseIndexFile = new File(packdir, + tmpBase + REVERSE_INDEX.getTmpExtension()); + tmpExts.put(REVERSE_INDEX, tmpReverseIndexFile); + if (!tmpReverseIndexFile.createNewFile()) { + throw new IOException(MessageFormat.format( + JGitText.get().cannotCreateIndexfile, + tmpReverseIndexFile.getPath())); + } + try (FileOutputStream fos = new FileOutputStream( + tmpReverseIndexFile); + FileChannel channel = fos.getChannel(); + OutputStream stream = Channels + .newOutputStream(channel)) { + pw.writeReverseIndex(stream); + channel.force(true); + } + } + + // write the object size + if (pconfig.isWriteObjSizeIndex()) { + File tmpSizeIdx = new File(packdir, tmpBase + ".objsize_tmp"); //$NON-NLS-1$ + tmpExts.put(OBJECT_SIZE_INDEX, tmpSizeIdx); + try (FileOutputStream fos = new FileOutputStream(tmpSizeIdx); + FileChannel idxChannel = fos.getChannel(); + OutputStream idxStream = Channels + .newOutputStream(idxChannel)) { + pw.writeObjectSizeIndex(idxStream); + idxChannel.force(true); + } + } + if (pw.prepareBitmapIndex(pm)) { - File tmpBitmapIdx = new File(packdir, tmpBase + ".bitmap_tmp"); //$NON-NLS-1$ + File tmpBitmapIdx = new File(packdir, + tmpBase + BITMAP_INDEX.getTmpExtension()); tmpExts.put(BITMAP_INDEX, tmpBitmapIdx); if (!tmpBitmapIdx.createNewFile()) @@ -1333,7 +1392,7 @@ public class GC { FileChannel idxChannel = fos.getChannel(); OutputStream idxStream = Channels .newOutputStream(idxChannel)) { - pw.writeBitmapIndex(idxStream); + pw.writeBitmapIndex(new PackBitmapIndexWriterV1(idxStream)); idxChannel.force(true); } } @@ -1412,6 +1471,21 @@ public class GC { } /** + * Define whether to include objects in `.keep` files when repacking. + * + * @param packKeptObjects Whether to include objects in `.keep` files when repacking. + */ + public void setPackKeptObjects(boolean packKeptObjects) { + this.packKeptObjects = Boolean.valueOf(packKeptObjects); + } + + @SuppressWarnings("boxing") + private boolean shouldPackKeptObjects() { + return Optional.ofNullable(packKeptObjects) + .orElse(pconfig.isPackKeptObjects()); + } + + /** * A class holding statistical data for a FileRepository regarding how many * objects are stored as loose or packed objects */ @@ -1429,6 +1503,18 @@ public class GC { public long numberOfPackFiles; /** + * The number of pack files that were created since the last bitmap + * generation. + */ + public long numberOfPackFilesSinceBitmap; + + /** + * The number of objects stored in pack files and as loose object + * created after the last bitmap generation. + */ + public long numberOfObjectsSinceBitmap; + + /** * The number of objects stored as loose objects. */ public long numberOfLooseObjects; @@ -1458,17 +1544,28 @@ public class GC { */ public long numberOfBitmaps; + /** + * The number of objects in the size-index of the packs + */ + public long numberOfSizeIndexedObjects; + @Override public String toString() { final StringBuilder b = new StringBuilder(); b.append("numberOfPackedObjects=").append(numberOfPackedObjects); //$NON-NLS-1$ b.append(", numberOfPackFiles=").append(numberOfPackFiles); //$NON-NLS-1$ + b.append(", numberOfPackFilesSinceBitmap=") //$NON-NLS-1$ + .append(numberOfPackFilesSinceBitmap); + b.append(", numberOfObjectsSinceBitmap=") //$NON-NLS-1$ + .append(numberOfObjectsSinceBitmap); b.append(", numberOfLooseObjects=").append(numberOfLooseObjects); //$NON-NLS-1$ b.append(", numberOfLooseRefs=").append(numberOfLooseRefs); //$NON-NLS-1$ b.append(", numberOfPackedRefs=").append(numberOfPackedRefs); //$NON-NLS-1$ b.append(", sizeOfLooseObjects=").append(sizeOfLooseObjects); //$NON-NLS-1$ b.append(", sizeOfPackedObjects=").append(sizeOfPackedObjects); //$NON-NLS-1$ b.append(", numberOfBitmaps=").append(numberOfBitmaps); //$NON-NLS-1$ + b.append(", numberOfSizeIndexedObjects=") //$NON-NLS-1$ + .append(numberOfSizeIndexedObjects); return b.toString(); } } @@ -1477,17 +1574,28 @@ public class GC { * Returns information about objects and pack files for a FileRepository. * * @return information about objects and pack files for a FileRepository - * @throws java.io.IOException + * @throws java.io.IOException + * if an IO error occurred */ public RepoStatistics getStatistics() throws IOException { RepoStatistics ret = new RepoStatistics(); Collection<Pack> packs = repo.getObjectDatabase().getPacks(); + long latestBitmapTime = 0L; for (Pack p : packs) { - ret.numberOfPackedObjects += p.getIndex().getObjectCount(); + long packedObjects = p.getIndex().getObjectCount(); + ret.numberOfPackedObjects += packedObjects; ret.numberOfPackFiles++; ret.sizeOfPackedObjects += p.getPackFile().length(); - if (p.getBitmapIndex() != null) + ret.numberOfSizeIndexedObjects += p.getObjectSizeIndexCount(); + if (p.getBitmapIndex() != null) { ret.numberOfBitmaps += p.getBitmapIndex().getBitmapCount(); + if (latestBitmapTime == 0L) { + latestBitmapTime = p.getFileSnapshot().lastModifiedInstant().toEpochMilli(); + } + } else if (latestBitmapTime == 0L) { + ret.numberOfPackFilesSinceBitmap++; + ret.numberOfObjectsSinceBitmap += packedObjects; + } } File objDir = repo.getObjectsDirectory(); String[] fanout = objDir.list(); @@ -1503,6 +1611,9 @@ public class GC { continue; ret.numberOfLooseObjects++; ret.sizeOfLooseObjects += f.length(); + if (f.lastModified() > latestBitmapTime) { + ret.numberOfObjectsSinceBitmap ++; + } } } } @@ -1578,12 +1689,31 @@ public class GC { * candidate for pruning. * * @param expire - * instant in time which defines object expiration - * objects with modification time before this instant are expired - * objects with modification time newer or equal to this instant - * are not expired + * instant in time which defines object expiration objects with + * modification time before this instant are expired objects with + * modification time newer or equal to this instant are not + * expired + * @deprecated use {@link #setExpire(Instant)} instead */ + @Deprecated(since = "7.2") public void setExpire(Date expire) { + this.expire = expire.toInstant(); + expireAgeMillis = -1; + } + + /** + * During gc() or prune() each unreferenced, loose object which has been + * created or modified after or at <code>expire</code> will not be pruned. + * Only older objects may be pruned. If set to null then every object is a + * candidate for pruning. + * + * @param expire + * instant in time which defines object expiration objects with + * modification time before this instant are expired objects with + * modification time newer or equal to this instant are not + * expired + */ + public void setExpire(Instant expire) { this.expire = expire; expireAgeMillis = -1; } @@ -1596,8 +1726,24 @@ public class GC { * * @param packExpire * instant in time which defines packfile expiration + * @deprecated use {@link #setPackExpire(Instant)} instead */ + @Deprecated(since = "7.2") public void setPackExpire(Date packExpire) { + this.packExpire = packExpire.toInstant(); + packExpireAgeMillis = -1; + } + + /** + * During gc() or prune() packfiles which are created or modified after or + * at <code>packExpire</code> will not be deleted. Only older packfiles may + * be deleted. If set to null then every packfile is a candidate for + * deletion. + * + * @param packExpire + * instant in time which defines packfile expiration + */ + public void setPackExpire(Instant packExpire) { this.packExpire = packExpire; packExpireAgeMillis = -1; } @@ -1665,6 +1811,8 @@ public class GC { } /** + * Whether number of packs exceeds gc.autopacklimit + * * @return {@code true} if number of packs > gc.autopacklimit (default * 50) */ @@ -1736,6 +1884,8 @@ public class GC { private FileChannel channel; + private ShutdownHook.Listener shutdownListener = this::close; + PidLock() { pidFile = repo.getDirectory().toPath().resolve(GC_PID); } @@ -1764,12 +1914,7 @@ public class GC { } channel.write(ByteBuffer .wrap(getProcDesc().getBytes(StandardCharsets.UTF_8))); - Thread cleanupHook = new Thread(() -> close()); - try { - Runtime.getRuntime().addShutdownHook(cleanupHook); - } catch (IllegalStateException e) { - // ignore - the VM is already shutting down - } + ShutdownHook.INSTANCE.register(shutdownListener); } catch (IOException | OverlappingFileLockException e) { try { failedToLock(); @@ -1816,7 +1961,7 @@ public class GC { } private String getProcDesc() { - StringBuffer s = new StringBuffer(Long.toString(getPID())); + StringBuilder s = new StringBuilder(Long.toString(getPID())); s.append(' '); s.append(getHostName()); return s.toString(); @@ -1838,6 +1983,7 @@ public class GC { public void close() { boolean wasLocked = false; try { + ShutdownHook.INSTANCE.unregister(shutdownListener); if (lock != null && lock.isValid()) { lock.release(); wasLocked = true; @@ -1852,7 +1998,7 @@ public class GC { token.close(); } if (wasLocked) { - FileUtils.delete(pidFile.toFile(), FileUtils.RETRY); + FileUtils.delete(pidFile.toFile(), FileUtils.RETRY | FileUtils.SKIP_MISSING); } } catch (IOException e) { LOG.error(MessageFormat diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java index 86994a9eeb..862aaab0ee 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java @@ -23,8 +23,7 @@ import java.time.Instant; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.util.FileUtils; -import org.eclipse.jgit.util.GitDateParser; -import org.eclipse.jgit.util.SystemReader; +import org.eclipse.jgit.util.GitTimeParser; /** * This class manages the gc.log file for a {@link FileRepository}. @@ -50,7 +49,7 @@ class GcLog { */ GcLog(FileRepository repo) { this.repo = repo; - logFile = new File(repo.getDirectory(), "gc.log"); //$NON-NLS-1$ + logFile = new File(repo.getCommonDirectory(), "gc.log"); //$NON-NLS-1$ lock = new LockFile(logFile); } @@ -62,8 +61,7 @@ class GcLog { if (logExpiryStr == null) { logExpiryStr = LOG_EXPIRY_DEFAULT; } - gcLogExpire = GitDateParser.parse(logExpiryStr, null, - SystemReader.getInstance().getLocale()).toInstant(); + gcLogExpire = GitTimeParser.parseInstant(logExpiryStr); } return gcLogExpire; } @@ -132,6 +130,7 @@ class GcLog { * @param content * The content to write * @throws IOException + * if an IO error occurred */ void write(String content) throws IOException { if (content.length() > 0) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GlobalAttributesNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GlobalAttributesNode.java index 254557c08f..bbb7476d65 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GlobalAttributesNode.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GlobalAttributesNode.java @@ -39,6 +39,7 @@ public class GlobalAttributesNode extends AttributesNode { * * @return the attributes node * @throws java.io.IOException + * if an IO error occurred */ public AttributesNode load() throws IOException { AttributesNode r = new AttributesNode(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java index a22cd3ffeb..e8d442b8fb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java @@ -39,13 +39,14 @@ public class InfoAttributesNode extends AttributesNode { * * @return the attributes node * @throws java.io.IOException + * if an IO error occurred */ public AttributesNode load() throws IOException { AttributesNode r = new AttributesNode(); FS fs = repository.getFS(); - File attributes = fs.resolve(repository.getDirectory(), + File attributes = fs.resolve(repository.getCommonDirectory(), Constants.INFO_ATTRIBUTES); FileRepository.AttributesNodeProviderImpl.loadRulesFromFile(r, attributes); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LargePackedWholeObject.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LargePackedWholeObject.java index e2fbd7a0b4..15223ad437 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LargePackedWholeObject.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LargePackedWholeObject.java @@ -44,25 +44,21 @@ class LargePackedWholeObject extends ObjectLoader { this.db = db; } - /** {@inheritDoc} */ @Override public int getType() { return type; } - /** {@inheritDoc} */ @Override public long getSize() { return size; } - /** {@inheritDoc} */ @Override public boolean isLarge() { return true; } - /** {@inheritDoc} */ @Override public byte[] getCachedBytes() throws LargeObjectException { try { @@ -72,7 +68,6 @@ class LargePackedWholeObject extends ObjectLoader { } } - /** {@inheritDoc} */ @Override public ObjectStream openStream() throws MissingObjectException, IOException { WindowCursor wc = new WindowCursor(db); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java index 2d73f9bd10..d0d320bf02 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java @@ -41,7 +41,6 @@ public class LazyObjectIdSetFile implements ObjectIdSet { this.src = src; } - /** {@inheritDoc} */ @Override public boolean contains(AnyObjectId objectId) { if (set == null) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java index f112947bae..22eef9468c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java @@ -39,7 +39,6 @@ class LocalCachedPack extends CachedPack { this.packs = packs.toArray(new Pack[0]); } - /** {@inheritDoc} */ @Override public long getObjectCount() throws IOException { long cnt = 0; @@ -54,7 +53,6 @@ class LocalCachedPack extends CachedPack { pack.copyPackAsIs(out, wc); } - /** {@inheritDoc} */ @Override public boolean hasObject(ObjectToPack obj, StoredObjectRepresentation rep) { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectRepresentation.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectRepresentation.java index 559718af3a..af571622b4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectRepresentation.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectRepresentation.java @@ -22,6 +22,11 @@ class LocalObjectRepresentation extends StoredObjectRepresentation { public int getFormat() { return PACK_WHOLE; } + + @Override + public boolean wasDeltaAttempted() { + return true; + } }; r.pack = pack; r.offset = offset; @@ -59,13 +64,11 @@ class LocalObjectRepresentation extends StoredObjectRepresentation { private ObjectId baseId; - /** {@inheritDoc} */ @Override public int getWeight() { return (int) Math.min(length, Integer.MAX_VALUE); } - /** {@inheritDoc} */ @Override public ObjectId getDeltaBase() { if (baseId == null && getFormat() == PACK_DELTA) { @@ -83,5 +86,10 @@ class LocalObjectRepresentation extends StoredObjectRepresentation { public int getFormat() { return PACK_DELTA; } + + @Override + public boolean wasDeltaAttempted() { + return true; + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectToPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectToPack.java index ac6cd212d5..ca25212e00 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectToPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectToPack.java @@ -29,14 +29,12 @@ class LocalObjectToPack extends ObjectToPack { super(src, type); } - /** {@inheritDoc} */ @Override protected void clearReuseAsIs() { super.clearReuseAsIs(); pack = null; } - /** {@inheritDoc} */ @Override public void select(StoredObjectRepresentation ref) { LocalObjectRepresentation ptr = (LocalObjectRepresentation) ref; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java index d06b5a72d1..9e12ee8a0c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java @@ -24,6 +24,7 @@ import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileTime; import java.text.MessageFormat; @@ -31,6 +32,7 @@ import java.time.Instant; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.util.ShutdownHook; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.util.FS; @@ -79,6 +81,7 @@ public class LockFile { * Get the lock file corresponding to the given file. * * @param file + * given file * @return lock file */ static File getLockFile(File file) { @@ -112,6 +115,8 @@ public class LockFile { private LockToken token; + private ShutdownHook.Listener shutdownListener = this::unlock; + /** * Create a new lock for any file. * @@ -137,15 +142,15 @@ public class LockFile { throw new IllegalStateException( MessageFormat.format(JGitText.get().lockAlreadyHeld, ref)); } - FileUtils.mkdirs(lck.getParentFile(), true); try { - token = FS.DETECTED.createNewFileAtomic(lck); + token = createLockFileWithRetry(); } catch (IOException e) { LOG.error(JGitText.get().failedCreateLockFile, lck, e); throw e; } boolean obtainedLock = token.isCreated(); if (obtainedLock) { + ShutdownHook.INSTANCE.register(shutdownListener); haveLck = true; isAppend = false; written = false; @@ -155,6 +160,19 @@ public class LockFile { return obtainedLock; } + private FS.LockToken createLockFileWithRetry() throws IOException { + try { + return createLockFile(); + } catch (NoSuchFileException e) { + return createLockFile(); + } + } + + private FS.LockToken createLockFile() throws IOException { + FileUtils.mkdirs(lck.getParentFile(), true); + return FS.DETECTED.createNewFileAtomic(lck); + } + /** * Try to establish the lock for appending. * @@ -470,6 +488,7 @@ public class LockFile { * the lock is not held. */ public boolean commit() { + ShutdownHook.INSTANCE.unregister(shutdownListener); if (os != null) { unlock(); throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotClosed, ref)); @@ -509,17 +528,6 @@ public class LockFile { * Get the modification time of the output file when it was committed. * * @return modification time of the lock file right before we committed it. - * @deprecated use {@link #getCommitLastModifiedInstant()} instead - */ - @Deprecated - public long getCommitLastModified() { - return commitSnapshot.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 Instant getCommitLastModifiedInstant() { return commitSnapshot.lastModifiedInstant(); @@ -550,6 +558,7 @@ public class LockFile { * The temporary file (if created) is deleted before returning. */ public void unlock() { + ShutdownHook.INSTANCE.unregister(shutdownListener); if (os != null) { try { os.close(); @@ -575,7 +584,6 @@ public class LockFile { written = false; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java index 326c5f6457..909b3e3082 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java @@ -26,8 +26,9 @@ import org.eclipse.jgit.internal.storage.file.FileObjectDatabase.InsertLooseObje import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.CoreConfig; +import org.eclipse.jgit.lib.CoreConfig.TrustStat; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.util.FileUtils; @@ -49,13 +50,13 @@ class LooseObjects { * Maximum number of attempts to read a loose object for which a stale file * handle exception is thrown */ - private final static int MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS = 5; + private final static int MAX_STALE_READ_RETRIES = 5; private final File directory; private final UnpackedObjectCache unpackedObjectCache; - private final boolean trustFolderStat; + private final TrustStat trustLooseObjectStat; /** * Initialize a reference to an on-disk object directory. @@ -68,9 +69,8 @@ class LooseObjects { LooseObjects(Config config, File dir) { directory = dir; unpackedObjectCache = new UnpackedObjectCache(); - trustFolderStat = config.getBoolean( - ConfigConstants.CONFIG_CORE_SECTION, - ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true); + trustLooseObjectStat = config.get(CoreConfig.KEY) + .getTrustLooseObjectStat(); } /** @@ -90,7 +90,6 @@ class LooseObjects { unpackedObjectCache().clear(); } - /** {@inheritDoc} */ @Override public String toString() { return "LooseObjects[" + directory + "]"; //$NON-NLS-1$ //$NON-NLS-2$ @@ -109,7 +108,8 @@ class LooseObjects { */ boolean has(AnyObjectId objectId) { boolean exists = hasWithoutRefresh(objectId); - if (trustFolderStat || exists) { + if (trustLooseObjectStat == TrustStat.ALWAYS + || exists) { return exists; } try (InputStream stream = Files.newInputStream(directory.toPath())) { @@ -164,10 +164,31 @@ class LooseObjects { } ObjectLoader open(WindowCursor curs, AnyObjectId id) throws IOException { - int readAttempts = 0; - while (readAttempts < MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS) { - readAttempts++; - File path = fileFor(id); + File path = fileFor(id); + for (int retries = 0; retries < MAX_STALE_READ_RETRIES; retries++) { + boolean reload = true; + switch (trustLooseObjectStat) { + case NEVER: + break; + case AFTER_OPEN: + try (InputStream stream = Files + .newInputStream(path.getParentFile().toPath())) { + // open the loose object's fanout directory to refresh + // attributes (on some NFS clients) + } catch (FileNotFoundException | NoSuchFileException e) { + // ignore + } + //$FALL-THROUGH$ + case ALWAYS: + if (!path.exists()) { + reload = false; + } + break; + case INHERIT: + // only used in CoreConfig internally + throw new IllegalStateException(); + } + if (reload) { try { return getObjectLoader(curs, path, id); } catch (FileNotFoundException noFile) { @@ -181,9 +202,10 @@ class LooseObjects { } if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format( - JGitText.get().looseObjectHandleIsStale, id.name(), - Integer.valueOf(readAttempts), Integer.valueOf( - MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS))); + JGitText.get().looseObjectHandleIsStale, + id.name(), Integer.valueOf(retries), + Integer.valueOf(MAX_STALE_READ_RETRIES))); + } } } } @@ -209,7 +231,7 @@ class LooseObjects { try { return getObjectLoaderWithoutRefresh(curs, path, id); } catch (FileNotFoundException e) { - if (trustFolderStat) { + if (trustLooseObjectStat == TrustStat.ALWAYS) { throw e; } try (InputStream stream = Files @@ -246,7 +268,7 @@ class LooseObjects { return getSizeWithoutRefresh(curs, id); } catch (FileNotFoundException noFile) { try { - if (trustFolderStat) { + if (trustLooseObjectStat == TrustStat.ALWAYS) { throw noFile; } try (InputStream stream = Files @@ -280,7 +302,7 @@ class LooseObjects { // that already exists. We can't be sure renameTo() would // fail on all platforms if dst exists, so we check first. // - FileUtils.delete(tmp, FileUtils.RETRY); + FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); return InsertLooseObjectResult.EXISTS_LOOSE; } @@ -298,7 +320,7 @@ class LooseObjects { // Any other IO error is considered a failure. // LOG.error(e.getMessage(), e); - FileUtils.delete(tmp, FileUtils.RETRY); + FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); return InsertLooseObjectResult.FAILURE; } @@ -310,7 +332,7 @@ class LooseObjects { // know what went wrong, so fail. // LOG.error(e.getMessage(), e); - FileUtils.delete(tmp, FileUtils.RETRY); + FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); return InsertLooseObjectResult.FAILURE; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java index f27daad897..af1671d442 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java @@ -70,6 +70,11 @@ import org.eclipse.jgit.util.FileUtils; * considered. */ public class ObjectDirectory extends FileObjectDatabase { + @FunctionalInterface + private interface TriFunctionThrowsException<A, A2, A3, R, E extends Exception> { + R apply(A a, A2 a2, A3 a3) throws E; + } + /** Maximum number of candidates offered as resolutions of abbreviation. */ private static final int RESOLVE_ABBREV_LIMIT = 256; @@ -145,7 +150,6 @@ public class ObjectDirectory extends FileObjectDatabase { } } - /** {@inheritDoc} */ @Override public final File getDirectory() { return loose.getDirectory(); @@ -169,13 +173,11 @@ public class ObjectDirectory extends FileObjectDatabase { return preserved.getDirectory(); } - /** {@inheritDoc} */ @Override public boolean exists() { return fs.exists(objects); } - /** {@inheritDoc} */ @Override public void create() throws IOException { loose.create(); @@ -183,7 +185,6 @@ public class ObjectDirectory extends FileObjectDatabase { packed.create(); } - /** {@inheritDoc} */ @Override public ObjectDirectoryInserter newInserter() { return new ObjectDirectoryInserter(this, config); @@ -199,12 +200,12 @@ public class ObjectDirectory extends FileObjectDatabase { return new PackInserter(this); } - /** {@inheritDoc} */ @Override public void close() { loose.close(); packed.close(); + preserved.close(); // Fully close all loaded alternates and clear the alternate list. AlternateHandle[] alt = alternates.get(); @@ -214,13 +215,11 @@ public class ObjectDirectory extends FileObjectDatabase { } } - /** {@inheritDoc} */ @Override public Collection<Pack> getPacks() { return packed.getPacks(); } - /** {@inheritDoc} */ @Override public long getApproximateObjectCount() { long count = 0; @@ -234,7 +233,6 @@ public class ObjectDirectory extends FileObjectDatabase { return count; } - /** {@inheritDoc} */ @Override public Optional<CommitGraph> getCommitGraph() { if (config.get(CoreConfig.KEY).enableCommitGraph()) { @@ -244,7 +242,6 @@ public class ObjectDirectory extends FileObjectDatabase { } /** - * {@inheritDoc} * <p> * Add a single existing pack to the list of available pack files. */ @@ -268,18 +265,16 @@ public class ObjectDirectory extends FileObjectDatabase { } PackFile bitmapIdx = pf.create(BITMAP_INDEX); - Pack res = new Pack(pack, bitmapIdx.exists() ? bitmapIdx : null); + Pack res = new Pack(config, pack, bitmapIdx.exists() ? bitmapIdx : null); packed.insert(res); return res; } - /** {@inheritDoc} */ @Override public String toString() { return "ObjectDirectory[" + getDirectory() + "]"; //$NON-NLS-1$ //$NON-NLS-2$ } - /** {@inheritDoc} */ @Override public boolean has(AnyObjectId objectId) { return loose.hasCached(objectId) @@ -358,9 +353,13 @@ public class ObjectDirectory extends FileObjectDatabase { @Override ObjectLoader openObject(WindowCursor curs, AnyObjectId objectId) throws IOException { - ObjectLoader ldr = openObjectWithoutRestoring(curs, objectId); - if (ldr == null && restoreFromSelfOrAlternate(objectId, null)) { + ObjectLoader ldr = getFromLocalObjectToPack(curs, objectId, + (p, c, l) -> p.load(c, l.offset)); + if (ldr == null) { ldr = openObjectWithoutRestoring(curs, objectId); + if (ldr == null && restoreFromSelfOrAlternate(objectId, null)) { + ldr = openObjectWithoutRestoring(curs, objectId); + } } return ldr; } @@ -429,11 +428,16 @@ public class ObjectDirectory extends FileObjectDatabase { return loose.open(curs, id); } + @SuppressWarnings("boxing") @Override long getObjectSize(WindowCursor curs, AnyObjectId id) throws IOException { - long sz = getObjectSizeWithoutRestoring(curs, id); - if (0 > sz && restoreFromSelfOrAlternate(id, null)) { + Long sz = getFromLocalObjectToPack(curs, id, + (p, c, l) -> p.getObjectSize(c, l)); + if (sz == null) { sz = getObjectSizeWithoutRestoring(curs, id); + if (sz < 0 && restoreFromSelfOrAlternate(id, null)) { + sz = getObjectSizeWithoutRestoring(curs, id); + } } return sz; } @@ -442,12 +446,12 @@ public class ObjectDirectory extends FileObjectDatabase { AnyObjectId id) throws IOException { if (loose.hasCached(id)) { long len = loose.getSize(curs, id); - if (0 <= len) { + if (len >= 0) { return len; } } long len = getPackedSizeFromSelfOrAlternate(curs, id, null); - if (0 <= len) { + if (len >= 0) { return len; } return getLooseSizeFromSelfOrAlternate(curs, id, null); @@ -457,14 +461,14 @@ public class ObjectDirectory extends FileObjectDatabase { AnyObjectId id, Set<AlternateHandle.Id> skips) throws PackMismatchException { long len = packed.getSize(curs, id); - if (0 <= len) { + if (len >= 0) { return len; } skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { len = alt.db.getPackedSizeFromSelfOrAlternate(curs, id, skips); - if (0 <= len) { + if (len >= 0) { return len; } } @@ -475,14 +479,14 @@ public class ObjectDirectory extends FileObjectDatabase { private long getLooseSizeFromSelfOrAlternate(WindowCursor curs, AnyObjectId id, Set<AlternateHandle.Id> skips) throws IOException { long len = loose.getSize(curs, id); - if (0 <= len) { + if (len >= 0) { return len; } skips = addMe(skips); for (AlternateHandle alt : myAlternates()) { if (!skips.contains(alt.getId())) { len = alt.db.getLooseSizeFromSelfOrAlternate(curs, id, skips); - if (0 <= len) { + if (len >= 0) { return len; } } @@ -490,6 +494,24 @@ public class ObjectDirectory extends FileObjectDatabase { return -1; } + private <R> R getFromLocalObjectToPack(WindowCursor curs, + AnyObjectId objectId, + TriFunctionThrowsException<Pack, WindowCursor, LocalObjectToPack, R, IOException> func) { + if (objectId instanceof LocalObjectToPack) { + LocalObjectToPack lotp = (LocalObjectToPack) objectId; + Pack pack = lotp.pack; + if (pack != null) { + try { + return func.apply(pack, curs, lotp); + } catch (IOException e) { + // lotp potentially repacked, continue as if lotp not + // provided + } + } + } + return null; + } + @Override void selectObjectRepresentation(PackWriter packer, ObjectToPack otp, WindowCursor curs) throws IOException { @@ -559,11 +581,11 @@ public class ObjectDirectory extends FileObjectDatabase { // If the object is already in the repository, remove temporary file. // if (loose.hasCached(id)) { - FileUtils.delete(tmp, FileUtils.RETRY); + FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); return InsertLooseObjectResult.EXISTS_LOOSE; } if (!createDuplicate && has(id)) { - FileUtils.delete(tmp, FileUtils.RETRY); + FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); return InsertLooseObjectResult.EXISTS_PACKED; } return loose.insert(tmp, id); @@ -811,7 +833,6 @@ public class ObjectDirectory extends FileObjectDatabase { } } - /** {@inheritDoc} */ @Override public ObjectDatabase newCachedDatabase() { return newCachedFileObjectDatabase(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryInserter.java index e6b2cc12f4..24fead86bd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryInserter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryInserter.java @@ -49,7 +49,6 @@ class ObjectDirectoryInserter extends ObjectInserter { config = cfg.get(WriteConfig.KEY); } - /** {@inheritDoc} */ @Override public ObjectId insert(int type, byte[] data, int off, int len) throws IOException { @@ -62,12 +61,19 @@ class ObjectDirectoryInserter extends ObjectInserter { * ODB. * * @param type + * object type * @param data + * object data * @param off + * first position within @{code data} * @param len + * length number of bytes to copy * @param createDuplicate - * @return ObjectId + * whether to insert a duplicate if an object with this id + * already exists + * @return ObjectId the name of the object * @throws IOException + * if an IO error occurred */ private ObjectId insert( int type, byte[] data, int off, int len, boolean createDuplicate) @@ -80,7 +86,6 @@ class ObjectDirectoryInserter extends ObjectInserter { return insertOneObject(tmp, id, createDuplicate); } - /** {@inheritDoc} */ @Override public ObjectId insert(int type, long len, InputStream is) throws IOException { @@ -93,11 +98,17 @@ class ObjectDirectoryInserter extends ObjectInserter { * ODB. * * @param type + * object type * @param len + * number of bytes to copy * @param is + * input stream * @param createDuplicate - * @return ObjectId + * whether to insert a duplicate if an object with this id + * already exists + * @return ObjectId the name of the object * @throws IOException + * if an IO error occurred */ ObjectId insert(int type, long len, InputStream is, boolean createDuplicate) throws IOException { @@ -132,25 +143,21 @@ class ObjectDirectoryInserter extends ObjectInserter { .format(JGitText.get().unableToCreateNewObject, dst)); } - /** {@inheritDoc} */ @Override public PackParser newPackParser(InputStream in) throws IOException { return new ObjectDirectoryPackParser(db, in); } - /** {@inheritDoc} */ @Override public ObjectReader newReader() { return new WindowCursor(db, this); } - /** {@inheritDoc} */ @Override public void flush() throws IOException { // Do nothing. Loose objects are immediately visible. } - /** {@inheritDoc} */ @Override public void close() { if (deflate != null) { @@ -197,7 +204,7 @@ class ObjectDirectoryInserter extends ObjectInserter { return tmp; } finally { if (delete) { - FileUtils.delete(tmp, FileUtils.RETRY); + FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); } } } @@ -225,7 +232,7 @@ class ObjectDirectoryInserter extends ObjectInserter { return tmp; } finally { if (delete) { - FileUtils.delete(tmp, FileUtils.RETRY); + FileUtils.delete(tmp, FileUtils.RETRY | FileUtils.SKIP_MISSING); } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java index 55b2646c46..d97d5a7ccd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java @@ -28,6 +28,7 @@ import java.util.zip.Deflater; import org.eclipse.jgit.errors.LockFailedException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.internal.storage.pack.PackIndexWriter; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig; @@ -71,6 +72,12 @@ public class ObjectDirectoryPackParser extends PackParser { */ private File tmpIdx; + /** + * Path of the object-size index created for the pack, to filter quickly + * objects by size in partial clones + */ + private File tmpObjectSizeIndex; + /** Read/write handle to {@link #tmpPack} while it is being parsed. */ private RandomAccessFile out; @@ -110,7 +117,7 @@ public class ObjectDirectoryPackParser extends PackParser { * @param version * the version to write. The special version 0 designates the * oldest (most compatible) format available for the objects. - * @see PackIndexWriter + * @see BasePackIndexWriter */ public void setIndexVersion(int version) { indexVersion = version; @@ -142,7 +149,6 @@ public class ObjectDirectoryPackParser extends PackParser { return newPack; } - /** {@inheritDoc} */ @Override public long getPackSize() { if (newPack == null) @@ -158,12 +164,12 @@ public class ObjectDirectoryPackParser extends PackParser { return size; } - /** {@inheritDoc} */ @Override public PackLock parse(ProgressMonitor receiving, ProgressMonitor resolving) throws IOException { tmpPack = File.createTempFile("incoming_", ".pack", db.getDirectory()); //$NON-NLS-1$ //$NON-NLS-2$ tmpIdx = new File(db.getDirectory(), baseName(tmpPack) + ".idx"); //$NON-NLS-1$ + try { out = new RandomAccessFile(tmpPack, "rw"); //$NON-NLS-1$ @@ -179,6 +185,14 @@ public class ObjectDirectoryPackParser extends PackParser { tmpPack.setReadOnly(); tmpIdx.setReadOnly(); + if (pconfig.isWriteObjSizeIndex()) { + tmpObjectSizeIndex = new File(db.getDirectory(), + baseName(tmpPack) + + PackExt.OBJECT_SIZE_INDEX.getExtension()); + writeObjectSizeIndex(pconfig.getMinBytesForObjSizeIndex()); + tmpObjectSizeIndex.setReadOnly(); + } + return renameAndOpenPack(getLockMessage()); } finally { if (def != null) @@ -193,40 +207,34 @@ public class ObjectDirectoryPackParser extends PackParser { } } - /** {@inheritDoc} */ @Override protected void onPackHeader(long objectCount) throws IOException { // Ignored, the count is not required. } - /** {@inheritDoc} */ @Override protected void onBeginWholeObject(long streamPosition, int type, long inflatedSize) throws IOException { crc.reset(); } - /** {@inheritDoc} */ @Override protected void onEndWholeObject(PackedObjectInfo info) throws IOException { info.setCRC((int) crc.getValue()); } - /** {@inheritDoc} */ @Override protected void onBeginOfsDelta(long streamPosition, long baseStreamPosition, long inflatedSize) throws IOException { crc.reset(); } - /** {@inheritDoc} */ @Override protected void onBeginRefDelta(long streamPosition, AnyObjectId baseId, long inflatedSize) throws IOException { crc.reset(); } - /** {@inheritDoc} */ @Override protected UnresolvedDelta onEndDelta() throws IOException { UnresolvedDelta delta = new UnresolvedDelta(); @@ -234,35 +242,30 @@ public class ObjectDirectoryPackParser extends PackParser { return delta; } - /** {@inheritDoc} */ @Override protected void onInflatedObjectData(PackedObjectInfo obj, int typeCode, byte[] data) throws IOException { // ObjectDirectory ignores this event. } - /** {@inheritDoc} */ @Override protected void onObjectHeader(Source src, byte[] raw, int pos, int len) throws IOException { crc.update(raw, pos, len); } - /** {@inheritDoc} */ @Override protected void onObjectData(Source src, byte[] raw, int pos, int len) throws IOException { crc.update(raw, pos, len); } - /** {@inheritDoc} */ @Override protected void onStoreStream(byte[] raw, int pos, int len) throws IOException { out.write(raw, pos, len); } - /** {@inheritDoc} */ @Override protected void onPackFooter(byte[] hash) throws IOException { packEnd = out.getFilePointer(); @@ -271,7 +274,6 @@ public class ObjectDirectoryPackParser extends PackParser { packHash = hash; } - /** {@inheritDoc} */ @Override protected ObjectTypeAndSize seekDatabase(UnresolvedDelta delta, ObjectTypeAndSize info) throws IOException { @@ -280,7 +282,6 @@ public class ObjectDirectoryPackParser extends PackParser { return readObjectHeader(info); } - /** {@inheritDoc} */ @Override protected ObjectTypeAndSize seekDatabase(PackedObjectInfo obj, ObjectTypeAndSize info) throws IOException { @@ -289,13 +290,11 @@ public class ObjectDirectoryPackParser extends PackParser { return readObjectHeader(info); } - /** {@inheritDoc} */ @Override protected int readDatabase(byte[] dst, int pos, int cnt) throws IOException { return out.read(dst, pos, cnt); } - /** {@inheritDoc} */ @Override protected boolean checkCRC(int oldCRC) { return oldCRC == (int) crc.getValue(); @@ -311,9 +310,11 @@ public class ObjectDirectoryPackParser extends PackParser { tmpIdx.deleteOnExit(); if (tmpPack != null && !tmpPack.delete() && tmpPack.exists()) tmpPack.deleteOnExit(); + if (tmpObjectSizeIndex != null && !tmpObjectSizeIndex.delete() + && tmpObjectSizeIndex.exists()) + tmpPack.deleteOnExit(); } - /** {@inheritDoc} */ @Override protected boolean onAppendBase(final int typeCode, final byte[] data, final PackedObjectInfo info) throws IOException { @@ -356,7 +357,6 @@ public class ObjectDirectoryPackParser extends PackParser { return true; } - /** {@inheritDoc} */ @Override protected void onEndThinPack() throws IOException { final byte[] buf = buffer(); @@ -405,14 +405,23 @@ public class ObjectDirectoryPackParser extends PackParser { try (FileOutputStream os = new FileOutputStream(tmpIdx)) { final PackIndexWriter iw; if (indexVersion <= 0) - iw = PackIndexWriter.createOldestPossible(os, list); + iw = BasePackIndexWriter.createOldestPossible(os, list); else - iw = PackIndexWriter.createVersion(os, indexVersion); + iw = BasePackIndexWriter.createVersion(os, indexVersion); iw.write(list, packHash); os.getChannel().force(true); } } + private void writeObjectSizeIndex(int minSize) throws IOException { + try (FileOutputStream os = new FileOutputStream(tmpObjectSizeIndex)) { + PackObjectSizeIndexWriter iw = PackObjectSizeIndexWriter + .createWriter(os, minSize); + iw.write(getSortedObjectList(null)); + os.getChannel().force(true); + } + } + private PackLock renameAndOpenPack(String lockMessage) throws IOException { if (!keepEmpty && getObjectCount() == 0) { @@ -487,6 +496,27 @@ public class ObjectDirectoryPackParser extends PackParser { JGitText.get().cannotMoveIndexTo, finalIdx), e); } + if (pconfig.isWriteObjSizeIndex() && tmpObjectSizeIndex != null) { + PackFile finalObjectSizeIndex = finalPack + .create(PackExt.OBJECT_SIZE_INDEX); + try { + FileUtils.rename(tmpObjectSizeIndex, finalObjectSizeIndex, + StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + cleanupTemporaryFiles(); + keep.unlock(); + if (!finalPack.delete()) + finalPack.deleteOnExit(); + if (!finalIdx.delete()) { + finalIdx.deleteOnExit(); + } + throw new IOException(MessageFormat + .format(JGitText.get().cannotMoveIndexTo, + finalObjectSizeIndex), + e); + } + } + boolean interrupted = false; try { FileSnapshot snapshot = FileSnapshot.save(finalPack); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java index 6e74136c1b..8988b41b55 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java @@ -14,9 +14,14 @@ package org.eclipse.jgit.internal.storage.file; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP; +import static org.eclipse.jgit.internal.storage.pack.PackExt.REVERSE_INDEX; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACKED_INDEX_GIT_USE_STRONGREFS; +import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX; import java.io.EOFException; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InterruptedIOException; @@ -31,6 +36,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.CRC32; @@ -50,12 +56,17 @@ import org.eclipse.jgit.errors.UnsupportedPackIndexVersionException; import org.eclipse.jgit.errors.UnsupportedPackVersionException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.BinaryDelta; +import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.pack.PackOutputStream; +import org.eclipse.jgit.internal.util.Optionally; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.Hex; import org.eclipse.jgit.util.LongList; import org.eclipse.jgit.util.NB; import org.eclipse.jgit.util.RawParseUtils; @@ -76,6 +87,8 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { public static final Comparator<Pack> SORT = (a, b) -> b.packLastModified .compareTo(a.packLastModified); + private boolean useStrongRefs; + private final PackFile packFile; private PackFile keepFile; @@ -84,6 +97,9 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { private RandomAccessFile fd; + /** For managing open/close accounting of {@link #fd}. */ + private final Object activeLock = new Object(); + /** Serializes reads performed against {@link #fd}. */ private final Object readLock = new Object(); @@ -102,17 +118,22 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { private volatile Exception invalidatingCause; @Nullable - private PackFile bitmapIdxFile; + private volatile PackFile bitmapIdxFile; private AtomicInteger transientErrorCount = new AtomicInteger(); private byte[] packChecksum; - private volatile PackIndex loadedIdx; + private volatile Optionally<PackIndex> loadedIdx = Optionally.empty(); + + private volatile Optionally<PackReverseIndex> reverseIdx = Optionally.empty(); + + private volatile PackObjectSizeIndex loadedObjSizeIdx; - private PackReverseIndex reverseIdx; + private volatile boolean attemptLoadObjSizeIdx; + + private volatile Optionally<PackBitmapIndex> bitmapIdx = Optionally.empty(); - private PackBitmapIndex bitmapIdx; /** * Objects we have tried to read, and discovered to be corrupt. @@ -126,12 +147,16 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { /** * Construct a reader for an existing, pre-indexed packfile. * + * @param cfg + * configuration this directory consults for write settings. * @param packFile * path of the <code>.pack</code> file holding the data. * @param bitmapIdxFile * existing bitmap index file with the same base as the pack */ - public Pack(File packFile, @Nullable PackFile bitmapIdxFile) { + public Pack(Config cfg, File packFile, @Nullable PackFile bitmapIdxFile) { + useStrongRefs = cfg.getBoolean(CONFIG_CORE_SECTION, + CONFIG_KEY_PACKED_INDEX_GIT_USE_STRONGREFS, WindowCache.getInstance().isPackedIndexGitUseStrongRefs()); this.packFile = new PackFile(packFile); this.fileSnapshot = PackFileSnapshot.save(packFile); this.packLastModified = fileSnapshot.lastModifiedInstant(); @@ -145,58 +170,107 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { } private PackIndex idx() throws IOException { - PackIndex idx = loadedIdx; - if (idx == null) { - synchronized (this) { - idx = loadedIdx; - if (idx == null) { - if (invalid) { - throw new PackInvalidException(packFile, - invalidatingCause); - } - try { - long start = System.currentTimeMillis(); - PackFile idxFile = packFile.create(INDEX); - idx = PackIndex.open(idxFile); - if (LOG.isDebugEnabled()) { - LOG.debug(String.format( - "Opening pack index %s, size %.3f MB took %d ms", //$NON-NLS-1$ - idxFile.getAbsolutePath(), - Float.valueOf(idxFile.length() - / (1024f * 1024)), - Long.valueOf(System.currentTimeMillis() - - start))); - } + Optional<PackIndex> optional = loadedIdx.getOptional(); + if (optional.isPresent()) { + return optional.get(); + } + return memoizeIdxIfNeeded(); + } - if (packChecksum == null) { - packChecksum = idx.packChecksum; - fileSnapshot.setChecksum( - ObjectId.fromRaw(packChecksum)); - } else if (!Arrays.equals(packChecksum, - idx.packChecksum)) { - throw new PackMismatchException(MessageFormat - .format(JGitText.get().packChecksumMismatch, - packFile.getPath(), - ObjectId.fromRaw(packChecksum) - .name(), - ObjectId.fromRaw(idx.packChecksum) - .name())); - } - loadedIdx = idx; - } catch (InterruptedIOException e) { - // don't invalidate the pack, we are interrupted from - // another thread - throw e; - } catch (IOException e) { - invalid = true; - invalidatingCause = e; - throw e; - } + private synchronized PackIndex memoizeIdxIfNeeded() throws IOException { + Optional<PackIndex> optional = loadedIdx.getOptional(); + if (optional.isPresent()) { + return optional.get(); + } + if (invalid) { + throw new PackInvalidException(packFile, invalidatingCause); + } + try { + long start = System.currentTimeMillis(); + PackFile idxFile = packFile.create(INDEX); + PackIndex idx = PackIndex.open(idxFile); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "Opening pack index %s, size %.3f MB took %d ms", //$NON-NLS-1$ + idxFile.getAbsolutePath(), + Float.valueOf(idxFile.length() + / (1024f * 1024)), + Long.valueOf(System.currentTimeMillis() + - start))); + } + if (packChecksum == null) { + packChecksum = idx.getChecksum(); + fileSnapshot.setChecksum( + ObjectId.fromRaw(packChecksum)); + } else if (!Arrays.equals(packChecksum, + idx.getChecksum())) { + throw new PackMismatchException(MessageFormat + .format(JGitText.get().packChecksumMismatch, + packFile.getPath(), + PackExt.PACK.getExtension(), + Hex.toHexString(packChecksum), + PackExt.INDEX.getExtension(), + Hex.toHexString(idx.getChecksum()))); + } + loadedIdx = optionally(idx); + return idx; + } catch (InterruptedIOException e) { + // don't invalidate the pack, we are interrupted from + // another thread + throw e; + } catch (IOException e) { + invalid = true; + invalidatingCause = e; + throw e; + } + } + + private PackObjectSizeIndex objectSizeIndex() throws IOException { + if (loadedObjSizeIdx != null) { + return loadedObjSizeIdx; + } + + if (attemptLoadObjSizeIdx) { + return null; + } + + synchronized (this) { + if (loadedObjSizeIdx != null) { + return loadedObjSizeIdx; + } + + PackObjectSizeIndex sizeIdx; + try { + long start = System.currentTimeMillis(); + PackFile sizeIdxFile = packFile.create(OBJECT_SIZE_INDEX); + if (attemptLoadObjSizeIdx || !sizeIdxFile.exists()) { + attemptLoadObjSizeIdx = true; + return null; } + sizeIdx = PackObjectSizeIndexLoader.load( + new FileInputStream(sizeIdxFile.getAbsoluteFile())); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "Opening obj size index %s, size %.3f MB took %d ms", //$NON-NLS-1$ + sizeIdxFile.getAbsolutePath(), + Float.valueOf( + sizeIdxFile.length() / (1024f * 1024)), + Long.valueOf(System.currentTimeMillis() - start))); + } + + loadedObjSizeIdx = sizeIdx; + } catch (InterruptedIOException e) { + // don't invalidate the pack, we are interrupted from + // another thread + return null; + } finally { + attemptLoadObjSizeIdx = true; } } - return idx; + + return loadedObjSizeIdx; } + /** * Get the File object which locates this pack on disk. * @@ -211,12 +285,69 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { * * @return the index for this pack file. * @throws java.io.IOException + * if an IO error occurred */ public PackIndex getIndex() throws IOException { return idx(); } /** + * Get the object size index for this pack file + * + * @return the object size index for this pack file if it exists (null + * otherwise) + * @throws IOException + * problem reading the index + */ + public boolean hasObjectSizeIndex() throws IOException { + return objectSizeIndex() != null; + } + + /** + * Number of objects in the object-size index of this pack + * + * @return number of objects in the index (0 if either the index is empty or + * it doesn't exist) + * @throws IOException + * if an IO error occurred while reading the index + */ + public long getObjectSizeIndexCount() throws IOException { + if (!hasObjectSizeIndex()) { + return 0; + } + + return objectSizeIndex().getObjectCount(); + } + + /** + * Return the size of the object from the object-size index. + * + * Caller MUST check that the pack has object-size index + * ({@link #hasObjectSizeIndex()}) and that the pack contains the object. + * + * @param id + * object id of an object in the pack + * @return size of the object from the index. Negative if the object is not + * in the index. + * @throws IOException + * if an IO error occurred while reading the index + */ + public long getIndexedObjectSize(AnyObjectId id) throws IOException { + int idxPos = idx().findPosition(id); + if (idxPos < 0) { + return -1; + } + + PackObjectSizeIndex sizeIdx = objectSizeIndex(); + if (sizeIdx == null) { + throw new IllegalStateException( + "Asking indexed size from a pack without object size index"); //$NON-NLS-1$ + } + + return sizeIdx.getSize(idxPos); + } + + /** * Get name extracted from {@code pack-*.pack} pattern. * * @return name extracted from {@code pack-*.pack} pattern. @@ -279,14 +410,28 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { } /** - * Close the resources utilized by this repository + * Close the resources utilized by these pack files + * + * @param packs + * packs to close + */ + public static void close(Set<Pack> packs) { + WindowCache.purge(packs); + packs.forEach(p -> p.closeIndices()); + } + + /** + * Close the resources utilized by this pack file */ public void close() { WindowCache.purge(this); - synchronized (this) { - loadedIdx = null; - reverseIdx = null; - } + closeIndices(); + } + + private synchronized void closeIndices() { + loadedIdx = Optionally.empty(); + reverseIdx = Optionally.empty(); + bitmapIdx = Optionally.empty(); } /** @@ -398,185 +543,202 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { final CRC32 crc2 = validate ? new CRC32() : null; final byte[] buf = out.getCopyBuffer(); + boolean isHeaderWritten = false; // Rip apart the header so we can discover the size. // - readFully(src.offset, buf, 0, 20, curs); - int c = buf[0] & 0xff; - final int typeCode = (c >> 4) & 7; - long inflatedLength = c & 15; - int shift = 4; - int headerCnt = 1; - while ((c & 0x80) != 0) { - c = buf[headerCnt++] & 0xff; - inflatedLength += ((long) (c & 0x7f)) << shift; - shift += 7; - } - - if (typeCode == Constants.OBJ_OFS_DELTA) { - do { + try { + readFully(src.offset, buf, 0, 20, curs); + + int c = buf[0] & 0xff; + final int typeCode = (c >> 4) & 7; + long inflatedLength = c & 15; + int shift = 4; + int headerCnt = 1; + while ((c & 0x80) != 0) { c = buf[headerCnt++] & 0xff; - } while ((c & 128) != 0); - if (validate) { - assert(crc1 != null && crc2 != null); - crc1.update(buf, 0, headerCnt); - crc2.update(buf, 0, headerCnt); + inflatedLength += ((long) (c & 0x7f)) << shift; + shift += 7; } - } else if (typeCode == Constants.OBJ_REF_DELTA) { - if (validate) { + + if (typeCode == Constants.OBJ_OFS_DELTA) { + do { + c = buf[headerCnt++] & 0xff; + } while ((c & 128) != 0); + if (validate) { + assert(crc1 != null && crc2 != null); + crc1.update(buf, 0, headerCnt); + crc2.update(buf, 0, headerCnt); + } + } else if (typeCode == Constants.OBJ_REF_DELTA) { + if (validate) { + assert(crc1 != null && crc2 != null); + crc1.update(buf, 0, headerCnt); + crc2.update(buf, 0, headerCnt); + } + + readFully(src.offset + headerCnt, buf, 0, 20, curs); + if (validate) { + assert(crc1 != null && crc2 != null); + crc1.update(buf, 0, 20); + crc2.update(buf, 0, 20); + } + headerCnt += 20; + } else if (validate) { assert(crc1 != null && crc2 != null); crc1.update(buf, 0, headerCnt); crc2.update(buf, 0, headerCnt); } - readFully(src.offset + headerCnt, buf, 0, 20, curs); - if (validate) { - assert(crc1 != null && crc2 != null); - crc1.update(buf, 0, 20); - crc2.update(buf, 0, 20); - } - headerCnt += 20; - } else if (validate) { - assert(crc1 != null && crc2 != null); - crc1.update(buf, 0, headerCnt); - crc2.update(buf, 0, headerCnt); - } + final long dataOffset = src.offset + headerCnt; + final long dataLength = src.length; + final long expectedCRC; + final ByteArrayWindow quickCopy; - final long dataOffset = src.offset + headerCnt; - final long dataLength = src.length; - final long expectedCRC; - final ByteArrayWindow quickCopy; - - // Verify the object isn't corrupt before sending. If it is, - // we report it missing instead. - // - try { - quickCopy = curs.quickCopy(this, dataOffset, dataLength); + // Verify the object isn't corrupt before sending. If it is, + // we report it missing instead. + // + try { + quickCopy = curs.quickCopy(this, dataOffset, dataLength); - if (validate && idx().hasCRC32Support()) { - assert(crc1 != null); - // Index has the CRC32 code cached, validate the object. - // - expectedCRC = idx().findCRC32(src); - if (quickCopy != null) { - quickCopy.crc32(crc1, dataOffset, (int) dataLength); - } else { - long pos = dataOffset; - long cnt = dataLength; - while (cnt > 0) { - final int n = (int) Math.min(cnt, buf.length); - readFully(pos, buf, 0, n, curs); - crc1.update(buf, 0, n); - pos += n; - cnt -= n; + if (validate && idx().hasCRC32Support()) { + assert(crc1 != null); + // Index has the CRC32 code cached, validate the object. + // + expectedCRC = idx().findCRC32(src); + if (quickCopy != null) { + quickCopy.crc32(crc1, dataOffset, (int) dataLength); + } else { + long pos = dataOffset; + long cnt = dataLength; + while (cnt > 0) { + final int n = (int) Math.min(cnt, buf.length); + readFully(pos, buf, 0, n, curs); + crc1.update(buf, 0, n); + pos += n; + cnt -= n; + } } + if (crc1.getValue() != expectedCRC) { + setCorrupt(src.offset); + throw new CorruptObjectException(MessageFormat.format( + JGitText.get().objectAtHasBadZlibStream, + Long.valueOf(src.offset), getPackFile())); + } + } else if (validate) { + // We don't have a CRC32 code in the index, so compute it + // now while inflating the raw data to get zlib to tell us + // whether or not the data is safe. + // + Inflater inf = curs.inflater(); + byte[] tmp = new byte[1024]; + if (quickCopy != null) { + quickCopy.check(inf, tmp, dataOffset, (int) dataLength); + } else { + assert(crc1 != null); + long pos = dataOffset; + long cnt = dataLength; + while (cnt > 0) { + final int n = (int) Math.min(cnt, buf.length); + readFully(pos, buf, 0, n, curs); + crc1.update(buf, 0, n); + inf.setInput(buf, 0, n); + while (inf.inflate(tmp, 0, tmp.length) > 0) + continue; + pos += n; + cnt -= n; + } + } + if (!inf.finished() || inf.getBytesRead() != dataLength) { + setCorrupt(src.offset); + throw new EOFException(MessageFormat.format( + JGitText.get().shortCompressedStreamAt, + Long.valueOf(src.offset))); + } + assert(crc1 != null); + expectedCRC = crc1.getValue(); + } else { + expectedCRC = -1; } - if (crc1.getValue() != expectedCRC) { - setCorrupt(src.offset); - throw new CorruptObjectException(MessageFormat.format( - JGitText.get().objectAtHasBadZlibStream, - Long.valueOf(src.offset), getPackFile())); - } - } else if (validate) { - // We don't have a CRC32 code in the index, so compute it - // now while inflating the raw data to get zlib to tell us - // whether or not the data is safe. + } catch (DataFormatException dataFormat) { + setCorrupt(src.offset); + + CorruptObjectException corruptObject = new CorruptObjectException( + MessageFormat.format( + JGitText.get().objectAtHasBadZlibStream, + Long.valueOf(src.offset), getPackFile()), + dataFormat); + + throw new StoredObjectRepresentationNotAvailableException( + corruptObject); + } + + if (quickCopy != null) { + // The entire object fits into a single byte array window slice, + // and we have it pinned. Write this out without copying. // - Inflater inf = curs.inflater(); - byte[] tmp = new byte[1024]; - if (quickCopy != null) { - quickCopy.check(inf, tmp, dataOffset, (int) dataLength); - } else { - assert(crc1 != null); + out.writeHeader(src, inflatedLength); + isHeaderWritten = true; + quickCopy.write(out, dataOffset, (int) dataLength); + + } else if (dataLength <= buf.length) { + // Tiny optimization: Lots of objects are very small deltas or + // deflated commits that are likely to fit in the copy buffer. + // + if (!validate) { long pos = dataOffset; long cnt = dataLength; while (cnt > 0) { final int n = (int) Math.min(cnt, buf.length); readFully(pos, buf, 0, n, curs); - crc1.update(buf, 0, n); - inf.setInput(buf, 0, n); - while (inf.inflate(tmp, 0, tmp.length) > 0) - continue; pos += n; cnt -= n; } } - if (!inf.finished() || inf.getBytesRead() != dataLength) { - setCorrupt(src.offset); - throw new EOFException(MessageFormat.format( - JGitText.get().shortCompressedStreamAt, - Long.valueOf(src.offset))); - } - assert(crc1 != null); - expectedCRC = crc1.getValue(); + out.writeHeader(src, inflatedLength); + isHeaderWritten = true; + out.write(buf, 0, (int) dataLength); } else { - expectedCRC = -1; - } - } catch (DataFormatException dataFormat) { - setCorrupt(src.offset); - - CorruptObjectException corruptObject = new CorruptObjectException( - MessageFormat.format( - JGitText.get().objectAtHasBadZlibStream, - Long.valueOf(src.offset), getPackFile()), - dataFormat); - - throw new StoredObjectRepresentationNotAvailableException( - corruptObject); - - } catch (IOException ioError) { - throw new StoredObjectRepresentationNotAvailableException(ioError); - } - - if (quickCopy != null) { - // The entire object fits into a single byte array window slice, - // and we have it pinned. Write this out without copying. - // - out.writeHeader(src, inflatedLength); - quickCopy.write(out, dataOffset, (int) dataLength); - - } else if (dataLength <= buf.length) { - // Tiny optimization: Lots of objects are very small deltas or - // deflated commits that are likely to fit in the copy buffer. - // - if (!validate) { + // Now we are committed to sending the object. As we spool it out, + // check its CRC32 code to make sure there wasn't corruption between + // the verification we did above, and us actually outputting it. + // long pos = dataOffset; long cnt = dataLength; while (cnt > 0) { final int n = (int) Math.min(cnt, buf.length); readFully(pos, buf, 0, n, curs); - pos += n; + if (validate) { + assert(crc2 != null); + crc2.update(buf, 0, n); + } cnt -= n; + if (!isHeaderWritten) { + if (invalid && cnt > 0) { + // Since this is not the last iteration and the packfile is invalid, + // better to assume the iterations will not all complete here while + // it is still likely recoverable. + throw new StoredObjectRepresentationNotAvailableException(invalidatingCause); + } + out.writeHeader(src, inflatedLength); + isHeaderWritten = true; + } + out.write(buf, 0, n); + pos += n; } - } - out.writeHeader(src, inflatedLength); - out.write(buf, 0, (int) dataLength); - } else { - // Now we are committed to sending the object. As we spool it out, - // check its CRC32 code to make sure there wasn't corruption between - // the verification we did above, and us actually outputting it. - // - out.writeHeader(src, inflatedLength); - long pos = dataOffset; - long cnt = dataLength; - while (cnt > 0) { - final int n = (int) Math.min(cnt, buf.length); - readFully(pos, buf, 0, n, curs); if (validate) { assert(crc2 != null); - crc2.update(buf, 0, n); + if (crc2.getValue() != expectedCRC) { + throw new CorruptObjectException(MessageFormat.format( + JGitText.get().objectAtHasBadZlibStream, + Long.valueOf(src.offset), getPackFile())); + } } - out.write(buf, 0, n); - pos += n; - cnt -= n; } - if (validate) { - assert(crc2 != null); - if (crc2.getValue() != expectedCRC) { - throw new CorruptObjectException(MessageFormat.format( - JGitText.get().objectAtHasBadZlibStream, - Long.valueOf(src.offset), getPackFile())); - } + } catch (IOException ioError) { + if (!isHeaderWritten) { + throw new StoredObjectRepresentationNotAvailableException(ioError); } + throw ioError; } } @@ -603,42 +765,53 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { throw new EOFException(); } - private synchronized void beginCopyAsIs() + private void beginCopyAsIs() throws StoredObjectRepresentationNotAvailableException { - if (++activeCopyRawData == 1 && activeWindows == 0) { - try { - doOpen(); - } catch (IOException thisPackNotValid) { - throw new StoredObjectRepresentationNotAvailableException( - thisPackNotValid); + synchronized (activeLock) { + if (++activeCopyRawData == 1 && activeWindows == 0) { + try { + doOpen(); + } catch (IOException thisPackNotValid) { + throw new StoredObjectRepresentationNotAvailableException( + thisPackNotValid); + } } } } - private synchronized void endCopyAsIs() { - if (--activeCopyRawData == 0 && activeWindows == 0) - doClose(); + private void endCopyAsIs() { + synchronized (activeLock) { + if (--activeCopyRawData == 0 && activeWindows == 0) { + doClose(); + } + } } - synchronized boolean beginWindowCache() throws IOException { - if (++activeWindows == 1) { - if (activeCopyRawData == 0) - doOpen(); - return true; + boolean beginWindowCache() throws IOException { + synchronized (activeLock) { + if (++activeWindows == 1) { + if (activeCopyRawData == 0) { + doOpen(); + } + return true; + } + return false; } - return false; } - synchronized boolean endWindowCache() { - final boolean r = --activeWindows == 0; - if (r && activeCopyRawData == 0) - doClose(); - return r; + boolean endWindowCache() { + synchronized (activeLock) { + boolean r = --activeWindows == 0; + if (r && activeCopyRawData == 0) { + doClose(); + } + return r; + } } private void doOpen() throws IOException { if (invalid) { - openFail(true, invalidatingCause); + openFail(invalidatingCause); throw new PackInvalidException(packFile, invalidatingCause); } try { @@ -649,34 +822,41 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { } } catch (InterruptedIOException e) { // don't invalidate the pack, we are interrupted from another thread - openFail(false, e); + openFail(e); throw e; } catch (FileNotFoundException fn) { - // don't invalidate the pack if opening an existing file failed - // since it may be related to a temporary lack of resources (e.g. - // max open files) - openFail(!packFile.exists(), fn); + if (!packFile.exists()) { + // Failure to open an existing file may be related to a temporary lack of resources + // (e.g. max open files) + invalid = true; + } + openFail(fn); throw fn; } catch (EOFException | AccessDeniedException | NoSuchFileException | CorruptObjectException | NoPackSignatureException | PackMismatchException | UnpackException | UnsupportedPackIndexVersionException | UnsupportedPackVersionException pe) { - // exceptions signaling permanent problems with a pack - openFail(true, pe); + invalid = true; // exceptions signaling permanent problems with a pack + openFail(pe); throw pe; - } catch (IOException | RuntimeException ge) { + } catch (IOException ioe) { + if (FileUtils.isStaleFileHandleInCausalChain(ioe)) { + invalid = true; + } + openFail(ioe); + throw ioe; + } catch (RuntimeException ge) { // generic exceptions could be transient so we should not mark the // pack invalid to avoid false MissingObjectExceptions - openFail(false, ge); + openFail(ge); throw ge; } } - private void openFail(boolean invalidate, Exception cause) { + private void openFail(Exception cause) { activeWindows = 0; activeCopyRawData = 0; - invalid = invalidate; invalidatingCause = cause; doClose(); } @@ -764,11 +944,11 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { fd.seek(length - 20); fd.readFully(buf, 0, 20); if (!Arrays.equals(buf, packChecksum)) { - throw new PackMismatchException(MessageFormat.format( - JGitText.get().packChecksumMismatch, - getPackFile(), - ObjectId.fromRaw(buf).name(), - ObjectId.fromRaw(idx.packChecksum).name())); + throw new PackMismatchException( + MessageFormat.format(JGitText.get().packChecksumMismatch, + getPackFile(), PackExt.PACK.getExtension(), + Hex.toHexString(buf), PackExt.INDEX.getExtension(), + Hex.toHexString(idx.getChecksum()))); } } @@ -1119,37 +1299,71 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { return getReverseIdx().findNextOffset(startOffset, maxOffset); } - synchronized PackBitmapIndex getBitmapIndex() throws IOException { + PackBitmapIndex getBitmapIndex() throws IOException { + Optional<PackBitmapIndex> optional = bitmapIdx.getOptional(); + if (optional.isPresent()) { + return optional.get(); + } + return memoizeBitmapIndexIfNeeded(); + } + + private synchronized PackBitmapIndex memoizeBitmapIndexIfNeeded() throws IOException { if (invalid || bitmapIdxFile == null) { return null; } - if (bitmapIdx == null) { - final PackBitmapIndex idx; - try { - idx = PackBitmapIndex.open(bitmapIdxFile, idx(), - getReverseIdx()); - } catch (FileNotFoundException e) { - // Once upon a time this bitmap file existed. Now it - // has been removed. Most likely an external gc has - // removed this packfile and the bitmap - bitmapIdxFile = null; - return null; - } - + Optional<PackBitmapIndex> optional = bitmapIdx.getOptional(); + if (optional.isPresent()) { + return optional.get(); + } + try { + PackBitmapIndex idx = PackBitmapIndex.open(bitmapIdxFile, idx(), + getReverseIdx()); // At this point, idx() will have set packChecksum. - if (Arrays.equals(packChecksum, idx.packChecksum)) { - bitmapIdx = idx; - } else { - bitmapIdxFile = null; + if (Arrays.equals(packChecksum, idx.getPackChecksum())) { + bitmapIdx = optionally(idx); + return idx; + } + } catch (FileNotFoundException e) { + // Once upon a time the bitmap or index files existed. Now one + // of them has been removed. Most likely an external gc has + // removed index, packfile or the bitmap + } catch (IOException e) { + if (!FileUtils.isStaleFileHandleInCausalChain(e)) { + throw e; } + // Ignore NFS stale handle exception the same way as + // FileNotFoundException above. + } + bitmapIdxFile = null; + return null; + } + + void setBitmapIndexFile(PackFile bitmapIndexFile) { + this.bitmapIdxFile = bitmapIndexFile; + } + + private PackReverseIndex getReverseIdx() throws IOException { + Optional<PackReverseIndex> optional = reverseIdx.getOptional(); + if (optional.isPresent()) { + return optional.get(); } - return bitmapIdx; + return memoizeReverseIdxIfNeeded(); } - private synchronized PackReverseIndex getReverseIdx() throws IOException { - if (reverseIdx == null) - reverseIdx = new PackReverseIndex(idx()); - return reverseIdx; + private synchronized PackReverseIndex memoizeReverseIdxIfNeeded() throws IOException { + if (invalid) { + throw new PackInvalidException(packFile, invalidatingCause); + } + Optional<PackReverseIndex> optional = reverseIdx.getOptional(); + if (optional.isPresent()) { + return optional.get(); + } + PackFile reverseIndexFile = packFile.create(REVERSE_INDEX); + PackReverseIndex revIdx = PackReverseIndexFactory.openOrCompute(reverseIndexFile, + getObjectCount(), () -> getIndex()); + revIdx.verifyPackChecksum(getPackFile().getPath()); + reverseIdx = optionally(revIdx); + return revIdx; } private boolean isCorrupt(long offset) { @@ -1184,4 +1398,8 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { + packFile.length() + ", packChecksum=" + ObjectId.fromRaw(packChecksum).name() + "]"; } + + private <T> Optionally<T> optionally(T element) { + return useStrongRefs ? new Optionally.Hard<>(element) : new Optionally.Soft<>(element); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndex.java index 8fb17fcf21..cbda8fc77c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndex.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndex.java @@ -14,6 +14,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.text.MessageFormat; +import java.util.function.Supplier; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.internal.JGitText; @@ -34,7 +35,7 @@ import com.googlecode.javaewah.EWAHCompressedBitmap; * {@link #findPosition(AnyObjectId)} can be used to build other bitmaps that a * compatible with the encoded bitmaps available from the index. */ -public abstract class PackBitmapIndex { +public interface PackBitmapIndex { /** Flag bit denoting the bitmap should be reused during index creation. */ public static final int FLAG_REUSE = 1; @@ -131,8 +132,14 @@ public abstract class PackBitmapIndex { reverseIndexSupplier, loadParallelRevIndex); } - /** Footer checksum applied on the bottom of the pack file. */ - byte[] packChecksum; + /** + * Footer checksum applied on the bottom of the pack file. + * + * @return checksum as a byte array + */ + default byte[] getPackChecksum() { + return null; + } /** * Finds the position in the bitmap of the object. @@ -147,7 +154,9 @@ public abstract class PackBitmapIndex { * Get the object at the bitmap position. * * @param position - * the id for which the object will be found. + * the offset in the bitmap which corresponds to an object of + * interest. This position is the same as the order of the object + * in the {@link PackFile}. * @return the ObjectId. * @throws java.lang.IllegalArgumentException * when the item is not found. @@ -195,6 +204,41 @@ public abstract class PackBitmapIndex { public abstract int getBitmapCount(); /** + * Returns the number of bitmaps in this bitmap index ready to use, not + * XOR'ed against other entries. + * + * @return the number of bitmaps in this bitmap index ready to use. + */ + public abstract int getBaseBitmapCount(); + + /** + * Current size in bytes of all base bitmaps in the index. + * + * Resolving xors for bitmaps can affect this size. + * + * @return Current size (in bytes) of all base bitmaps in this index. + */ + public abstract long getBaseBitmapSizeInBytes(); + + /** + * Returns the number of bitmaps in this bitmap index XOR'ed against other + * entries. + * + * @return the number of bitmaps in this bitmap index represented as XOR + * masks. + */ + public abstract int getXorBitmapCount(); + + /** + * Current size in bytes of all XOR'ed bitmaps in the index. + * + * Resolving xors for bitmaps can affect this size. + * + * @return Current size (in bytes) of all xor bitmaps in this index. + */ + public abstract long getXorBitmapSizeInBytes(); + + /** * Supplier that propagates IOException. * * @param <T> @@ -203,8 +247,13 @@ public abstract class PackBitmapIndex { @FunctionalInterface public interface SupplierWithIOException<T> { /** + * Get result. + * + * @see Supplier#get() + * * @return result * @throws IOException + * if an IO error occurred */ T get() throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexBuilder.java index 5666b57609..08d2b7178f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexBuilder.java @@ -11,9 +11,9 @@ package org.eclipse.jgit.internal.storage.file; import java.text.MessageFormat; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import org.eclipse.jgit.internal.JGitText; @@ -41,8 +41,7 @@ public class PackBitmapIndexBuilder extends BasePackBitmapIndex { private final EWAHCompressedBitmap tags; private final BlockList<PositionEntry> byOffset; - private final LinkedList<StoredBitmap> - bitmapsToWriteXorBuffer = new LinkedList<>(); + private final ArrayDeque<StoredBitmap> bitmapsToWriteXorBuffer = new ArrayDeque<>(); private List<StoredEntry> bitmapsToWrite = new ArrayList<>(); @@ -58,7 +57,7 @@ public class PackBitmapIndexBuilder extends BasePackBitmapIndex { * ObjectId (name); it will be resorted in place. */ public PackBitmapIndexBuilder(List<ObjectToPack> objects) { - super(new ObjectIdOwnerMap<StoredBitmap>()); + super(new ObjectIdOwnerMap<>()); byOffset = new BlockList<>(objects.size()); sortByOffsetAndIndex(byOffset, positionEntries, objects); @@ -106,7 +105,7 @@ public class PackBitmapIndexBuilder extends BasePackBitmapIndex { .signum(a.getOffset() - b.getOffset())); for (int i = 0; i < entries.size(); i++) { PositionEntry e = positionEntries.get(entries.get(i)); - e.offsetPosition = i; + e.ridxPosition = i; byOffset.add(e); } } @@ -191,8 +190,8 @@ public class PackBitmapIndexBuilder extends BasePackBitmapIndex { throw new IllegalStateException(); } bestBitmap.trim(); - StoredEntry result = new StoredEntry(entry.namePosition, bestBitmap, - bestXorOffset, bitmapToWrite.getFlags()); + StoredEntry result = new StoredEntry(entry, entry.idxPosition, + bestBitmap, bestXorOffset, bitmapToWrite.getFlags()); return result; } @@ -214,7 +213,6 @@ public class PackBitmapIndexBuilder extends BasePackBitmapIndex { getBitmaps().add(result); } - /** {@inheritDoc} */ @Override public EWAHCompressedBitmap ofObjectType( EWAHCompressedBitmap bitmap, int type) { @@ -231,16 +229,14 @@ public class PackBitmapIndexBuilder extends BasePackBitmapIndex { throw new IllegalArgumentException(); } - /** {@inheritDoc} */ @Override public int findPosition(AnyObjectId objectId) { PositionEntry entry = positionEntries.get(objectId); if (entry == null) return -1; - return entry.offsetPosition; + return entry.ridxPosition; } - /** {@inheritDoc} */ @Override public ObjectId getObject(int position) throws IllegalArgumentException { ObjectId objectId = byOffset.get(position); @@ -294,7 +290,6 @@ public class PackBitmapIndexBuilder extends BasePackBitmapIndex { return PackBitmapIndexV1.OPT_FULL; } - /** {@inheritDoc} */ @Override public int getBitmapCount() { return bitmapsToWriteXorBuffer.size() + bitmapsToWrite.size(); @@ -311,7 +306,6 @@ public class PackBitmapIndexBuilder extends BasePackBitmapIndex { bitmapsToWrite = new ArrayList<>(size); } - /** {@inheritDoc} */ @Override public int getObjectCount() { return byOffset.size(); @@ -328,54 +322,100 @@ public class PackBitmapIndexBuilder extends BasePackBitmapIndex { generateStoredEntry(bitmapsToWriteXorBuffer.pollFirst())); } - Collections.reverse(bitmapsToWrite); - return bitmapsToWrite; + List<StoredEntry> bitmapsToReturn = new ArrayList<>(bitmapsToWrite); + Collections.reverse(bitmapsToReturn); + return bitmapsToReturn; } /** Data object for the on disk representation of a bitmap entry. */ public static final class StoredEntry { - private final long objectId; + private final ObjectId objectId; + + private final long idxPosition; + private final EWAHCompressedBitmap bitmap; + private final int xorOffset; + private final int flags; - StoredEntry(long objectId, EWAHCompressedBitmap bitmap, - int xorOffset, int flags) { + /** + * Create a StoredEntry + * + * @param objectId + * objectId of the object associated with the bitmap + * @param idxPosition + * position of this object into the pack index (i.e. sorted + * by sha1) + * @param bitmap + * bitmap associated with this object + * @param xorOffset + * offset of the bitmap against which this bitmap is + * xor-compressed. If 0, then this bitmap is not + * xor-compressed against any other bitmap + * @param flags + * flags for this bitmap + */ + public StoredEntry(ObjectId objectId, long idxPosition, + EWAHCompressedBitmap bitmap, int xorOffset, int flags) { this.objectId = objectId; + this.idxPosition = idxPosition; this.bitmap = bitmap; this.xorOffset = xorOffset; this.flags = flags; } - /** @return the bitmap */ + /** + * Get the bitmap + * + * @return the bitmap + */ public EWAHCompressedBitmap getBitmap() { return bitmap; } - /** @return the xorOffset */ + /** + * Get the xorOffset + * + * @return the xorOffset + */ public int getXorOffset() { return xorOffset; } - /** @return the flags */ + /** + * Get the flags + * + * @return the flags + */ public int getFlags() { return flags; } - /** @return the ObjectId */ - public long getObjectId() { + /** + * @return the position of the object with this bitmap in the primary + * index (i.e. ordered by sha1) + */ + public long getIdxPosition() { + return idxPosition; + } + + /** + * @return the objectId of the object associated with this bitmap + */ + public ObjectId getObjectId() { return objectId; } } private static final class PositionEntry extends ObjectIdOwnerMap.Entry { - final int namePosition; + final int idxPosition; - int offsetPosition; + int ridxPosition; - PositionEntry(AnyObjectId objectId, int namePosition) { + PositionEntry(AnyObjectId objectId, int idxPosition) { super(objectId); - this.namePosition = namePosition; + this.idxPosition = idxPosition; } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexRemapper.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexRemapper.java index e1b6f780c7..ffbc0737ac 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexRemapper.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexRemapper.java @@ -28,8 +28,8 @@ import com.googlecode.javaewah.IntIterator; * implementations this implementation is not thread safe, as it is intended to * be used with a PackBitmapIndexBuilder, which is also not thread safe. */ -public class PackBitmapIndexRemapper extends PackBitmapIndex - implements Iterable<PackBitmapIndexRemapper.Entry> { +public class PackBitmapIndexRemapper + implements PackBitmapIndex, Iterable<PackBitmapIndexRemapper.Entry> { private final BasePackBitmapIndex oldPackIndex; final PackBitmapIndex newPackIndex; @@ -79,32 +79,47 @@ public class PackBitmapIndexRemapper extends PackBitmapIndex oldPackIndex.getObject(pos)); } - /** {@inheritDoc} */ @Override public int findPosition(AnyObjectId objectId) { return newPackIndex.findPosition(objectId); } - /** {@inheritDoc} */ @Override public ObjectId getObject(int position) throws IllegalArgumentException { return newPackIndex.getObject(position); } - /** {@inheritDoc} */ @Override public int getObjectCount() { return newPackIndex.getObjectCount(); } - /** {@inheritDoc} */ + @Override + public int getBaseBitmapCount() { + return newPackIndex.getBaseBitmapCount(); + } + + @Override + public long getBaseBitmapSizeInBytes() { + return newPackIndex.getBaseBitmapSizeInBytes(); + } + + @Override + public int getXorBitmapCount() { + return newPackIndex.getXorBitmapCount(); + } + + @Override + public long getXorBitmapSizeInBytes() { + return newPackIndex.getXorBitmapSizeInBytes(); + } + @Override public EWAHCompressedBitmap ofObjectType( EWAHCompressedBitmap bitmap, int type) { return newPackIndex.ofObjectType(bitmap, type); } - /** {@inheritDoc} */ @Override public Iterator<Entry> iterator() { if (oldPackIndex == null) @@ -141,7 +156,6 @@ public class PackBitmapIndexRemapper extends PackBitmapIndex }; } - /** {@inheritDoc} */ @Override public EWAHCompressedBitmap getBitmap(AnyObjectId objectId) { EWAHCompressedBitmap bitmap = newPackIndex.getBitmap(objectId); @@ -173,13 +187,16 @@ public class PackBitmapIndexRemapper extends PackBitmapIndex this.flags = flags; } - /** @return the flags associated with the bitmap. */ + /** + * Get flags + * + * @return the flags associated with the bitmap. + */ public int getFlags() { return flags; } } - /** {@inheritDoc} */ @Override public int getBitmapCount() { // The count is only useful for the end index, not the remapper. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexV1.java index 988dc6c4ff..19608c1ce5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexV1.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexV1.java @@ -46,6 +46,8 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { private static final int MAX_XOR_OFFSET = 126; + private byte[] packChecksum; + private static final ExecutorService executor = Executors .newCachedThreadPool(new ThreadFactory() { private final ThreadFactory baseFactory = Executors @@ -84,7 +86,7 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { throws IOException { // An entry is object id, xor offset, flag byte, and a length encoded // bitmap. The object id is an int32 of the nth position sorted by name. - super(new ObjectIdOwnerMap<StoredBitmap>()); + super(new ObjectIdOwnerMap<>()); this.bitmaps = getBitmaps(); // Optionally start loading reverse index in parallel to loading bitmap @@ -214,7 +216,6 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { this.reverseIndex = computedReverseIndex; } - /** {@inheritDoc} */ @Override public int findPosition(AnyObjectId objectId) { long offset = packIndex.findOffset(objectId); @@ -223,7 +224,6 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { return reverseIndex.findPosition(offset); } - /** {@inheritDoc} */ @Override public ObjectId getObject(int position) throws IllegalArgumentException { ObjectId objectId = reverseIndex.findObjectByPosition(position); @@ -232,13 +232,11 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { return objectId; } - /** {@inheritDoc} */ @Override public int getObjectCount() { return (int) packIndex.getObjectCount(); } - /** {@inheritDoc} */ @Override public EWAHCompressedBitmap ofObjectType( EWAHCompressedBitmap bitmap, int type) { @@ -255,13 +253,11 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { throw new IllegalArgumentException(); } - /** {@inheritDoc} */ @Override public int getBitmapCount() { return bitmaps.size(); } - /** {@inheritDoc} */ @Override public boolean equals(Object o) { // TODO(cranger): compare the pack checksum? @@ -270,12 +266,16 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { return false; } - /** {@inheritDoc} */ @Override public int hashCode() { return getPackIndex().hashCode(); } + @Override + public byte[] getPackChecksum() { + return this.packChecksum; + } + PackIndex getPackIndex() { return packIndex; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexWriterV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexWriterV1.java index a5c8423dfd..38d7c90894 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexWriterV1.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexWriterV1.java @@ -19,6 +19,7 @@ import java.text.MessageFormat; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.PackBitmapIndexBuilder.StoredEntry; +import org.eclipse.jgit.internal.storage.pack.PackBitmapIndexWriter; import org.eclipse.jgit.lib.Constants; import com.googlecode.javaewah.EWAHCompressedBitmap; @@ -28,7 +29,7 @@ import com.googlecode.javaewah.EWAHCompressedBitmap; * * @see PackBitmapIndexV1 */ -public class PackBitmapIndexWriterV1 { +public class PackBitmapIndexWriterV1 implements PackBitmapIndexWriter { private final DigestOutputStream out; private final DataOutput dataOutput; @@ -60,6 +61,7 @@ public class PackBitmapIndexWriterV1 { * an error occurred while writing to the output stream, or this * index format cannot store the object data supplied. */ + @Override public void write(PackBitmapIndexBuilder bitmaps, byte[] packDataChecksum) throws IOException { if (bitmaps == null || packDataChecksum.length != 20) @@ -113,7 +115,7 @@ public class PackBitmapIndexWriterV1 { private void writeBitmapEntry(StoredEntry entry) throws IOException { // Write object, XOR offset, and bitmap - dataOutput.writeInt((int) entry.getObjectId()); + dataOutput.writeInt((int) entry.getIdxPosition()); out.write(entry.getXorOffset()); out.write(entry.getFlags()); writeBitmap(entry.getBitmap()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java index 6a99cb3d83..544961b936 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java @@ -17,6 +17,8 @@ import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -24,6 +26,8 @@ import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -41,10 +45,12 @@ import org.eclipse.jgit.internal.storage.pack.PackWriter; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.CoreConfig; +import org.eclipse.jgit.lib.CoreConfig.TrustStat; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.Iterators; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,11 +71,13 @@ class PackDirectory { private static final PackList NO_PACKS = new PackList(FileSnapshot.DIRTY, new Pack[0]); + private final Config config; + private final File directory; private final AtomicReference<PackList> packList; - private final boolean trustFolderStat; + private final TrustStat trustPackStat; /** * Initialize a reference to an on-disk 'pack' directory. @@ -80,16 +88,10 @@ class PackDirectory { * the location of the {@code pack} directory. */ PackDirectory(Config config, File directory) { + this.config = config; this.directory = directory; packList = new AtomicReference<>(NO_PACKS); - - // Whether to trust the pack folder's modification time. If set to false - // we will always scan the .git/objects/pack folder to check for new - // pack files. If set to true (default) we use the folder's size, - // modification time, and key (inode) and assume that no new pack files - // can be in this folder if these attributes have not changed. - trustFolderStat = config.getBoolean(ConfigConstants.CONFIG_CORE_SECTION, - ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true); + trustPackStat = config.get(CoreConfig.KEY).getTrustPackStat(); } /** @@ -108,22 +110,22 @@ class PackDirectory { void close() { PackList packs = packList.get(); if (packs != NO_PACKS && packList.compareAndSet(packs, NO_PACKS)) { - for (Pack p : packs.packs) { - p.close(); - } + Pack.close(Set.of(packs.packs)); } } Collection<Pack> getPacks() { - PackList list = packList.get(); - if (list == NO_PACKS) { - list = scanPacks(list); - } + PackList list; + do { + list = packList.get(); + if (list == NO_PACKS) { + list = scanPacks(list); + } + } while (searchPacksAgain(list)); Pack[] packs = list.packs; return Collections.unmodifiableCollection(Arrays.asList(packs)); } - /** {@inheritDoc} */ @Override public String toString() { return "PackDirectory[" + getDirectory() + "]"; //$NON-NLS-1$ //$NON-NLS-2$ @@ -275,12 +277,14 @@ class PackDirectory { PackList pList = packList.get(); int retries = 0; SEARCH: for (;;) { - for (Pack p : pList.packs) { + for (Pack p : pList.inReverse()) { try { LocalObjectRepresentation rep = p.representation(curs, otp); p.resetTransientErrorCount(); if (rep != null) { - packer.select(otp, rep); + if (!packer.select(otp, rep)) { + return; + } packer.checkSearchForReuseTimeout(); } } catch (SearchForReuseTimeout e) { @@ -309,38 +313,42 @@ class PackDirectory { } private void handlePackError(IOException e, Pack p) { - String warnTmpl = null; + String warnTemplate = null; + String debugTemplate = null; int transientErrorCount = 0; - String errTmpl = JGitText.get().exceptionWhileReadingPack; + String errorTemplate = JGitText.get().exceptionWhileReadingPack; if ((e instanceof CorruptObjectException) || (e instanceof PackInvalidException)) { - warnTmpl = JGitText.get().corruptPack; - LOG.warn(MessageFormat.format(warnTmpl, + warnTemplate = JGitText.get().corruptPack; + LOG.warn(MessageFormat.format(warnTemplate, p.getPackFile().getAbsolutePath()), e); // Assume the pack is corrupted, and remove it from the list. remove(p); } else if (e instanceof FileNotFoundException) { if (p.getPackFile().exists()) { - errTmpl = JGitText.get().packInaccessible; + errorTemplate = JGitText.get().packInaccessible; transientErrorCount = p.incrementTransientErrorCount(); } else { - warnTmpl = JGitText.get().packWasDeleted; + debugTemplate = JGitText.get().packWasDeleted; remove(p); } } else if (FileUtils.isStaleFileHandleInCausalChain(e)) { - warnTmpl = JGitText.get().packHandleIsStale; + warnTemplate = JGitText.get().packHandleIsStale; remove(p); } else { transientErrorCount = p.incrementTransientErrorCount(); } - if (warnTmpl != null) { - LOG.warn(MessageFormat.format(warnTmpl, + if (warnTemplate != null) { + LOG.warn(MessageFormat.format(warnTemplate, p.getPackFile().getAbsolutePath()), e); + } else if (debugTemplate != null) { + LOG.debug(MessageFormat.format(debugTemplate, + p.getPackFile().getAbsolutePath()), e); } else { if (doLogExponentialBackoff(transientErrorCount)) { // Don't remove the pack from the list, as the error may be // transient. - LOG.error(MessageFormat.format(errTmpl, + LOG.error(MessageFormat.format(errorTemplate, p.getPackFile().getAbsolutePath(), Integer.valueOf(transientErrorCount)), e); } @@ -350,15 +358,33 @@ class PackDirectory { /** * @param n * count of consecutive failures - * @return @{code true} if i is a power of 2 + * @return {@code true} if i is a power of 2 */ private boolean doLogExponentialBackoff(int n) { return (n & (n - 1)) == 0; } boolean searchPacksAgain(PackList old) { - return ((!trustFolderStat) || old.snapshot.isModified(directory)) - && old != scanPacks(old); + switch (trustPackStat) { + case NEVER: + break; + case AFTER_OPEN: + try (InputStream stream = Files + .newInputStream(directory.toPath())) { + // open the pack directory to refresh attributes (on some NFS clients) + } catch (IOException e) { + // ignore + } + //$FALL-THROUGH$ + case ALWAYS: + if (!old.snapshot.isModified(directory)) { + return false; + } + break; + case INHERIT: + // only used in CoreConfig internally + } + return old != scanPacks(old); } void insert(Pack pack) { @@ -455,10 +481,14 @@ class PackDirectory { && !oldPack.getFileSnapshot().isModified(packFile)) { forReuse.remove(packFile.getName()); list.add(oldPack); + PackFile bitMaps = packFilesByExt.get(BITMAP_INDEX); + if (bitMaps != null) { + oldPack.setBitmapIndexFile(bitMaps); + } continue; } - list.add(new Pack(packFile, packFilesByExt.get(BITMAP_INDEX))); + list.add(new Pack(config, packFile, packFilesByExt.get(BITMAP_INDEX))); foundNew = true; } @@ -472,9 +502,7 @@ class PackDirectory { return old; } - for (Pack p : forReuse.values()) { - p.close(); - } + Pack.close(new HashSet<>(forReuse.values())); if (list.isEmpty()) { return new PackList(snapshot, NO_PACKS.packs); @@ -533,7 +561,7 @@ class PackDirectory { for (String name : nameList) { try { PackFile pack = new PackFile(directory, name); - if (pack.getPackExt() != null) { + if (pack.getPackExt() != null && !pack.isTmpGCFile()) { Map<PackExt, PackFile> packByExt = packFilesByExtById .get(pack.getId()); if (packByExt == null) { @@ -560,5 +588,13 @@ class PackDirectory { this.snapshot = monitor; this.packs = packs; } + + Iterable<Pack> inReverse() { + return Iterators.iterable(reverseIterator()); + } + + Iterator<Pack> reverseIterator() { + return Iterators.reverseIterator(packs); + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java index 19979d0ed5..5f2015b834 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java @@ -27,6 +27,7 @@ public class PackFile extends File { private static final long serialVersionUID = 1L; private static final String PREFIX = "pack-"; //$NON-NLS-1$ + private static final String TMP_GC_PREFIX = ".tmp-"; //$NON-NLS-1$ private final String base; // PREFIX + id i.e. // pack-0123456789012345678901234567890123456789 @@ -126,6 +127,13 @@ public class PackFile extends File { } /** + * @return whether the file is a temporary GC file + */ + public boolean isTmpGCFile() { + return id.startsWith(TMP_GC_PREFIX); + } + + /** * Create a new similar PackFile with the given extension instead. * * @param ext @@ -177,7 +185,11 @@ public class PackFile extends File { private static PackExt getPackExt(String endsWithExtension) { for (PackExt ext : PackExt.values()) { - if (endsWithExtension.endsWith(ext.getExtension())) { + if (endsWithExtension.equals(ext.getExtension())) { + return ext; + } + + if (endsWithExtension.equals("old-" + ext.getExtension())) { return ext; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java index a784af8c3f..dbf7d8ae5d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java @@ -48,7 +48,6 @@ class PackFileSnapshot extends FileSnapshot { this.checksum = checksum; } - /** {@inheritDoc} */ @Override public boolean isModified(File packFile) { if (!super.isModified(packFile)) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java index f4f62d4205..b3e4efb4fc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java @@ -42,8 +42,8 @@ import org.eclipse.jgit.util.io.SilentFileInputStream; * by ObjectId. * </p> */ -public abstract class PackIndex - implements Iterable<PackIndex.MutableEntry>, ObjectIdSet { +public interface PackIndex + extends Iterable<PackIndex.MutableEntry>, ObjectIdSet { /** * Open an existing pack <code>.idx</code> file for reading. * <p> @@ -61,10 +61,12 @@ public abstract class PackIndex * the file exists but could not be read due to security errors, * unrecognized data version, or unexpected data corruption. */ - public static PackIndex open(File idxFile) throws IOException { + static PackIndex open(File idxFile) throws IOException { try (SilentFileInputStream fd = new SilentFileInputStream( idxFile)) { - return read(fd); + return read(fd); + } catch (FileNotFoundException e) { + throw e; } catch (IOException ioe) { throw new IOException( MessageFormat.format(JGitText.get().unreadablePackIndex, @@ -90,7 +92,7 @@ public abstract class PackIndex * @throws org.eclipse.jgit.errors.CorruptObjectException * the stream does not contain a valid pack index. */ - public static PackIndex read(InputStream fd) throws IOException, + static PackIndex read(InputStream fd) throws IOException, CorruptObjectException { final byte[] hdr = new byte[8]; IO.readFully(fd, hdr, 0, hdr.length); @@ -107,16 +109,13 @@ public abstract class PackIndex } private static boolean isTOC(byte[] h) { - final byte[] toc = PackIndexWriter.TOC; + final byte[] toc = BasePackIndexWriter.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. * @@ -124,13 +123,12 @@ public abstract class PackIndex * the object to look for. Must not be null. * @return true if the object is listed in this index; false otherwise. */ - public boolean hasObject(AnyObjectId id) { + default boolean hasObject(AnyObjectId id) { return findOffset(id) != -1; } - /** {@inheritDoc} */ @Override - public boolean contains(AnyObjectId id) { + default boolean contains(AnyObjectId id) { return findOffset(id) != -1; } @@ -146,7 +144,7 @@ public abstract class PackIndex * </p> */ @Override - public abstract Iterator<MutableEntry> iterator(); + Iterator<MutableEntry> iterator(); /** * Obtain the total number of objects described by this index. @@ -154,7 +152,7 @@ public abstract class PackIndex * @return number of objects in this index, and likewise in the associated * pack that this index was generated from. */ - public abstract long getObjectCount(); + long getObjectCount(); /** * Obtain the total number of objects needing 64 bit offsets. @@ -162,7 +160,7 @@ public abstract class PackIndex * @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. */ - public abstract long getOffset64Count(); + long getOffset64Count(); /** * Get ObjectId for the n-th object entry returned by {@link #iterator()}. @@ -184,7 +182,7 @@ public abstract class PackIndex * is 0, the second is 1, etc. * @return the ObjectId for the corresponding entry. */ - public abstract ObjectId getObjectId(long nthPosition); + ObjectId getObjectId(long nthPosition); /** * Get ObjectId for the n-th object entry returned by {@link #iterator()}. @@ -208,7 +206,7 @@ public abstract class PackIndex * negative, but still valid. * @return the ObjectId for the corresponding entry. */ - public final ObjectId getObjectId(int nthPosition) { + default ObjectId getObjectId(int nthPosition) { if (nthPosition >= 0) return getObjectId((long) nthPosition); final int u31 = nthPosition >>> 1; @@ -227,7 +225,7 @@ public abstract class PackIndex * etc. Positions past 2**31-1 are negative, but still valid. * @return the offset in a pack for the corresponding entry. */ - abstract long getOffset(long nthPosition); + long getOffset(long nthPosition); /** * Locate the file offset position for the requested object. @@ -238,7 +236,7 @@ public abstract class PackIndex * object does not exist in this index and is thus not stored in the * associated pack. */ - public abstract long findOffset(AnyObjectId objId); + long findOffset(AnyObjectId objId); /** * Locate the position of this id in the list of object-ids in the index @@ -249,7 +247,7 @@ public abstract class PackIndex * of ids stored in this index; -1 if the object does not exist in * this index and is thus not stored in the associated pack. */ - public abstract int findPosition(AnyObjectId objId); + int findPosition(AnyObjectId objId); /** * Retrieve stored CRC32 checksum of the requested object raw-data @@ -263,7 +261,7 @@ public abstract class PackIndex * @throws java.lang.UnsupportedOperationException * when this index doesn't support CRC32 checksum */ - public abstract long findCRC32(AnyObjectId objId) + long findCRC32(AnyObjectId objId) throws MissingObjectException, UnsupportedOperationException; /** @@ -271,7 +269,7 @@ public abstract class PackIndex * * @return true if CRC32 is stored, false otherwise */ - public abstract boolean hasCRC32Support(); + boolean hasCRC32Support(); /** * Find objects matching the prefix abbreviation. @@ -287,25 +285,27 @@ public abstract class PackIndex * @throws java.io.IOException * the index cannot be read. */ - public abstract void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, - int matchLimit) throws IOException; + void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, + int matchLimit) throws IOException; /** + * Get pack checksum + * * @return the checksum of the pack; caller must not modify it * @since 5.5 */ - public byte[] getChecksum() { - return packChecksum; - } + byte[] getChecksum(); /** * Represent mutable entry of pack index consisting of object id and offset * in pack (both mutable). * */ - public static class MutableEntry { + class MutableEntry { + /** Buffer of the ObjectId visited by the EntriesIterator. */ final MutableObjectId idBuffer = new MutableObjectId(); + /** Offset into the packfile of the current object. */ long offset; /** @@ -317,42 +317,89 @@ public abstract class PackIndex return offset; } - /** @return hex string describing the object id of this entry. */ + /** + * Get hex string representation of the entry's object id + * + * @return hex string describing the object id of this entry. + */ public String name() { - ensureId(); return idBuffer.name(); } - /** @return a copy of the object id. */ + /** + * Create a copy of the object id + * + * @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 */ + /** + * Clone the entry + * + * @return a complete copy of this entry, that won't modify + */ public MutableEntry cloneEntry() { final MutableEntry r = new MutableEntry(); - ensureId(); r.idBuffer.fromObjectId(idBuffer); r.offset = offset; return r; } - void ensureId() { - // Override in implementations. + /** + * Similar to {@link Comparable#compareTo(Object)}, using only the + * object id in the entry. + * + * @param other + * Another mutable entry (probably from another index) + * + * @return a negative integer, zero, or a positive integer as this + * object is less than, equal to, or greater than the specified + * object. + */ + public int compareBySha1To(MutableEntry other) { + return idBuffer.compareTo(other.idBuffer); + } + + /** + * Copy the current ObjectId to dest + * <p> + * Like {@link #toObjectId()}, but reusing the destination instead of + * creating a new ObjectId instance. + * + * @param dest + * destination for the object id + */ + public void copyOidTo(MutableObjectId dest) { + dest.fromObjectId(idBuffer); } } + /** + * Base implementation of the iterator over index entries. + */ abstract class EntriesIterator implements Iterator<MutableEntry> { - protected final MutableEntry entry = initEntry(); + private final long objectCount; - protected long returnedNumber = 0; + private final MutableEntry entry = new MutableEntry(); - protected abstract MutableEntry initEntry(); + /** Counts number of entries accessed so far. */ + private long returnedNumber = 0; + + /** + * Construct an iterator that can move objectCount times forward. + * + * @param objectCount + * the number of objects in the PackFile. + */ + protected EntriesIterator(long objectCount) { + this.objectCount = objectCount; + } @Override public boolean hasNext() { - return returnedNumber < getObjectCount(); + return returnedNumber < objectCount; } /** @@ -360,7 +407,55 @@ public abstract class PackIndex * element. */ @Override - public abstract MutableEntry next(); + public MutableEntry next() { + readNext(); + returnedNumber++; + return entry; + } + + /** + * Used by subclasses to load the next entry into the MutableEntry. + * <p> + * Subclasses are expected to populate the entry with + * {@link #setIdBuffer} and {@link #setOffset}. + */ + protected abstract void readNext(); + + /** + * Copies to the entry an {@link ObjectId} from the int buffer and + * position idx + * + * @param raw + * the raw data + * @param idx + * the index into {@code raw} + */ + protected void setIdBuffer(int[] raw, int idx) { + entry.idBuffer.fromRaw(raw, idx); + } + + /** + * Copies to the entry an {@link ObjectId} from the byte array at + * position idx. + * + * @param raw + * the raw data + * @param idx + * the index into {@code raw} + */ + protected void setIdBuffer(byte[] raw, int idx) { + entry.idBuffer.fromRaw(raw, idx); + } + + /** + * Sets the {@code offset} to the entry + * + * @param offset + * the offset in the pack file + */ + protected void setOffset(long offset) { + entry.offset = offset; + } @Override public void remove() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java index fff410b4ce..be48358a0d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java @@ -29,13 +29,16 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.NB; -class PackIndexV1 extends PackIndex { +class PackIndexV1 implements PackIndex { private static final int IDX_HDR_LEN = 256 * 4; private static final int RECORD_SIZE = 4 + Constants.OBJECT_ID_LENGTH; private final long[] idxHeader; + /** Footer checksum applied on the bottom of the pack file. */ + protected byte[] packChecksum; + byte[][] idxdata; private long objectCnt; @@ -72,13 +75,11 @@ class PackIndexV1 extends PackIndex { IO.readFully(fd, packChecksum, 0, packChecksum.length); } - /** {@inheritDoc} */ @Override public long getObjectCount() { return objectCnt; } - /** {@inheritDoc} */ @Override public long getOffset64Count() { long n64 = 0; @@ -111,7 +112,6 @@ class PackIndexV1 extends PackIndex { return (int) (nthPosition - base); } - /** {@inheritDoc} */ @Override public ObjectId getObjectId(long nthPosition) { final int levelOne = findLevelOne(nthPosition); @@ -121,14 +121,13 @@ class PackIndexV1 extends PackIndex { } @Override - long getOffset(long nthPosition) { + public long getOffset(long nthPosition) { final int levelOne = findLevelOne(nthPosition); final int levelTwo = getLevelTwo(nthPosition, levelOne); final int p = (4 + Constants.OBJECT_ID_LENGTH) * levelTwo; return NB.decodeUInt32(idxdata[levelOne], p); } - /** {@inheritDoc} */ @Override public long findOffset(AnyObjectId objId) { final int levelOne = objId.getFirstByte(); @@ -142,10 +141,9 @@ class PackIndexV1 extends PackIndex { 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); + return (((long) b0) << 24) | (b1 << 16) | (b2 << 8) | b3; } - /** {@inheritDoc} */ @Override public int findPosition(AnyObjectId objId) { int levelOne = objId.getFirstByte(); @@ -193,25 +191,21 @@ class PackIndexV1 extends PackIndex { return -1; } - /** {@inheritDoc} */ @Override public long findCRC32(AnyObjectId objId) { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public boolean hasCRC32Support() { return false; } - /** {@inheritDoc} */ @Override public Iterator<MutableEntry> iterator() { - return new IndexV1Iterator(); + return new EntriesIteratorV1(this); } - /** {@inheritDoc} */ @Override public void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, int matchLimit) throws IOException { @@ -247,32 +241,35 @@ class PackIndexV1 extends PackIndex { return (RECORD_SIZE * mid) + 4; } - private class IndexV1Iterator extends EntriesIterator { - int levelOne; + @Override + public byte[] getChecksum() { + return packChecksum; + } - int levelTwo; + private static class EntriesIteratorV1 extends EntriesIterator { + private int levelOne; - @Override - protected MutableEntry initEntry() { - return new MutableEntry() { - @Override - protected void ensureId() { - idBuffer.fromRaw(idxdata[levelOne], levelTwo - - Constants.OBJECT_ID_LENGTH); - } - }; + private int levelTwo; + + private final PackIndexV1 packIndex; + + private EntriesIteratorV1(PackIndexV1 packIndex) { + super(packIndex.objectCnt); + this.packIndex = packIndex; } @Override - public MutableEntry next() { - for (; levelOne < idxdata.length; levelOne++) { - if (idxdata[levelOne] == null) + protected void readNext() { + for (; levelOne < packIndex.idxdata.length; levelOne++) { + if (packIndex.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; + if (levelTwo < packIndex.idxdata[levelOne].length) { + super.setOffset(NB.decodeUInt32(packIndex.idxdata[levelOne], + levelTwo)); + this.levelTwo += Constants.OBJECT_ID_LENGTH + 4; + super.setIdBuffer(packIndex.idxdata[levelOne], + levelTwo - Constants.OBJECT_ID_LENGTH); + return; } levelTwo = 0; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java index 7a390060c7..36e54fcd62 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java @@ -28,7 +28,7 @@ import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.NB; /** Support for the pack index v2 format. */ -class PackIndexV2 extends PackIndex { +class PackIndexV2 implements PackIndex { private static final long IS_O64 = 1L << 31; private static final int FANOUT = 256; @@ -37,6 +37,9 @@ class PackIndexV2 extends PackIndex { private static final byte[] NO_BYTES = {}; + /** Footer checksum applied on the bottom of the pack file. */ + protected byte[] packChecksum; + private long objectCnt; private final long[] fanoutTable; @@ -131,13 +134,11 @@ class PackIndexV2 extends PackIndex { IO.readFully(fd, packChecksum, 0, packChecksum.length); } - /** {@inheritDoc} */ @Override public long getObjectCount() { return objectCnt; } - /** {@inheritDoc} */ @Override public long getOffset64Count() { return offset64.length / 8; @@ -165,7 +166,6 @@ class PackIndexV2 extends PackIndex { return (int) (nthPosition - base); } - /** {@inheritDoc} */ @Override public ObjectId getObjectId(long nthPosition) { final int levelOne = findLevelOne(nthPosition); @@ -174,7 +174,6 @@ class PackIndexV2 extends PackIndex { return ObjectId.fromRaw(names[levelOne], p4 + p); // p * 5 } - /** {@inheritDoc} */ @Override public long getOffset(long nthPosition) { final int levelOne = findLevelOne(nthPosition); @@ -182,7 +181,6 @@ class PackIndexV2 extends PackIndex { return getOffset(levelOne, levelTwo); } - /** {@inheritDoc} */ @Override public long findOffset(AnyObjectId objId) { final int levelOne = objId.getFirstByte(); @@ -192,7 +190,6 @@ class PackIndexV2 extends PackIndex { return getOffset(levelOne, levelTwo); } - /** {@inheritDoc} */ @Override public int findPosition(AnyObjectId objId) { int levelOne = objId.getFirstByte(); @@ -211,7 +208,6 @@ class PackIndexV2 extends PackIndex { return p; } - /** {@inheritDoc} */ @Override public long findCRC32(AnyObjectId objId) throws MissingObjectException { final int levelOne = objId.getFirstByte(); @@ -221,19 +217,16 @@ class PackIndexV2 extends PackIndex { return NB.decodeUInt32(crc32[levelOne], levelTwo << 2); } - /** {@inheritDoc} */ @Override public boolean hasCRC32Support() { return true; } - /** {@inheritDoc} */ @Override public Iterator<MutableEntry> iterator() { - return new EntriesIteratorV2(); + return new EntriesIteratorV2(this); } - /** {@inheritDoc} */ @Override public void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, int matchLimit) throws IOException { @@ -291,37 +284,39 @@ class PackIndexV2 extends PackIndex { return -1; } - private class EntriesIteratorV2 extends EntriesIterator { - int levelOne; + @Override + public byte[] getChecksum() { + return packChecksum; + } - int levelTwo; + private static class EntriesIteratorV2 extends EntriesIterator { + private int levelOne = 0; - @Override - protected MutableEntry initEntry() { - return new MutableEntry() { - @Override - protected void ensureId() { - idBuffer.fromRaw(names[levelOne], levelTwo - - Constants.OBJECT_ID_LENGTH / 4); - } - }; + private int levelTwo = 0; + + private final PackIndexV2 packIndex; + + private EntriesIteratorV2(PackIndexV2 packIndex) { + super(packIndex.objectCnt); + this.packIndex = packIndex; } @Override - public MutableEntry next() { - for (; levelOne < names.length; levelOne++) { - if (levelTwo < names[levelOne].length) { + protected void readNext() { + for (; levelOne < packIndex.names.length; levelOne++) { + if (levelTwo < packIndex.names[levelOne].length) { int idx = levelTwo / (Constants.OBJECT_ID_LENGTH / 4) * 4; - long offset = NB.decodeUInt32(offset32[levelOne], idx); + long offset = NB.decodeUInt32(packIndex.offset32[levelOne], + idx); if ((offset & IS_O64) != 0) { idx = (8 * (int) (offset & ~IS_O64)); - offset = NB.decodeUInt64(offset64, idx); + offset = NB.decodeUInt64(packIndex.offset64, idx); } - entry.offset = offset; - - levelTwo += Constants.OBJECT_ID_LENGTH / 4; - returnedNumber++; - return entry; + super.setOffset(offset); + this.levelTwo += Constants.OBJECT_ID_LENGTH / 4; + super.setIdBuffer(packIndex.names[levelOne], + levelTwo - Constants.OBJECT_ID_LENGTH / 4); + return; } levelTwo = 0; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java index e1612bb579..f0b6193066 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java @@ -21,10 +21,10 @@ import org.eclipse.jgit.util.NB; /** * Creates the version 1 (old style) pack table of contents files. * - * @see PackIndexWriter + * @see BasePackIndexWriter * @see PackIndexV1 */ -class PackIndexWriterV1 extends PackIndexWriter { +class PackIndexWriterV1 extends BasePackIndexWriter { static boolean canStore(PackedObjectInfo oe) { // We are limited to 4 GB per pack as offset is 32 bit unsigned int. // @@ -35,7 +35,6 @@ class PackIndexWriterV1 extends PackIndexWriter { super(dst); } - /** {@inheritDoc} */ @Override protected void writeImpl() throws IOException { writeFanOutTable(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java index 7adabad708..b72b35a464 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java @@ -19,10 +19,10 @@ import org.eclipse.jgit.util.NB; /** * Creates the version 2 pack table of contents files. * - * @see PackIndexWriter + * @see BasePackIndexWriter * @see PackIndexV2 */ -class PackIndexWriterV2 extends PackIndexWriter { +class PackIndexWriterV2 extends BasePackIndexWriter { private static final int MAX_OFFSET_32 = 0x7fffffff; private static final int IS_OFFSET_64 = 0x80000000; @@ -30,7 +30,6 @@ class PackIndexWriterV2 extends PackIndexWriter { super(dst); } - /** {@inheritDoc} */ @Override protected void writeImpl() throws IOException { writeTOC(2); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java index 0bceca72ea..fdc2f80075 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java @@ -31,7 +31,6 @@ class PackInputStream extends InputStream { wc.pin(pack, pos); } - /** {@inheritDoc} */ @Override public int read(byte[] b, int off, int len) throws IOException { int n = wc.copy(pack, pos, b, off, len); @@ -39,7 +38,6 @@ class PackInputStream extends InputStream { return n; } - /** {@inheritDoc} */ @Override public int read() throws IOException { byte[] buf = new byte[1]; @@ -47,7 +45,6 @@ class PackInputStream extends InputStream { return n == 1 ? buf[0] & 0xff : -1; } - /** {@inheritDoc} */ @Override public void close() { wc.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java index d6209c4a79..97a854b8cd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java @@ -77,6 +77,7 @@ import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.internal.storage.pack.PackIndexWriter; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; @@ -150,7 +151,6 @@ public class PackInserter extends ObjectInserter { return buffer().length; } - /** {@inheritDoc} */ @Override public ObjectId insert(int type, byte[] data, int off, int len) throws IOException { @@ -166,10 +166,9 @@ public class PackInserter extends ObjectInserter { long offset = beginObject(type, len); packOut.compress.write(data, off, len); packOut.compress.finish(); - return endObject(id, offset); + return endObject(id, offset, len, type); } - /** {@inheritDoc} */ @Override public ObjectId insert(int type, long len, InputStream in) throws IOException { @@ -196,7 +195,7 @@ public class PackInserter extends ObjectInserter { len -= n; } packOut.compress.finish(); - return endObject(md.toObjectId(), offset); + return endObject(md.toObjectId(), offset, len, type); } private long beginObject(int type, long len) throws IOException { @@ -208,10 +207,12 @@ public class PackInserter extends ObjectInserter { return offset; } - private ObjectId endObject(ObjectId id, long offset) { + private ObjectId endObject(ObjectId id, long offset, long len, int type) { PackedObjectInfo obj = new PackedObjectInfo(id); + obj.setType(type); obj.setOffset(offset); obj.setCRC((int) packOut.crc32.getValue()); + obj.setFullSize(len); objectList.add(obj); objectMap.addIfAbsent(obj); return id; @@ -224,6 +225,12 @@ public class PackInserter extends ObjectInserter { p.substring(0, p.lastIndexOf('.')) + ".idx"); //$NON-NLS-1$ } + private static File getFileFor(File packFile, PackExt ext) { + String p = packFile.getName(); + return new File(packFile.getParentFile(), + p.substring(0, p.lastIndexOf('.')) + ext.getExtension()); + } + private void beginPack() throws IOException { objectList = new BlockList<>(); objectMap = new ObjectIdOwnerMap<>(); @@ -243,19 +250,16 @@ public class PackInserter extends ObjectInserter { return 12; } - /** {@inheritDoc} */ @Override public PackParser newPackParser(InputStream in) { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public ObjectReader newReader() { return new Reader(); } - /** {@inheritDoc} */ @Override public void flush() throws IOException { if (tmpPack == null) { @@ -276,7 +280,11 @@ public class PackInserter extends ObjectInserter { Collections.sort(objectList); File tmpIdx = idxFor(tmpPack); // TODO(nasserg) Use PackFile? writePackIndex(tmpIdx, packHash, objectList); - + File tmpObjSizeIdx = null; + if (pconfig.isWriteObjSizeIndex()) { + tmpObjSizeIdx = getFileFor(tmpPack, PackExt.OBJECT_SIZE_INDEX); + writeObjectSizeIndex(tmpObjSizeIdx, objectList, pconfig); + } PackFile realPack = new PackFile(db.getPackDirectory(), computeName(objectList), PackExt.PACK); db.closeAllPackHandles(realPack); @@ -301,6 +309,13 @@ public class PackInserter extends ObjectInserter { realIdx), e); } + if (tmpObjSizeIdx != null) { + PackFile realObjSizeIdx = realPack + .create(PackExt.OBJECT_SIZE_INDEX); + tmpObjSizeIdx.setReadOnly(); + FileUtils.rename(tmpObjSizeIdx, realObjSizeIdx, ATOMIC_MOVE); + } + boolean interrupted = false; try { FileSnapshot snapshot = FileSnapshot.save(realPack); @@ -325,11 +340,21 @@ public class PackInserter extends ObjectInserter { private static void writePackIndex(File idx, byte[] packHash, List<PackedObjectInfo> list) throws IOException { try (OutputStream os = new FileOutputStream(idx)) { - PackIndexWriter w = PackIndexWriter.createVersion(os, INDEX_VERSION); + PackIndexWriter w = BasePackIndexWriter.createVersion(os, + INDEX_VERSION); w.write(list, packHash); } } + private static void writeObjectSizeIndex(File objIdx, + List<PackedObjectInfo> list, PackConfig cfg) throws IOException { + try (OutputStream os = new FileOutputStream(objIdx)) { + PackObjectSizeIndexWriter w = PackObjectSizeIndexWriter + .createWriter(os, cfg.getMinBytesForObjSizeIndex()); + w.write(list); + } + } + private ObjectId computeName(List<PackedObjectInfo> list) { SHA1 md = digest().reset(); byte[] buf = buffer(); @@ -340,7 +365,6 @@ public class PackInserter extends ObjectInserter { return ObjectId.fromRaw(md.digest()); } - /** {@inheritDoc} */ @Override public void close() { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexHelper.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexHelper.java new file mode 100644 index 0000000000..acef741fc6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexHelper.java @@ -0,0 +1,92 @@ +package org.eclipse.jgit.internal.storage.file; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.transport.PackedObjectInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A wrapper to write the object size index for existing packs (on disk) + */ +public class PackObjectSizeIndexHelper { + + private final static Logger LOG = LoggerFactory + .getLogger(PackObjectSizeIndexHelper.class); + + /** + * Add an object size index to all the packs without one in this object + * database. + * + * @param db + * object db for packs + * @param pm + * progress monitor for the object listing phase (adding size to + * the objects in index) + * @throws IOException + * an error reading the packs or writing the object size index + * file + */ + public static void forAllPacks(ObjectDirectory db, ProgressMonitor pm) + throws IOException { + WindowCursor wc = (WindowCursor) db.newReader(); + for (Pack pack : db.getPacks()) { + LOG.info("Checking " + pack.getPackName()); //$NON-NLS-1$ + if (pack.hasObjectSizeIndex()) { + LOG.debug(" has object size index"); //$NON-NLS-1$ + continue; + } + + List<PackedObjectInfo> objectsInPack = getObjectsInPack(wc, pack, + pm); + LOG.debug(String.format(" index has %d objects", //$NON-NLS-1$ + objectsInPack.size())); + if (objectsInPack.isEmpty()) { + continue; + } + + LOG.info(" start writing object size index"); //$NON-NLS-1$ + PackFile packFile = pack.getPackFile() + .create(PackExt.OBJECT_SIZE_INDEX); + long start = System.currentTimeMillis(); + try (FileOutputStream out = new FileOutputStream(packFile)) { + PackObjectSizeIndexWriter writer = PackObjectSizeIndexWriter + .createWriter(out, 0); + writer.write(objectsInPack); + } + LOG.info(String.format(" done writing. Took %d ms", //$NON-NLS-1$ + System.currentTimeMillis() - start)); + } + } + + private static List<PackedObjectInfo> getObjectsInPack(WindowCursor wc, + Pack pack, ProgressMonitor pm) throws IOException { + PackIndex idx = pack.getIndex(); + PackReverseIndex ridx = new PackReverseIndexComputed(idx); + pm.beginTask("Adding size to objects in index", //$NON-NLS-1$ + (int) idx.getObjectCount()); + // This is much faster in offset order + List<PackedObjectInfo> objs = new ArrayList<>( + (int) idx.getObjectCount()); + for (int i = 0; i < idx.getObjectCount(); i++) { + ObjectId oid = ridx.findObjectByPosition(i); + PackedObjectInfo poi = new PackedObjectInfo(oid); + long offset = idx.findOffset(oid); + poi.setFullSize(pack.getObjectSize(wc, offset)); + poi.setType(pack.getObjectType(wc, offset)); + objs.add(poi); + pm.update(1); + } + pm.endTask(); + return objs; + } + + private PackObjectSizeIndexHelper() { + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexLoader.java index 9d6941823a..dff9fa52c8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexLoader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexLoader.java @@ -11,7 +11,9 @@ package org.eclipse.jgit.internal.storage.file; import java.io.IOException; import java.io.InputStream; +import java.text.MessageFormat; import java.util.Arrays; +import org.eclipse.jgit.internal.JGitText; /** * Chooses the specific implementation of the object-size index based on the @@ -26,17 +28,23 @@ public class PackObjectSizeIndexLoader { * input stream at the beginning of the object size data * @return an implementation of the object size index * @throws IOException - * error reading the streams + * error reading the stream, empty stream or content is not an + * object size index */ public static PackObjectSizeIndex load(InputStream in) throws IOException { byte[] header = in.readNBytes(4); if (!Arrays.equals(header, PackObjectSizeIndexWriter.HEADER)) { - throw new IOException("Stream is not an object index"); //$NON-NLS-1$ + throw new IOException(MessageFormat.format( + JGitText.get().unreadableObjectSizeIndex, + Integer.valueOf(header.length), + Arrays.toString(header))); } int version = in.readNBytes(1)[0]; if (version != 1) { - throw new IOException("Unknown object size version: " + version); //$NON-NLS-1$ + throw new IOException(MessageFormat.format( + JGitText.get().unsupportedObjectSizeIndexVersion, + Integer.valueOf(version))); } return PackObjectSizeIndexV1.parse(in); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexV1.java index be2ff67e4f..9957f54fbf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexV1.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexV1.java @@ -12,7 +12,7 @@ package org.eclipse.jgit.internal.storage.file; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; -import java.util.Arrays; +import java.text.MessageFormat; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.util.NB; @@ -35,7 +35,7 @@ class PackObjectSizeIndexV1 implements PackObjectSizeIndex { private final UInt24Array positions24; - private final int[] positions32; + private final IntArray positions32; /** * Parallel array to concat(positions24, positions32) with the size of the @@ -45,35 +45,37 @@ class PackObjectSizeIndexV1 implements PackObjectSizeIndex { * doesn't fit in an int and |value|-1 is the position for the size in the * size64 array e.g. a value of -1 is sizes64[0], -2 = sizes64[1], ... */ - private final int[] sizes32; + private final IntArray sizes32; - private final long[] sizes64; + private final LongArray sizes64; static PackObjectSizeIndex parse(InputStream in) throws IOException { /** Header and version already out of the input */ - IndexInputStreamReader stream = new IndexInputStreamReader(in); - int threshold = stream.readInt(); // minSize - int objCount = stream.readInt(); + byte[] buffer = new byte[8]; + in.readNBytes(buffer, 0, 8); + int threshold = NB.decodeInt32(buffer, 0); // minSize + int objCount = NB.decodeInt32(buffer, 4); if (objCount == 0) { return new EmptyPackObjectSizeIndex(threshold); } - return new PackObjectSizeIndexV1(stream, threshold, objCount); + return new PackObjectSizeIndexV1(in, threshold, objCount); } - private PackObjectSizeIndexV1(IndexInputStreamReader stream, int threshold, + private PackObjectSizeIndexV1(InputStream stream, int threshold, int objCount) throws IOException { this.threshold = threshold; UInt24Array pos24 = null; - int[] pos32 = null; + IntArray pos32 = null; + StreamHelper helper = new StreamHelper(); byte positionEncoding; - while ((positionEncoding = stream.readByte()) != 0) { + while ((positionEncoding = helper.readByte(stream)) != 0) { if (Byte.compareUnsigned(positionEncoding, BITS_24) == 0) { - int sz = stream.readInt(); + int sz = helper.readInt(stream); pos24 = new UInt24Array(stream.readNBytes(sz * 3)); } else if (Byte.compareUnsigned(positionEncoding, BITS_32) == 0) { - int sz = stream.readInt(); - pos32 = stream.readIntArray(sz); + int sz = helper.readInt(stream); + pos32 = IntArray.from(stream, sz); } else { throw new UnsupportedEncodingException( String.format(JGitText.get().unknownPositionEncoding, @@ -81,16 +83,16 @@ class PackObjectSizeIndexV1 implements PackObjectSizeIndex { } } positions24 = pos24 != null ? pos24 : UInt24Array.EMPTY; - positions32 = pos32 != null ? pos32 : new int[0]; + positions32 = pos32 != null ? pos32 : IntArray.EMPTY; - sizes32 = stream.readIntArray(objCount); - int c64sizes = stream.readInt(); + sizes32 = IntArray.from(stream, objCount); + int c64sizes = helper.readInt(stream); if (c64sizes == 0) { - sizes64 = new long[0]; + sizes64 = LongArray.EMPTY; return; } - sizes64 = stream.readLongArray(c64sizes); - int c128sizes = stream.readInt(); + sizes64 = LongArray.from(stream, c64sizes); + int c128sizes = helper.readInt(stream); if (c128sizes != 0) { // this MUST be 0 (we don't support 128 bits sizes yet) throw new IOException(JGitText.get().unsupportedSizesObjSizeIndex); @@ -102,8 +104,8 @@ class PackObjectSizeIndexV1 implements PackObjectSizeIndex { int pos = -1; if (!positions24.isEmpty() && idxOffset <= positions24.getLastValue()) { pos = positions24.binarySearch(idxOffset); - } else if (positions32.length > 0 && idxOffset >= positions32[0]) { - int pos32 = Arrays.binarySearch(positions32, idxOffset); + } else if (!positions32.empty() && idxOffset >= positions32.get(0)) { + int pos32 = positions32.binarySearch(idxOffset); if (pos32 >= 0) { pos = pos32 + positions24.size(); } @@ -112,17 +114,17 @@ class PackObjectSizeIndexV1 implements PackObjectSizeIndex { return -1; } - int objSize = sizes32[pos]; + int objSize = sizes32.get(pos); if (objSize < 0) { int secondPos = Math.abs(objSize) - 1; - return sizes64[secondPos]; + return sizes64.get(secondPos); } return objSize; } @Override public long getObjectCount() { - return positions24.size() + positions32.length; + return (long) positions24.size() + positions32.size(); } @Override @@ -131,69 +133,128 @@ class PackObjectSizeIndexV1 implements PackObjectSizeIndex { } /** - * Wrapper to read parsed content from the byte stream + * A byte[] that should be interpreted as an int[] */ - private static class IndexInputStreamReader { + private static class IntArray { + private static final IntArray EMPTY = new IntArray(new byte[0]); - private final byte[] buffer = new byte[8]; + private static final int INT_SIZE = 4; - private final InputStream in; + private final byte[] data; - IndexInputStreamReader(InputStream in) { - this.in = in; - } + private final int size; - int readInt() throws IOException { - int n = in.readNBytes(buffer, 0, 4); - if (n < 4) { - throw new IOException(JGitText.get().unableToReadFullInt); + static IntArray from(InputStream in, int ints) throws IOException { + int expectedBytes = ints * INT_SIZE; + byte[] data = in.readNBytes(expectedBytes); + if (data.length < expectedBytes) { + throw new IOException(MessageFormat + .format(JGitText.get().unableToReadFullArray, + Integer.valueOf(ints))); } - return NB.decodeInt32(buffer, 0); + return new IntArray(data); + } + + private IntArray(byte[] data) { + this.data = data; + size = data.length / INT_SIZE; } - int[] readIntArray(int intsCount) throws IOException { - if (intsCount == 0) { - return new int[0]; + /** + * Returns position of element in array, -1 if not there + * + * @param needle + * element to look for + * @return position of the element in the array or -1 if not found + */ + int binarySearch(int needle) { + if (size == 0) { + return -1; } + int high = size; + int low = 0; + do { + int mid = (low + high) >>> 1; + int cmp = Integer.compare(needle, get(mid)); + if (cmp < 0) + high = mid; + else if (cmp == 0) { + return mid; + } else + low = mid + 1; + } while (low < high); + return -1; + } - int[] dest = new int[intsCount]; - for (int i = 0; i < intsCount; i++) { - dest[i] = readInt(); + int get(int position) { + if (position < 0 || position >= size) { + throw new IndexOutOfBoundsException(position); } - return dest; + return NB.decodeInt32(data, position * INT_SIZE); } - long readLong() throws IOException { - int n = in.readNBytes(buffer, 0, 8); - if (n < 8) { - throw new IOException(JGitText.get().unableToReadFullInt); + boolean empty() { + return size == 0; + } + + int size() { + return size; + } + } + + /** + * A byte[] that should be interpreted as an long[] + */ + private static class LongArray { + private static final LongArray EMPTY = new LongArray(new byte[0]); + + private static final int LONG_SIZE = 8; // bytes + + private final byte[] data; + + private final int size; + + static LongArray from(InputStream in, int longs) throws IOException { + byte[] data = in.readNBytes(longs * LONG_SIZE); + if (data.length < longs * LONG_SIZE) { + throw new IOException(MessageFormat + .format(JGitText.get().unableToReadFullArray, + Integer.valueOf(longs))); } - return NB.decodeInt64(buffer, 0); + return new LongArray(data); } - long[] readLongArray(int longsCount) throws IOException { - if (longsCount == 0) { - return new long[0]; + private LongArray(byte[] data) { + this.data = data; + size = data.length / LONG_SIZE; + } + + long get(int position) { + if (position < 0 || position >= size) { + throw new IndexOutOfBoundsException(position); } + return NB.decodeInt64(data, position * LONG_SIZE); + } + } - long[] dest = new long[longsCount]; - for (int i = 0; i < longsCount; i++) { - dest[i] = readLong(); + private static class StreamHelper { + private final byte[] buffer = new byte[8]; + + int readInt(InputStream in) throws IOException { + int n = in.readNBytes(buffer, 0, 4); + if (n < 4) { + throw new IOException(JGitText.get().unableToReadFullInt); } - return dest; + return NB.decodeInt32(buffer, 0); } - byte readByte() throws IOException { + byte readByte(InputStream in) throws IOException { int n = in.readNBytes(buffer, 0, 1); if (n != 1) { throw new IOException(JGitText.get().cannotReadByte); } return buffer[0]; } - - byte[] readNBytes(int sz) throws IOException { - return in.readNBytes(sz); - } } private static class EmptyPackObjectSizeIndex diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexWriter.java index 65a065dd55..328643688d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexWriter.java @@ -72,11 +72,13 @@ public abstract class PackObjectSizeIndexWriter { * * Store position (in the main index) to size as parallel arrays. * - * <p>Positions in the main index fit well in unsigned 24 bits (16M) for most + * <p> + * Positions in the main index fit well in unsigned 24 bits (16M) for most * repositories, but some outliers have even more objects, so we need to * store also 32 bits positions. * - * <p>Sizes are stored as a first array parallel to positions. If a size + * <p> + * Sizes are stored as a first array parallel to positions. If a size * doesn't fit in an element of that array, then we encode there a position * on the next-size array. This "overflow" array doesn't have entries for * all positions. @@ -85,30 +87,36 @@ public abstract class PackObjectSizeIndexWriter { * * positions [10, 500, 1000, 1001] * sizes (32bits) [15MB, -1, 6MB, -2] - * ___/ ______/ + * ___/ ______/ * / / * sizes (64 bits) [3GB, 6GB] * </pre> * - * <p>For sizes we use 32 bits as the first level and 64 for the rare objects + * <p> + * For sizes we use 32 bits as the first level and 64 for the rare objects * over 2GB. * - * <p>A 24/32/64 bits hierarchy of arrays saves space if we have a lot of small - * objects, but wastes space if we have only big ones. The min size to index is - * controlled by conf and in principle we want to index only rather - * big objects (e.g. > 10MB). We could support more dynamics read/write of sizes + * <p> + * A 24/32/64 bits hierarchy of arrays saves space if we have a lot of small + * objects, but wastes space if we have only big ones. The min size to index + * is controlled by conf and in principle we want to index only rather big + * objects (e.g. > 10MB). We could support more dynamics read/write of sizes * (e.g. 24 only if the threshold will include many of those objects) but it - * complicates a lot code and spec. If needed it could go for a v2 of the protocol. - * - * <p>Format: + * complicates a lot code and spec. If needed it could go for a v2 of the + * protocol. * + * <p> + * Format: + * <ul> * <li>A header with the magic number (4 bytes) * <li>The index version (1 byte) * <li>The minimum object size (4 bytes) * <li>Total count of objects indexed (C, 4 bytes) + * </ul> * (if count == 0, stop here) - * + * <p> * Blocks of + * <ul> * <li>Size per entry in bits (1 byte, either 24 (0x18) or 32 (0x20)) * <li>Count of entries (4 bytes) (c, as a signed int) * <li>positions encoded in s bytes each (i.e s*c bytes) @@ -120,6 +128,7 @@ public abstract class PackObjectSizeIndexWriter { * <li>Count of 64 bit sizes (s64) (or 0 if no more indirections) * <li>64 bit sizes (s64 * 8 bytes) * <li>0 (end) + * </ul> */ static class PackObjectSizeWriterV1 extends PackObjectSizeIndexWriter { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java index 1a5adb4a16..720a3bcbff 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java @@ -10,11 +10,8 @@ package org.eclipse.jgit.internal.storage.file; -import java.text.MessageFormat; - import org.eclipse.jgit.errors.CorruptObjectException; -import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry; +import org.eclipse.jgit.errors.PackMismatchException; import org.eclipse.jgit.lib.ObjectId; /** @@ -27,92 +24,27 @@ import org.eclipse.jgit.lib.ObjectId; * @see PackIndex * @see Pack */ -public class PackReverseIndex { - /** Index we were created from, and that has our ObjectId data. */ - private final PackIndex index; - - /** The number of bytes per entry in the offsetIndex. */ - private final long bucketSize; - +public interface PackReverseIndex { /** - * An index into the nth mapping, where the value is the position after the - * the last index that contains the values of the bucket. For example given - * offset o (and bucket = o / bucketSize), the offset will be contained in - * the range nth[offsetIndex[bucket - 1]] inclusive to - * nth[offsetIndex[bucket]] exclusive. - * - * See {@link #binarySearch} + * Magic bytes that uniquely identify git reverse index files. */ - private final int[] offsetIndex; + byte[] MAGIC = { 'R', 'I', 'D', 'X' }; - /** Mapping from indices in offset order to indices in SHA-1 order. */ - private final int[] nth; + /** + * The first reverse index file version. + */ + int VERSION_1 = 1; /** - * Create reverse index from straight/forward pack index, by indexing all - * its entries. + * Verify that the pack checksum found in the reverse index matches that + * from the pack file. * - * @param packIndex - * forward index - entries to (reverse) index. + * @param packFilePath + * the path to display in event of a mismatch + * @throws PackMismatchException + * if the checksums do not match */ - public PackReverseIndex(PackIndex packIndex) { - index = packIndex; - - final long cnt = index.getObjectCount(); - if (cnt + 1 > Integer.MAX_VALUE) - throw new IllegalArgumentException( - JGitText.get().hugeIndexesAreNotSupportedByJgitYet); - - if (cnt == 0) { - bucketSize = Long.MAX_VALUE; - offsetIndex = new int[1]; - nth = new int[0]; - return; - } - - final long[] offsetsBySha1 = new long[(int) cnt]; - - long maxOffset = 0; - int ith = 0; - for (MutableEntry me : index) { - final long o = me.getOffset(); - offsetsBySha1[ith++] = o; - if (o > maxOffset) - maxOffset = o; - } - - bucketSize = maxOffset / cnt + 1; - int[] bucketIndex = new int[(int) cnt]; - int[] bucketValues = new int[(int) cnt + 1]; - for (int oi = 0; oi < offsetsBySha1.length; oi++) { - final long o = offsetsBySha1[oi]; - final int bucket = (int) (o / bucketSize); - final int bucketValuesPos = oi + 1; - final int current = bucketIndex[bucket]; - bucketIndex[bucket] = bucketValuesPos; - bucketValues[bucketValuesPos] = current; - } - - int nthByOffset = 0; - nth = new int[offsetsBySha1.length]; - offsetIndex = bucketIndex; // Reuse the allocation - for (int bi = 0; bi < bucketIndex.length; bi++) { - final int start = nthByOffset; - // Insertion sort of the values in the bucket. - for (int vi = bucketIndex[bi]; vi > 0; vi = bucketValues[vi]) { - final int nthBySha1 = vi - 1; - final long o = offsetsBySha1[nthBySha1]; - int insertion = nthByOffset++; - for (; start < insertion; insertion--) { - if (o > offsetsBySha1[nth[insertion - 1]]) - break; - nth[insertion] = nth[insertion - 1]; - } - nth[insertion] = nthBySha1; - } - offsetIndex[bi] = nthByOffset; - } - } + void verifyPackChecksum(String packFilePath) throws PackMismatchException; /** * Search for object id with the specified start offset in this pack @@ -122,12 +54,7 @@ public class PackReverseIndex { * start offset of object to find. * @return object id for this offset, or null if no object was found. */ - public ObjectId findObject(long offset) { - final int ith = binarySearch(offset); - if (ith < 0) - return null; - return index.getObjectId(nth[ith]); - } + ObjectId findObject(long offset); /** * Search for the next offset to the specified offset in this pack (reverse) @@ -144,42 +71,25 @@ public class PackReverseIndex { * @throws org.eclipse.jgit.errors.CorruptObjectException * when there is no object with the provided offset. */ - public long findNextOffset(long offset, long maxOffset) - throws CorruptObjectException { - final int ith = binarySearch(offset); - if (ith < 0) - throw new CorruptObjectException( - MessageFormat.format( - JGitText.get().cantFindObjectInReversePackIndexForTheSpecifiedOffset, - Long.valueOf(offset))); - - if (ith + 1 == nth.length) - return maxOffset; - return index.getOffset(nth[ith + 1]); - } - - int findPosition(long offset) { - return binarySearch(offset); - } + long findNextOffset(long offset, long maxOffset) + throws CorruptObjectException; - private int binarySearch(long offset) { - int bucket = (int) (offset / bucketSize); - int low = bucket == 0 ? 0 : offsetIndex[bucket - 1]; - int high = offsetIndex[bucket]; - while (low < high) { - final int mid = (low + high) >>> 1; - final long o = index.getOffset(nth[mid]); - if (offset < o) - high = mid; - else if (offset == o) - return mid; - else - low = mid + 1; - } - return -1; - } + /** + * Find the position in the reverse index of the object at the given pack + * offset. + * + * @param offset + * the pack offset of the object + * @return the position in the reverse index of the object + */ + int findPosition(long offset); - ObjectId findObjectByPosition(int nthPosition) { - return index.getObjectId(nth[nthPosition]); - } + /** + * Find the object that is in the given position in the reverse index. + * + * @param nthPosition + * the position of the object in the reverse index + * @return the object in that position + */ + ObjectId findObjectByPosition(int nthPosition); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputed.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputed.java new file mode 100644 index 0000000000..0b487a2819 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputed.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2023, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.file; + +import java.text.MessageFormat; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.PackMismatchException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry; +import org.eclipse.jgit.lib.ObjectId; + +/** + * Reverse index for forward pack index which is computed from the forward pack + * index. + * <p> + * Creating an instance uses an insertion sort of the entries in the forward + * index, so it runs in quadratic time on average. + */ +final class PackReverseIndexComputed implements PackReverseIndex { + /** + * Index we were created from, and that has our ObjectId data. + */ + private final PackIndex index; + + /** + * The difference in offset between the start of an offset bucket and the + * start of its succeeding bucket. + */ + private final long bucketSize; + + /** + * The indexes into indexPosInOffsetOrder at which the next bucket starts. + * <p> + * For example, given offset o (and therefore bucket = o / bucketSize), the + * indexPos corresponding to o will be contained in the range + * indexPosInOffsetOrder[nextBucketStart[bucket - 1]] inclusive to + * indexPosInOffsetOrder[nextBucketStart[bucket]] exclusive. + * <p> + * This range information can speed up #binarySearch by identifying the + * relevant bucket and only searching within its range. + * <p> + * See {@link #binarySearch} + */ + private final int[] nextBucketStart; + + /** + * Mapping from indices in offset order to indices in SHA-1 order. + */ + private final int[] indexPosInOffsetOrder; + + /** + * Create reverse index from straight/forward pack index, by indexing all + * its entries. + * + * @param packIndex + * forward index - entries to (reverse) index. + */ + PackReverseIndexComputed(PackIndex packIndex) { + index = packIndex; + + long rawCnt = index.getObjectCount(); + if (rawCnt + 1 > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + JGitText.get().hugeIndexesAreNotSupportedByJgitYet); + } + int cnt = (int) rawCnt; + + if (cnt == 0) { + bucketSize = Long.MAX_VALUE; + nextBucketStart = new int[1]; + indexPosInOffsetOrder = new int[0]; + return; + } + + // Sort the index positions according to the corresponding pack offsets. + // Use bucket sort since the offsets are somewhat uniformly distributed + // over the range (0, pack size). + long[] offsetsInIndexOrder = new long[cnt]; + long maxOffset = 0; + int i = 0; + for (MutableEntry entry : index) { + long offset = entry.getOffset(); + offsetsInIndexOrder[i++] = offset; + if (offset > maxOffset) { + maxOffset = offset; + } + } + + bucketSize = maxOffset / cnt + 1; + // The values in each bucket, stored as a linked list. Given a bucket, + // headValues[bucket] contains the first value, + // furtherValues[headValues[bucket]] contains the second, + // furtherValues[furtherValues[headValues[bucket]]] the third, and so + // on. The linked list stops when a value is 0. The values themselves + // are shifted index positions. There won't be any + // collisions because every index position is unique. + int[] headValues = new int[cnt]; + int[] furtherValues = new int[cnt + 1]; + for (int indexPos = 0; indexPos < cnt; indexPos++) { + // The offset determines which bucket this index position falls + // into, since the goal is sort into offset order. + long offset = offsetsInIndexOrder[indexPos]; + int bucket = (int) (offset / bucketSize); + // Store the index positions as 1-indexed so that default + // initialized value 0 can be interpreted as the end of the bucket + // values. + int asBucketValue = indexPos + 1; + // If there is an existing value in this bucket, push the value to + // the front of the linked list. + int current = headValues[bucket]; + headValues[bucket] = asBucketValue; + furtherValues[asBucketValue] = current; + } + + int nthByOffset = 0; + indexPosInOffsetOrder = new int[cnt]; + nextBucketStart = headValues; // Reuse the allocation + for (int bi = 0; bi < headValues.length; bi++) { + // Insertion sort of the values in the bucket. + int start = nthByOffset; + for (int vi = headValues[bi]; vi > 0; vi = furtherValues[vi]) { + int nthBySha1 = vi - 1; + long o = offsetsInIndexOrder[nthBySha1]; + int insertion = nthByOffset++; + for (; start < insertion; insertion--) { + if (o > offsetsInIndexOrder[indexPosInOffsetOrder[insertion + - 1]]) { + break; + } + indexPosInOffsetOrder[insertion] = indexPosInOffsetOrder[insertion + - 1]; + } + indexPosInOffsetOrder[insertion] = nthBySha1; + } + nextBucketStart[bi] = nthByOffset; + } + } + + @Override + public void verifyPackChecksum(String packFilePath) + throws PackMismatchException { + // There is no file with a checksum. + } + + @Override + public ObjectId findObject(long offset) { + final int ith = binarySearch(offset); + if (ith < 0) { + return null; + } + return index.getObjectId(indexPosInOffsetOrder[ith]); + } + + @Override + public long findNextOffset(long offset, long maxOffset) + throws CorruptObjectException { + final int ith = binarySearch(offset); + if (ith < 0) { + throw new CorruptObjectException(MessageFormat.format(JGitText + .get().cantFindObjectInReversePackIndexForTheSpecifiedOffset, + Long.valueOf(offset))); + } + + if (ith + 1 == indexPosInOffsetOrder.length) { + return maxOffset; + } + return index.getOffset(indexPosInOffsetOrder[ith + 1]); + } + + @Override + public int findPosition(long offset) { + return binarySearch(offset); + } + + private int binarySearch(long offset) { + int bucket = (int) (offset / bucketSize); + int low = bucket == 0 ? 0 : nextBucketStart[bucket - 1]; + int high = nextBucketStart[bucket]; + while (low < high) { + final int mid = (low + high) >>> 1; + final long o = index.getOffset(indexPosInOffsetOrder[mid]); + if (offset < o) { + high = mid; + } else if (offset == o) { + return mid; + } else { + low = mid + 1; + } + } + return -1; + } + + @Override + public ObjectId findObjectByPosition(int nthPosition) { + return index.getObjectId(indexPosInOffsetOrder[nthPosition]); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexFactory.java new file mode 100644 index 0000000000..32830c3cf0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexFactory.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.file; + +import static org.eclipse.jgit.internal.storage.file.PackReverseIndex.MAGIC; +import static org.eclipse.jgit.internal.storage.file.PackReverseIndex.VERSION_1; + +import java.io.DataInput; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.security.DigestInputStream; +import java.text.MessageFormat; +import java.util.Arrays; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.io.SilentFileInputStream; + +/** + * Factory for creating instances of {@link PackReverseIndex}. + */ +public final class PackReverseIndexFactory { + /** + * Create an in-memory pack reverse index by reading it from the given file + * if the file exists, or computing it from the given pack index if the file + * doesn't exist. + * + * @param idxFile + * the file to read the pack file from, if it exists + * @param objectCount + * the number of objects in the corresponding pack + * @param packIndexSupplier + * a function to lazily get the corresponding forward index + * @return the reverse index instance + * @throws IOException + * if reading from the file fails + */ + static PackReverseIndex openOrCompute(File idxFile, long objectCount, + PackBitmapIndex.SupplierWithIOException<PackIndex> packIndexSupplier) + throws IOException { + try (SilentFileInputStream fd = new SilentFileInputStream(idxFile)) { + return readFromFile(fd, objectCount, packIndexSupplier); + } catch (FileNotFoundException e) { + return computeFromIndex(packIndexSupplier.get()); + } catch (IOException e) { + throw new IOException( + MessageFormat.format(JGitText.get().unreadablePackIndex, + idxFile.getAbsolutePath()), + e); + } + } + + /** + * Compute an in-memory pack reverse index from the in-memory pack forward + * index. This computation uses insertion sort, which has a quadratic + * runtime on average. + * + * @param packIndex + * the forward index to compute from + * @return the reverse index instance + */ + public static PackReverseIndex computeFromIndex(PackIndex packIndex) { + return new PackReverseIndexComputed(packIndex); + } + + /** + * Read an in-memory pack reverse index from the given input stream. This + * has a linear runtime. + * + * @param src + * the input stream to read the contents from + * @param objectCount + * the number of objects in the corresponding pack + * @param packIndexSupplier + * a function to lazily get the corresponding forward index + * @return the reverse index instance + * @throws IOException + * if reading from the input stream fails + */ + static PackReverseIndex readFromFile(InputStream src, long objectCount, + PackBitmapIndex.SupplierWithIOException<PackIndex> packIndexSupplier) + throws IOException { + final DigestInputStream digestIn = new DigestInputStream(src, + Constants.newMessageDigest()); + + final byte[] magic = new byte[MAGIC.length]; + IO.readFully(digestIn, magic); + if (!Arrays.equals(magic, MAGIC)) { + throw new IOException( + MessageFormat.format(JGitText.get().expectedGot, + Arrays.toString(MAGIC), Arrays.toString(magic))); + } + + DataInput dataIn = new SimpleDataInput(digestIn); + int version = dataIn.readInt(); + switch (version) { + case VERSION_1: + return new PackReverseIndexV1(digestIn, objectCount, + packIndexSupplier); + default: + throw new IOException(MessageFormat.format( + JGitText.get().unsupportedPackReverseIndexVersion, + String.valueOf(version))); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1.java new file mode 100644 index 0000000000..6a47352124 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2022, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.file; + +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; + +import java.io.DataInput; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.DigestInputStream; +import java.text.MessageFormat; +import java.util.Arrays; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.PackMismatchException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.util.Hex; +import org.eclipse.jgit.util.IO; + +/** + * Reverse index for forward pack index which is parsed from a version 1 file. + * <p> + * The file format is specified at + * https://git-scm.com/docs/pack-format#_pack_rev_files_have_the_format. + */ +final class PackReverseIndexV1 implements PackReverseIndex { + static final int OID_VERSION_SHA1 = 1; + + static final int OID_VERSION_SHA256 = 2; + + private static final int SHA1_BYTES = OBJECT_ID_LENGTH; + + private final DigestInputStream inputStream; + + private final DataInput dataIn; + + /** + * A lazy supplier for the corresponding PackIndex. The PackIndex is not + * needed during instantiation and parsing, only later when querying the + * reverse index. Allow lazy loading so that the parsing of the forward and + * reverse indices could happen in parallel. + */ + private final PackBitmapIndex.SupplierWithIOException<PackIndex> packIndexSupplier; + + private int objectCount; + + private byte[] packChecksum; + + private int[] indexPositionsSortedByOffset; + + private PackIndex packIndex; + + PackReverseIndexV1(DigestInputStream inputStream, long objectCount, + PackBitmapIndex.SupplierWithIOException<PackIndex> packIndexSupplier) + throws IOException { + try { + this.objectCount = Math.toIntExact(objectCount); + } catch (ArithmeticException e) { + throw new IllegalArgumentException( + JGitText.get().hugeIndexesAreNotSupportedByJgitYet, e); + } + + this.inputStream = inputStream; + dataIn = new SimpleDataInput(inputStream); + + int oid_version = dataIn.readInt(); + switch (oid_version) { + case OID_VERSION_SHA1: + // JGit Pack only supports AnyObjectId, which represents SHA1. + break; + case OID_VERSION_SHA256: + throw new IOException(MessageFormat.format( + JGitText.get().unsupportedObjectIdVersion, "SHA256")); //$NON-NLS-1$ + default: + throw new IOException(MessageFormat.format( + JGitText.get().unsupportedObjectIdVersion, + String.valueOf(oid_version))); + } + + indexPositionsSortedByOffset = new int[this.objectCount]; + this.packIndexSupplier = packIndexSupplier; + + parseBody(); + parseChecksums(); + } + + @Override + public void verifyPackChecksum(String packFilePath) + throws PackMismatchException { + if (!Arrays.equals(packChecksum, getPackIndex().getChecksum())) { + throw new PackMismatchException( + MessageFormat.format(JGitText.get().packChecksumMismatch, + packFilePath, PackExt.INDEX.getExtension(), + Hex.toHexString(getPackIndex().getChecksum()), + PackExt.REVERSE_INDEX.getExtension(), + Hex.toHexString(packChecksum))); + } + } + + private void parseBody() throws IOException { + for (int i = 0; i < objectCount; i++) { + indexPositionsSortedByOffset[i] = dataIn.readInt(); + } + } + + private void parseChecksums() throws IOException { + packChecksum = new byte[SHA1_BYTES]; + IO.readFully(inputStream, packChecksum); + + // Take digest before reading the self checksum changes it. + byte[] observedSelfChecksum = inputStream.getMessageDigest().digest(); + + byte[] readSelfChecksum = new byte[SHA1_BYTES]; + IO.readFully(inputStream, readSelfChecksum); + + if (!Arrays.equals(readSelfChecksum, observedSelfChecksum)) { + throw new CorruptObjectException(MessageFormat.format( + JGitText.get().corruptReverseIndexChecksumIncorrect, + Hex.toHexString(readSelfChecksum), + Hex.toHexString(observedSelfChecksum))); + } + } + + @Override + public ObjectId findObject(long offset) { + int reversePosition = findPosition(offset); + if (reversePosition < 0) { + return null; + } + int forwardPosition = findForwardPositionByReversePosition( + reversePosition); + return getPackIndex().getObjectId(forwardPosition); + } + + @Override + public long findNextOffset(long offset, long maxOffset) + throws CorruptObjectException { + int position = findPosition(offset); + if (position < 0) { + throw new CorruptObjectException(MessageFormat.format(JGitText + .get().cantFindObjectInReversePackIndexForTheSpecifiedOffset, + Long.valueOf(offset))); + } + if (position + 1 == objectCount) { + return maxOffset; + } + return findOffsetByReversePosition(position + 1); + } + + @Override + public int findPosition(long offset) { + return binarySearchByOffset(offset); + } + + @Override + public ObjectId findObjectByPosition(int position) { + return getPackIndex() + .getObjectId(findForwardPositionByReversePosition(position)); + } + + private long findOffsetByReversePosition(int position) { + return getPackIndex() + .getOffset(findForwardPositionByReversePosition(position)); + } + + private int findForwardPositionByReversePosition(int reversePosition) { + assert (reversePosition >= 0); + assert (reversePosition < indexPositionsSortedByOffset.length); + return indexPositionsSortedByOffset[reversePosition]; + } + + private int binarySearchByOffset(long wantedOffset) { + int low = 0; + int high = objectCount - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + long offsetAtMid = findOffsetByReversePosition(mid); + if (offsetAtMid == wantedOffset) { + return mid; + } else if (offsetAtMid > wantedOffset) { + high = mid - 1; + } else { + low = mid + 1; + } + } + return -1; + } + + private PackIndex getPackIndex() { + if (packIndex == null) { + try { + packIndex = packIndexSupplier.get(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return packIndex; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexWriter.java index 4c8417b115..94a4e9c129 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexWriter.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.internal.storage.file.PackReverseIndex.VERSION_1; + import java.io.BufferedOutputStream; import java.io.DataOutput; import java.io.IOException; @@ -29,16 +31,6 @@ import org.eclipse.jgit.transport.PackedObjectInfo; */ public abstract class PackReverseIndexWriter { /** - * Magic bytes that uniquely identify git reverse index files. - */ - protected static byte[] MAGIC = { 'R', 'I', 'D', 'X' }; - - /** - * The first reverse index file version. - */ - protected static final int VERSION_1 = 1; - - /** * Stream to write contents to while maintaining a checksum. */ protected final DigestOutputStream out; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexWriterV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexWriterV1.java index 7630724d09..ff1809f446 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexWriterV1.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndexWriterV1.java @@ -9,6 +9,10 @@ */ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.internal.storage.file.PackReverseIndex.MAGIC; +import static org.eclipse.jgit.internal.storage.file.PackReverseIndex.VERSION_1; +import static org.eclipse.jgit.internal.storage.file.PackReverseIndexV1.OID_VERSION_SHA1; + import java.io.IOException; import java.io.OutputStream; import java.util.List; @@ -24,8 +28,6 @@ import org.eclipse.jgit.util.IntList.IntComparator; * https://git-scm.com/docs/pack-format#_pack_rev_files_have_the_format. */ final class PackReverseIndexWriterV1 extends PackReverseIndexWriter { - private static final int OID_VERSION_SHA1 = 1; - private static final int DEFAULT_OID_VERSION = OID_VERSION_SHA1; PackReverseIndexWriterV1(final OutputStream dst) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java index 106313db63..5584f13db1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java @@ -98,7 +98,6 @@ class PackedBatchRefUpdate extends BatchRefUpdate { this.shouldLockLooseRefs = shouldLockLooseRefs; } - /** {@inheritDoc} */ @Override public void execute(RevWalk walk, ProgressMonitor monitor, List<String> options) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java index 0416a648e5..319a9ed710 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java @@ -16,6 +16,7 @@ package org.eclipse.jgit.internal.storage.file; import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.lib.Constants.HEAD; import static org.eclipse.jgit.lib.Constants.LOGS; +import static org.eclipse.jgit.lib.Constants.L_LOGS; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; import static org.eclipse.jgit.lib.Constants.PACKED_REFS; import static org.eclipse.jgit.lib.Constants.R_HEADS; @@ -37,39 +38,44 @@ import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.Paths; import java.security.DigestInputStream; import java.security.MessageDigest; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.api.PackRefsCommand; import org.eclipse.jgit.errors.InvalidObjectIdException; import org.eclipse.jgit.errors.LockFailedException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.ObjectWritingException; import org.eclipse.jgit.events.RefsChangedEvent; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.CoreConfig.TrustPackedRefsStat; +import org.eclipse.jgit.lib.CoreConfig; +import org.eclipse.jgit.lib.CoreConfig.TrustStat; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefComparator; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefWriter; +import org.eclipse.jgit.lib.ReflogReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.SymbolicRef; import org.eclipse.jgit.revwalk.RevObject; @@ -81,6 +87,7 @@ import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; import org.eclipse.jgit.util.RefList; import org.eclipse.jgit.util.RefMap; +import org.eclipse.jgit.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -120,6 +127,8 @@ public class RefDirectory extends RefDatabase { private final File gitDir; + private final File gitCommonDir; + final File refsDir; final File packedRefsFile; @@ -155,7 +164,7 @@ public class RefDirectory extends RefDatabase { * {@code RepositoryCache} is used, this lock instance will be used by all * threads. */ - final ReentrantLock inProcessPackedRefsLock = new ReentrantLock(true); + final ReentrantLock inProcessPackedRefsLock; /** * Number of modifications made to this database. @@ -175,41 +184,36 @@ public class RefDirectory extends RefDatabase { private List<Integer> retrySleepMs = RETRY_SLEEP_MS; - private final boolean trustFolderStat; - - private final TrustPackedRefsStat trustPackedRefsStat; + private final CoreConfig coreConfig; RefDirectory(RefDirectory refDb) { parent = refDb.parent; gitDir = refDb.gitDir; + gitCommonDir = refDb.gitCommonDir; refsDir = refDb.refsDir; logsDir = refDb.logsDir; logsRefsDir = refDb.logsRefsDir; packedRefsFile = refDb.packedRefsFile; looseRefs.set(refDb.looseRefs.get()); packedRefs.set(refDb.packedRefs.get()); - trustFolderStat = refDb.trustFolderStat; - trustPackedRefsStat = refDb.trustPackedRefsStat; + coreConfig = refDb.coreConfig; + inProcessPackedRefsLock = refDb.inProcessPackedRefsLock; } RefDirectory(FileRepository db) { final FS fs = db.getFS(); parent = db; gitDir = db.getDirectory(); - refsDir = fs.resolve(gitDir, R_REFS); - logsDir = fs.resolve(gitDir, LOGS); - logsRefsDir = fs.resolve(gitDir, LOGS + '/' + R_REFS); - packedRefsFile = fs.resolve(gitDir, PACKED_REFS); + gitCommonDir = db.getCommonDirectory(); + refsDir = fs.resolve(gitCommonDir, R_REFS); + logsDir = fs.resolve(gitCommonDir, LOGS); + logsRefsDir = fs.resolve(gitCommonDir, L_LOGS + R_REFS); + packedRefsFile = fs.resolve(gitCommonDir, PACKED_REFS); looseRefs.set(RefList.<LooseRef> emptyList()); packedRefs.set(NO_PACKED_REFS); - trustFolderStat = db.getConfig() - .getBoolean(ConfigConstants.CONFIG_CORE_SECTION, - ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true); - trustPackedRefsStat = db.getConfig() - .getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, - ConfigConstants.CONFIG_KEY_TRUST_PACKED_REFS_STAT, - TrustPackedRefsStat.UNSET); + coreConfig = db.getConfig().get(CoreConfig.KEY); + inProcessPackedRefsLock = new ReentrantLock(true); } Repository getRepository() { @@ -245,7 +249,6 @@ public class RefDirectory extends RefDatabase { return new SnapshottingRefDirectory(this); } - /** {@inheritDoc} */ @Override public void create() throws IOException { FileUtils.mkdir(refsDir); @@ -254,7 +257,6 @@ public class RefDirectory extends RefDatabase { newLogWriter(false).create(); } - /** {@inheritDoc} */ @Override public void close() { clearReferences(); @@ -265,14 +267,39 @@ public class RefDirectory extends RefDatabase { packedRefs.set(NO_PACKED_REFS); } - /** {@inheritDoc} */ @Override public void refresh() { super.refresh(); clearReferences(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + * For a RefDirectory database, by default this packs non-symbolic, loose + * tag refs into packed-refs. If {@code all} flag is set, this packs all the + * non-symbolic, loose refs. + */ + @Override + public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs) + throws IOException { + String prefix = packRefs.isAll() ? R_REFS : R_TAGS; + Collection<Ref> refs = getRefsByPrefix(prefix); + List<String> refsToBePacked = new ArrayList<>(refs.size()); + pm.beginTask(JGitText.get().packRefs, refs.size()); + try { + for (Ref ref : refs) { + if (!ref.isSymbolic() && ref.getStorage().isLoose()) { + refsToBePacked.add(ref.getName()); + } + pm.update(1); + } + pack(refsToBePacked); + } finally { + pm.endTask(); + } + } + @Override public boolean isNameConflicting(String name) throws IOException { // Cannot be nested within an existing reference. @@ -310,7 +337,6 @@ public class RefDirectory extends RefDatabase { } } - /** {@inheritDoc} */ @Override public Ref exactRef(String name) throws IOException { try { @@ -320,7 +346,6 @@ public class RefDirectory extends RefDatabase { } } - /** {@inheritDoc} */ @Override @NonNull public Map<String, Ref> exactRef(String... refs) throws IOException { @@ -339,7 +364,6 @@ public class RefDirectory extends RefDatabase { } } - /** {@inheritDoc} */ @Override @Nullable public Ref firstExactRef(String... refs) throws IOException { @@ -357,7 +381,6 @@ public class RefDirectory extends RefDatabase { } } - /** {@inheritDoc} */ @Override public Map<String, Ref> getRefs(String prefix) throws IOException { final RefList<LooseRef> oldLoose = looseRefs.get(); @@ -397,10 +420,18 @@ public class RefDirectory extends RefDatabase { return new RefMap(prefix, packed, upcast(loose), symbolic.toRefList()); } - /** {@inheritDoc} */ + @Override + public List<Ref> getRefsByPrefix(String... prefixes) throws IOException { + return getRefsByPrefix(StringUtils.commonPrefix(prefixes)) + .parallelStream() + .filter(ref -> Stream.of(prefixes) + .anyMatch(ref.getName()::startsWith)) + .collect(Collectors.toUnmodifiableList()); + } + @Override public List<Ref> getAdditionalRefs() throws IOException { - List<Ref> ret = new LinkedList<>(); + List<Ref> ret = new ArrayList<>(); for (String name : additionalRefsNames) { Ref r = exactRef(name); if (r != null) @@ -409,6 +440,11 @@ public class RefDirectory extends RefDatabase { return ret; } + @Override + public ReflogReader getReflogReader(Ref ref) throws IOException { + return new ReflogReaderImpl(getRepository(), ref.getName()); + } + @SuppressWarnings("unchecked") private RefList<Ref> upcast(RefList<? extends Ref> loose) { return (RefList<Ref>) loose; @@ -532,7 +568,6 @@ public class RefDirectory extends RefDatabase { } } - /** {@inheritDoc} */ @Override public Ref peel(Ref ref) throws IOException { final Ref leaf = ref.getLeaf(); @@ -583,7 +618,6 @@ public class RefDirectory extends RefDatabase { fireRefsChanged(); } - /** {@inheritDoc} */ @Override public RefDirectoryUpdate newUpdate(String name, boolean detach) throws IOException { @@ -607,7 +641,6 @@ public class RefDirectory extends RefDatabase { return new RefDirectoryUpdate(this, ref); } - /** {@inheritDoc} */ @Override public RefDirectoryRename newRename(String fromName, String toName) throws IOException { @@ -620,7 +653,6 @@ public class RefDirectory extends RefDatabase { return new RefDirectoryRename(from, to); } - /** {@inheritDoc} */ @Override public PackedBatchRefUpdate newBatchUpdate() { return new PackedBatchRefUpdate(this); @@ -641,7 +673,6 @@ public class RefDirectory extends RefDatabase { return new PackedBatchRefUpdate(this, shouldLockLooseRefs); } - /** {@inheritDoc} */ @Override public boolean performsAtomicTransactions() { return true; @@ -670,42 +701,47 @@ public class RefDirectory extends RefDatabase { } String name = dst.getName(); - // Write the packed-refs file using an atomic update. We might - // wind up reading it twice, before and after the lock, to ensure - // we don't miss an edit made externally. - PackedRefList packed = getPackedRefs(); - if (packed.contains(name)) { - inProcessPackedRefsLock.lock(); + // Get and keep the packed-refs lock while updating packed-refs and + // removing any loose ref + inProcessPackedRefsLock.lock(); + try { + LockFile lck = lockPackedRefsOrThrow(); try { - LockFile lck = lockPackedRefsOrThrow(); - try { + // Write the packed-refs file using an atomic update. We might + // wind up reading it twice, before and after checking if the + // ref to delete is included or not, to ensure + // we don't rely on a PackedRefList that is a result of in-memory + // or NFS caching. + PackedRefList packed = getPackedRefs(); + if (packed.contains(name)) { + // Force update our packed-refs snapshot before writing packed = refreshPackedRefs(); int idx = packed.find(name); if (0 <= idx) { commitPackedRefs(lck, packed.remove(idx), packed, true); } - } finally { - lck.unlock(); } - } finally { - inProcessPackedRefsLock.unlock(); - } - } - RefList<LooseRef> curLoose, newLoose; - do { - curLoose = looseRefs.get(); - int idx = curLoose.find(name); - if (idx < 0) - break; - newLoose = curLoose.remove(idx); - } while (!looseRefs.compareAndSet(curLoose, newLoose)); + RefList<LooseRef> curLoose, newLoose; + do { + curLoose = looseRefs.get(); + int idx = curLoose.find(name); + if (idx < 0) { + break; + } + newLoose = curLoose.remove(idx); + } while (!looseRefs.compareAndSet(curLoose, newLoose)); - int levels = levelsIn(name) - 2; - delete(logFor(name), levels); - if (dst.getStorage().isLoose()) { - update.unlock(); - delete(fileFor(name), levels); + int levels = levelsIn(name) - 2; + delete(logFor(name), levels); + if (dst.getStorage().isLoose()) { + deleteAndUnlock(fileFor(name), levels, update); + } + } finally { + lck.unlock(); + } + } finally { + inProcessPackedRefsLock.unlock(); } modCnt.incrementAndGet(); @@ -722,6 +758,7 @@ public class RefDirectory extends RefDatabase { * @param refs * the refs to be added. Must be fully qualified. * @throws java.io.IOException + * if an IO error occurred */ public void pack(List<String> refs) throws IOException { pack(refs, Collections.emptyMap()); @@ -811,7 +848,7 @@ public class RefDirectory extends RefDatabase { } Ref packedRef = newPacked.get(refName); ObjectId clr_oid = currentLooseRef.getObjectId(); - if (clr_oid != null + if (clr_oid != null && packedRef != null && clr_oid.equals(packedRef.getObjectId())) { RefList<LooseRef> curLoose, newLoose; do { @@ -823,7 +860,7 @@ public class RefDirectory extends RefDatabase { newLoose = curLoose.remove(idx); } while (!looseRefs.compareAndSet(curLoose, newLoose)); int levels = levelsIn(refName) - 2; - delete(refFile, levels, rLck); + deleteAndUnlock(refFile, levels, rLck); } } finally { if (shouldUnlock) { @@ -866,26 +903,29 @@ public class RefDirectory extends RefDatabase { * has this attributes simply return it. Otherwise create a new peeled * {@link ObjectIdRef} where Storage is set to PACKED. * - * @param f + * @param ref + * given ref * @return a ref for Storage PACKED having the same name, id, peeledId as f * @throws MissingObjectException + * if an object is missing * @throws IOException + * if an IO error occurred */ - private Ref peeledPackedRef(Ref f) + private Ref peeledPackedRef(Ref ref) throws MissingObjectException, IOException { - if (f.getStorage().isPacked() && f.isPeeled()) { - return f; + if (ref.getStorage().isPacked() && ref.isPeeled()) { + return ref; } - if (!f.isPeeled()) { - f = peel(f); + if (!ref.isPeeled()) { + ref = peel(ref); } - ObjectId peeledObjectId = f.getPeeledObjectId(); + ObjectId peeledObjectId = ref.getPeeledObjectId(); if (peeledObjectId != null) { - return new ObjectIdRef.PeeledTag(PACKED, f.getName(), - f.getObjectId(), peeledObjectId); + return new ObjectIdRef.PeeledTag(PACKED, ref.getName(), + ref.getObjectId(), peeledObjectId); } - return new ObjectIdRef.PeeledNonTag(PACKED, f.getName(), - f.getObjectId()); + return new ObjectIdRef.PeeledNonTag(PACKED, ref.getName(), + ref.getObjectId()); } void log(boolean force, RefUpdate update, String msg, boolean deref) @@ -928,7 +968,7 @@ public class RefDirectory extends RefDatabase { PackedRefList getPackedRefs() throws IOException { final PackedRefList curList = packedRefs.get(); - switch (trustPackedRefsStat) { + switch (coreConfig.getTrustPackedRefsStat()) { case NEVER: break; case AFTER_OPEN: @@ -944,12 +984,8 @@ public class RefDirectory extends RefDatabase { return curList; } break; - case UNSET: - if (trustFolderStat - && !curList.snapshot.isModified(packedRefsFile)) { - return curList; - } - break; + case INHERIT: + // only used in CoreConfig internally } return refreshPackedRefs(curList); @@ -1134,6 +1170,11 @@ public class RefDirectory extends RefDatabase { LooseRef scanRef(LooseRef ref, String name) throws IOException { final File path = fileFor(name); + + if (coreConfig.getTrustLooseRefStat() == TrustStat.AFTER_OPEN) { + refreshPathToLooseRef(Paths.get(name)); + } + FileSnapshot currentSnapshot = null; if (ref != null) { @@ -1219,6 +1260,29 @@ public class RefDirectory extends RefDatabase { return new LooseUnpeeled(loose.snapshot, name, id); } + /** + * Workaround for issues caused by NFS caching. Refresh directories starting + * from the repository root to a loose ref by opening an input stream. This + * refreshes file attributes of the loose ref (at least on some NFS + * clients). + * + * @param refPath + * path of a loose ref relative to the repository root + */ + void refreshPathToLooseRef(Path refPath) { + for (int i = 1; i < refPath.getNameCount(); i++) { + File dir = fileFor(refPath.subpath(0, i).toString()); + // Use Files.newInputStream(Path) as it is consistent with other + // code where a refresh is being done (see getPackedRefs()) and also + // it performs slightly better than Files.newDirectoryStream(Path) + try (InputStream stream = Files.newInputStream(dir.toPath())) { + // open the dir to refresh attributes (on some NFS clients) + } catch (IOException e) { + break; // loose ref may not exist + } + } + } + private static boolean isSymRef(byte[] buf, int n) { if (n < 6) return false; @@ -1234,6 +1298,7 @@ public class RefDirectory extends RefDatabase { * * @return {@code true} if we are currently cloning a repository * @throws IOException + * if an IO error occurred */ boolean isInClone() throws IOException { return hasDanglingHead() && !packedRefsFile.exists() && !hasLooseRef(); @@ -1289,7 +1354,12 @@ public class RefDirectory extends RefDatabase { name = name.substring(R_REFS.length()); return new File(refsDir, name); } - return new File(gitDir, name); + // HEAD needs to get resolved from git dir as resolving it from common dir + // would always lead back to current default branch + if (name.equals(HEAD)) { + return new File(gitDir, name); + } + return new File(gitCommonDir, name); } static int levelsIn(String name) { @@ -1300,19 +1370,37 @@ public class RefDirectory extends RefDatabase { } static void delete(File file, int depth) throws IOException { - delete(file, depth, null); + delete(file); + deleteEmptyParentDirs(file, depth); } - private static void delete(File file, int depth, LockFile rLck) - throws IOException { + private static void delete(File file) throws IOException { if (!file.delete() && file.isFile()) { - throw new IOException(MessageFormat.format( - JGitText.get().fileCannotBeDeleted, file)); + throw new IOException( + MessageFormat.format(JGitText.get().fileCannotBeDeleted, + file)); + } + } + + private static void deleteAndUnlock(File file, int depth, + RefDirectoryUpdate refUpdate) throws IOException { + delete(file); + if (refUpdate != null) { + refUpdate.unlock(); // otherwise cannot delete parent directories emptied by the update } + deleteEmptyParentDirs(file, depth); + } + private static void deleteAndUnlock(File file, int depth, LockFile rLck) + throws IOException { + delete(file); if (rLck != null) { - rLck.unlock(); // otherwise cannot delete dir below + rLck.unlock(); // otherwise cannot delete parent directories of the lock file } + deleteEmptyParentDirs(file, depth); + } + + private static void deleteEmptyParentDirs(File file, int depth) { File dir = file.getParentFile(); for (int i = 0; i < depth; ++i) { try { @@ -1420,10 +1508,10 @@ public class RefDirectory extends RefDatabase { implements LooseRef { private final FileSnapshot snapShot; - LoosePeeledTag(FileSnapshot snapshot, @NonNull String refName, + LoosePeeledTag(FileSnapshot snapShot, @NonNull String refName, @NonNull ObjectId id, @NonNull ObjectId p) { super(LOOSE, refName, id, p); - this.snapShot = snapshot; + this.snapShot = snapShot; } @Override @@ -1441,10 +1529,10 @@ public class RefDirectory extends RefDatabase { implements LooseRef { private final FileSnapshot snapShot; - LooseNonTag(FileSnapshot snapshot, @NonNull String refName, + LooseNonTag(FileSnapshot snapShot, @NonNull String refName, @NonNull ObjectId id) { super(LOOSE, refName, id); - this.snapShot = snapshot; + this.snapShot = snapShot; } @Override @@ -1497,10 +1585,10 @@ public class RefDirectory extends RefDatabase { LooseRef { private final FileSnapshot snapShot; - LooseSymbolicRef(FileSnapshot snapshot, @NonNull String refName, + LooseSymbolicRef(FileSnapshot snapShot, @NonNull String refName, @NonNull Ref target) { super(refName, target); - this.snapShot = snapshot; + this.snapShot = snapShot; } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java index d07299e45a..1c5c48a296 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java @@ -68,7 +68,6 @@ class RefDirectoryRename extends RefRename { return refdb; } - /** {@inheritDoc} */ @Override protected Result doRename() throws IOException { if (source.getRef().isSymbolic()) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java index 0dcb3196c5..436957bb24 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java @@ -32,19 +32,16 @@ class RefDirectoryUpdate extends RefUpdate { database = r; } - /** {@inheritDoc} */ @Override protected RefDirectory getRefDatabase() { return database; } - /** {@inheritDoc} */ @Override protected Repository getRepository() { return database.getRepository(); } - /** {@inheritDoc} */ @Override protected boolean tryLock(boolean deref) throws IOException { shouldDeref = deref; @@ -54,6 +51,7 @@ class RefDirectoryUpdate extends RefUpdate { String name = dst.getName(); lock = new LockFile(database.fileFor(name)); if (lock.lock()) { + doAfterLocking(name); dst = database.findRef(name); setOldObjectId(dst != null ? dst.getObjectId() : null); return true; @@ -61,7 +59,6 @@ class RefDirectoryUpdate extends RefUpdate { return false; } - /** {@inheritDoc} */ @Override protected void unlock() { if (lock != null) { @@ -70,7 +67,6 @@ class RefDirectoryUpdate extends RefUpdate { } } - /** {@inheritDoc} */ @Override protected Result doUpdate(Result status) throws IOException { WriteConfig wc = database.getRepository().getConfig() @@ -112,7 +108,6 @@ class RefDirectoryUpdate extends RefUpdate { } } - /** {@inheritDoc} */ @Override protected Result doDelete(Result status) throws IOException { if (getRef().getStorage() != Ref.Storage.NEW) @@ -120,7 +115,6 @@ class RefDirectoryUpdate extends RefUpdate { return status; } - /** {@inheritDoc} */ @Override protected Result doLink(String target) throws IOException { WriteConfig wc = database.getRepository().getConfig() @@ -141,4 +135,14 @@ class RefDirectoryUpdate extends RefUpdate { return Result.NEW; return Result.FORCED; } + + /** + * Do any actions needed immediately after a lock on the ref is acquired + * + * @param name + * the name of the reference. + */ + protected void doAfterLocking(String name) { + // No actions by default + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java index cb80043b7e..6870d7686e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java @@ -60,7 +60,6 @@ public class ReflogEntryImpl implements Serializable, ReflogEntry { /* (non-Javadoc) * @see org.eclipse.jgit.internal.storage.file.ReflogEntry#getOldId() */ - /** {@inheritDoc} */ @Override public ObjectId getOldId() { return oldId; @@ -69,7 +68,6 @@ public class ReflogEntryImpl implements Serializable, ReflogEntry { /* (non-Javadoc) * @see org.eclipse.jgit.internal.storage.file.ReflogEntry#getNewId() */ - /** {@inheritDoc} */ @Override public ObjectId getNewId() { return newId; @@ -78,7 +76,6 @@ public class ReflogEntryImpl implements Serializable, ReflogEntry { /* (non-Javadoc) * @see org.eclipse.jgit.internal.storage.file.ReflogEntry#getWho() */ - /** {@inheritDoc} */ @Override public PersonIdent getWho() { return who; @@ -87,13 +84,11 @@ public class ReflogEntryImpl implements Serializable, ReflogEntry { /* (non-Javadoc) * @see org.eclipse.jgit.internal.storage.file.ReflogEntry#getComment() */ - /** {@inheritDoc} */ @Override public String getComment() { return comment; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { @@ -104,7 +99,6 @@ public class ReflogEntryImpl implements Serializable, ReflogEntry { /* (non-Javadoc) * @see org.eclipse.jgit.internal.storage.file.ReflogEntry#parseCheckout() */ - /** {@inheritDoc} */ @Override public CheckoutEntry parseCheckout() { if (getComment().startsWith(CheckoutEntryImpl.CHECKOUT_MOVING_FROM)) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogReaderImpl.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogReaderImpl.java index 99a9e0938e..f1888eb90f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogReaderImpl.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogReaderImpl.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.lib.Constants.HEAD; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -32,16 +34,19 @@ class ReflogReaderImpl implements ReflogReader { /** * @param db + * repository to read reflogs from * @param refname + * {@code Ref} name */ ReflogReaderImpl(Repository db, String refname) { - logName = new File(db.getDirectory(), Constants.LOGS + '/' + refname); + File logBaseDir = refname.equals(HEAD) ? db.getDirectory() + : db.getCommonDirectory(); + logName = new File(logBaseDir, Constants.L_LOGS + refname); } /* (non-Javadoc) * @see org.eclipse.jgit.internal.storage.file.ReflogReaader#getLastEntry() */ - /** {@inheritDoc} */ @Override public ReflogEntry getLastEntry() throws IOException { return getReverseEntry(0); @@ -50,7 +55,6 @@ class ReflogReaderImpl implements ReflogReader { /* (non-Javadoc) * @see org.eclipse.jgit.internal.storage.file.ReflogReaader#getReverseEntries() */ - /** {@inheritDoc} */ @Override public List<ReflogEntry> getReverseEntries() throws IOException { return getReverseEntries(Integer.MAX_VALUE); @@ -59,7 +63,6 @@ class ReflogReaderImpl implements ReflogReader { /* (non-Javadoc) * @see org.eclipse.jgit.internal.storage.file.ReflogReaader#getReverseEntry(int) */ - /** {@inheritDoc} */ @Override public ReflogEntry getReverseEntry(int number) throws IOException { if (number < 0) @@ -89,7 +92,6 @@ class ReflogReaderImpl implements ReflogReader { /* (non-Javadoc) * @see org.eclipse.jgit.internal.storage.file.ReflogReaader#getReverseEntries(int) */ - /** {@inheritDoc} */ @Override public List<ReflogEntry> getReverseEntries(int max) throws IOException { final byte[] log; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java index 7032083a4d..b1ceb14809 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java @@ -91,6 +91,7 @@ public class ReflogWriter { * Create the log directories. * * @throws java.io.IOException + * if an IO error occurred * @return this writer. */ public ReflogWriter create() throws IOException { @@ -110,6 +111,7 @@ public class ReflogWriter { * a {@link org.eclipse.jgit.lib.ReflogEntry} object. * @return this writer * @throws java.io.IOException + * if an IO error occurred */ public ReflogWriter log(String refName, ReflogEntry entry) throws IOException { @@ -132,6 +134,7 @@ public class ReflogWriter { * reflog message * @return this writer * @throws java.io.IOException + * if an IO error occurred */ public ReflogWriter log(String refName, ObjectId oldId, ObjectId newId, PersonIdent ident, String message) throws IOException { @@ -150,6 +153,7 @@ public class ReflogWriter { * whether to dereference symbolic refs * @return this writer * @throws java.io.IOException + * if an IO error occurred */ public ReflogWriter log(RefUpdate update, String msg, boolean deref) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataInput.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataInput.java index 6a80519d0b..7a564cc6ab 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataInput.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataInput.java @@ -31,14 +31,12 @@ class SimpleDataInput implements DataInput { this.fd = fd; } - /** {@inheritDoc} */ @Override public int readInt() throws IOException { readFully(buf, 0, 4); return NB.decodeInt32(buf, 0); } - /** {@inheritDoc} */ @Override public long readLong() throws IOException { readFully(buf, 0, 8); @@ -57,79 +55,66 @@ class SimpleDataInput implements DataInput { return NB.decodeUInt32(buf, 0); } - /** {@inheritDoc} */ @Override public void readFully(byte[] b) throws IOException { readFully(b, 0, b.length); } - /** {@inheritDoc} */ @Override public void readFully(byte[] b, int off, int len) throws IOException { IO.readFully(fd, b, off, len); } - /** {@inheritDoc} */ @Override public int skipBytes(int n) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public boolean readBoolean() throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public byte readByte() throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public int readUnsignedByte() throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public short readShort() throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public int readUnsignedShort() throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public char readChar() throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public float readFloat() throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public double readDouble() throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public String readLine() throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public String readUTF() throws IOException { throw new UnsupportedOperationException(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataOutput.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataOutput.java index d01f6b4bda..74840889ce 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataOutput.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataOutput.java @@ -31,88 +31,74 @@ class SimpleDataOutput implements DataOutput { this.fd = fd; } - /** {@inheritDoc} */ @Override public void writeShort(int v) throws IOException { NB.encodeInt16(buf, 0, v); fd.write(buf, 0, 2); } - /** {@inheritDoc} */ @Override public void writeInt(int v) throws IOException { NB.encodeInt32(buf, 0, v); fd.write(buf, 0, 4); } - /** {@inheritDoc} */ @Override public void writeLong(long v) throws IOException { NB.encodeInt64(buf, 0, v); fd.write(buf, 0, 8); } - /** {@inheritDoc} */ @Override public void write(int b) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void write(byte[] b) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void write(byte[] b, int off, int len) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void writeBoolean(boolean v) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void writeByte(int v) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void writeChar(int v) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void writeFloat(float v) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void writeDouble(double v) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void writeBytes(String s) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void writeChars(String s) throws IOException { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public void writeUTF(String s) throws IOException { throw new UnsupportedOperationException(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectory.java index 0b9748096e..1dc5776e06 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectory.java @@ -13,19 +13,24 @@ package org.eclipse.jgit.internal.storage.file; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.revwalk.RevWalk; +import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** * Snapshotting write-through cache of a {@link RefDirectory}. * <p> * This is intended to be short-term write-through snapshot based cache used in - * a request scope to avoid re-reading packed-refs on each read. A future - * improvement could also snapshot loose refs. + * a request scope to avoid re-reading packed-refs on each read and to avoid + * refreshing paths to a loose ref that has already been refreshed. * <p> * Only use this class when concurrent writes from other requests (not using the * same instance of SnapshottingRefDirectory) generally need not be visible to @@ -35,6 +40,7 @@ import java.util.List; */ class SnapshottingRefDirectory extends RefDirectory { final RefDirectory refDb; + private final Set<File> refreshedLooseRefDirs = ConcurrentHashMap.newKeySet(); private volatile boolean isValid; @@ -67,14 +73,28 @@ class SnapshottingRefDirectory extends RefDirectory { return packedRefs.get(); } - /** {@inheritDoc} */ + @Override + void refreshPathToLooseRef(Path refPath) { + for (int i = 1; i < refPath.getNameCount(); i++) { + File dir = fileFor(refPath.subpath(0, i).toString()); + if (!refreshedLooseRefDirs.contains(dir)) { + try (InputStream stream = Files.newInputStream(dir.toPath())) { + // open the dir to refresh attributes (on some NFS clients) + } catch (IOException e) { + break; // loose ref may not exist + } finally { + refreshedLooseRefDirs.add(dir); + } + } + } + } + @Override void delete(RefDirectoryUpdate update) throws IOException { refreshSnapshot(); super.delete(update); } - /** {@inheritDoc} */ @Override public RefDirectoryUpdate newUpdate(String name, boolean detach) throws IOException { @@ -82,19 +102,16 @@ class SnapshottingRefDirectory extends RefDirectory { return super.newUpdate(name, detach); } - /** {@inheritDoc} */ @Override public PackedBatchRefUpdate newBatchUpdate() { return new SnapshotPackedBatchRefUpdate(this); } - /** {@inheritDoc} */ @Override public PackedBatchRefUpdate newBatchUpdate(boolean shouldLockLooseRefs) { return new SnapshotPackedBatchRefUpdate(this, shouldLockLooseRefs); } - /** {@inheritDoc} */ @Override RefDirectoryUpdate newTemporaryUpdate() throws IOException { refreshSnapshot(); @@ -113,6 +130,7 @@ class SnapshottingRefDirectory extends RefDirectory { } synchronized void invalidateSnapshot() { + refreshedLooseRefDirs.clear(); isValid = false; } @@ -126,6 +144,7 @@ class SnapshottingRefDirectory extends RefDirectory { * threads use this snapshot. * * @throws IOException + * if an IO error occurred */ private synchronized void refreshSnapshot() throws IOException { compareAndSetPackedRefs(packedRefs.get(), refDb.getPackedRefs()); @@ -148,29 +167,29 @@ class SnapshottingRefDirectory extends RefDirectory { } private static <T> T invalidateSnapshotOnError( - SupplierThrowsException<T, IOException> f, RefDatabase refDb) + SupplierThrowsException<T, IOException> f, SnapshottingRefDirectory refDb) throws IOException { return invalidateSnapshotOnError(a -> f.call(), null, refDb); } private static <A, R> R invalidateSnapshotOnError( FunctionThrowsException<A, R, IOException> f, A a, - RefDatabase refDb) throws IOException { + SnapshottingRefDirectory refDb) throws IOException { try { return f.apply(a); } catch (IOException e) { - ((SnapshottingRefDirectory) refDb).invalidateSnapshot(); + refDb.invalidateSnapshot(); throw e; } } private static <A1, A2, A3> void invalidateSnapshotOnError( TriConsumerThrowsException<A1, A2, A3, IOException> f, A1 a1, A2 a2, - A3 a3, RefDatabase refDb) throws IOException { + A3 a3, SnapshottingRefDirectory refDb) throws IOException { try { f.accept(a1, a2, a3); } catch (IOException e) { - ((SnapshottingRefDirectory) refDb).invalidateSnapshot(); + refDb.invalidateSnapshot(); throw e; } } @@ -182,39 +201,57 @@ class SnapshottingRefDirectory extends RefDirectory { @Override public Result forceUpdate() throws IOException { - return invalidateSnapshotOnError(() -> super.forceUpdate(), + return invalidateSnapshotOnError(super::forceUpdate, getRefDatabase()); } @Override public Result update() throws IOException { - return invalidateSnapshotOnError(() -> super.update(), - getRefDatabase()); + return invalidateSnapshotOnError(super::update, getRefDatabase()); } @Override public Result update(RevWalk walk) throws IOException { - return invalidateSnapshotOnError(rw -> super.update(rw), walk, + return invalidateSnapshotOnError(super::update, walk, getRefDatabase()); } @Override public Result delete() throws IOException { - return invalidateSnapshotOnError(() -> super.delete(), - getRefDatabase()); + return invalidateSnapshotOnError(super::delete, getRefDatabase()); } @Override public Result delete(RevWalk walk) throws IOException { - return invalidateSnapshotOnError(rw -> super.delete(rw), walk, + return invalidateSnapshotOnError(super::delete, walk, getRefDatabase()); } @Override public Result link(String target) throws IOException { - return invalidateSnapshotOnError(t -> super.link(t), target, + return invalidateSnapshotOnError(super::link, target, getRefDatabase()); } + + /** + * Invalidate the SnapshottingRefDirectory snapshot after locking the + * ref. + * <p> + * Doing this after locking the ref ensures that the upcoming write is + * not based on a cached value. + * + * @param name + * the name of the reference. + */ + @Override + protected void doAfterLocking(String name) { + getRefDatabase().invalidateSnapshot(); + } + + @Override + public SnapshottingRefDirectory getRefDatabase() { + return (SnapshottingRefDirectory) super.getRefDatabase(); + } } private static class SnapshotRefDirectoryRename extends RefDirectoryRename { @@ -225,26 +262,30 @@ class SnapshottingRefDirectory extends RefDirectory { @Override public RefUpdate.Result rename() throws IOException { - return invalidateSnapshotOnError(() -> super.rename(), - getRefDirectory()); + return invalidateSnapshotOnError(super::rename, getRefDirectory()); + } + + @Override + public SnapshottingRefDirectory getRefDirectory() { + return (SnapshottingRefDirectory) super.getRefDirectory(); } } private static class SnapshotPackedBatchRefUpdate extends PackedBatchRefUpdate { - SnapshotPackedBatchRefUpdate(RefDirectory refdb) { - super(refdb); + SnapshotPackedBatchRefUpdate(RefDirectory refDb) { + super(refDb); } - SnapshotPackedBatchRefUpdate(RefDirectory refdb, + SnapshotPackedBatchRefUpdate(RefDirectory refDb, boolean shouldLockLooseRefs) { - super(refdb, shouldLockLooseRefs); + super(refDb, shouldLockLooseRefs); } @Override public void execute(RevWalk walk, ProgressMonitor monitor, List<String> options) throws IOException { - invalidateSnapshotOnError((rw, m, o) -> super.execute(rw, m, o), + invalidateSnapshotOnError(super::execute, walk, monitor, options, getRefDatabase()); } @@ -254,5 +295,10 @@ class SnapshottingRefDirectory extends RefDirectory { invalidateSnapshotOnError((rw, m, a3) -> super.execute(rw, m), walk, monitor, null, getRefDatabase()); } + + @Override + public SnapshottingRefDirectory getRefDatabase() { + return (SnapshottingRefDirectory) super.getRefDatabase(); + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java index 25653b3ce3..15c125c684 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java @@ -15,8 +15,10 @@ import java.io.IOException; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.Collections; +import java.util.HashSet; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -373,6 +375,8 @@ public class WindowCache { } /** + * Get singleton instance + * * @return the cached instance. */ public static WindowCache getInstance() { @@ -395,7 +399,11 @@ public class WindowCache { } static final void purge(Pack pack) { - cache.removeAll(pack); + purge(Collections.singleton(pack)); + } + + static final void purge(Set<Pack> packs) { + cache.queueRemoveAll(packs); } /** cleanup released and/or garbage collected windows. */ @@ -435,7 +443,32 @@ public class WindowCache { private final AtomicBoolean publishMBean = new AtomicBoolean(); - private boolean useStrongRefs; + private final boolean useStrongRefs; + + private final boolean useStrongIndexRefs; + + /** Removers are purely CPU/mem bound (no I/O), so likely should not go above # CPUs */ + private final int idealNumRemovers; + + /** Number of blocks to split the Pack removal into. + * + * Consolidation is better with more blocks since it increases + * the wait before moving to the next set by allowing more work + * to accumulate in the next set. On the flip side, the more + * blocks, the more synchronization overhead increasing each + * removers latency. + */ + private final int numRemovalBlocks; + + private final int removalBlockSize; + + private Set<Pack> packsToRemove = new HashSet<>(); + + private Set<Pack> packsBeingRemoved; + + private int numRemovers; + + private int blockBeingRemoved; private WindowCache(WindowCacheConfig cfg) { tableSize = tableSize(cfg); @@ -467,6 +500,7 @@ public class WindowCache { windowSizeShift = bits(cfg.getPackedGitWindowSize()); windowSize = 1 << windowSizeShift; useStrongRefs = cfg.isPackedGitUseStrongRefs(); + useStrongIndexRefs = cfg.isPackedIndexGitUseStrongRefs(); queue = useStrongRefs ? new StrongCleanupQueue(this) : new SoftCleanupQueue(this); @@ -474,6 +508,23 @@ public class WindowCache { statsRecorder = mbean; publishMBean.set(cfg.getExposeStatsViaJmx()); + /* Since each worker will only process up to one full set of blocks, at least 2 + * workers are needed anytime there are queued removals to ensure that all the + * blocks will get processed. However, if workers are maxed out at only 2, then + * enough newer workers will never start in order to make it safe for older + * workers to quit early. At least 3 workers are needed to make older worker + * relief transitions possible. + */ + idealNumRemovers = Math.max(3, Runtime.getRuntime().availableProcessors()); + + int bs = 1024; + if (tableSize < 2 * bs) { + bs = tableSize / 2; + } + removalBlockSize = bs; + numRemovalBlocks = tableSize / removalBlockSize; + blockBeingRemoved = numRemovalBlocks - 1; + if (maxFiles < 1) throw new IllegalArgumentException(JGitText.get().openFilesMustBeAtLeast1); if (maxBytes < windowSize) @@ -488,6 +539,8 @@ public class WindowCache { } /** + * Get cache statistics + * * @return cache statistics for the WindowCache */ public WindowCacheStats getStats() { @@ -701,22 +754,105 @@ public class WindowCache { } /** - * Clear all entries related to a single file. + * Asynchronously clear all entries related to files. * <p> - * Typically this method is invoked during {@link Pack#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. + * Typically this method is invoked during {@link Pack#close()}, or + * {@link Pack#close(Set)}, when we know the packs are never going to be + * useful to us again (for example, they no longer exist on disk after gc). + * A concurrent reader loading an entry from these same packs may cause a + * pack to become stuck in the cache anyway. * - * @param pack - * the file to purge all entries of. + * Work on clearing files will be split up into blocks so that removing + * can be shared by more than one thread. This potential work sharing + * can provide 2 optimizations for removals: + * <ol> + * <li> It provides an opportunity for separate removal requests to be + * consolidated into one removal pass.</li> + * <li> It can reduce removing thread latencies by sharing the removal work + * with other removing threads which otherwise might not have any work to do + * due to their removal request being consolidated. This makes the system + * more efficient and can actually reduce latencies as system load increases + * due to pack removals!</li> + * </ol> + * The optimizations above are all achieved without blockng threads to wait + * for other threads to complete (although naturally there are some + * synchronization points), and while ensuring that no threads do more work + * than if they were the only thread available to perform a removal. + * + * @param packs + * the files to purge all entries of */ - private void removeAll(Pack pack) { - for (int s = 0; s < tableSize; s++) { + private void queueRemoveAll(Set<Pack> packs) { + synchronized (this) { + packsToRemove.addAll(packs); + if (numRemovers >= idealNumRemovers) { + return; + } + numRemovers++; + } + for (int numRemoved = 0; removeNextBlock(numRemoved); numRemoved++) { + // empty + } + synchronized (this) { + if (numRemovers > 0) { + numRemovers--; + } + } + } + + /** Determine which block to remove next, if any, and do so. + * + * @param numRemoved + * the number of already processed block removals by the current thread + * @return whether more processing should be done by the current thread + */ + private boolean removeNextBlock(int numRemoved) { + Set<Pack> toRemove; + int block; + synchronized (this) { + if (packsBeingRemoved == null || blockBeingRemoved >= numRemovalBlocks - 1) { + if (packsToRemove.isEmpty()) { + return false; + } + + blockBeingRemoved = 0; + packsBeingRemoved = packsToRemove; + packsToRemove = new HashSet<>(); + } else { + blockBeingRemoved++; + } + + toRemove = packsBeingRemoved; + block = blockBeingRemoved; + } + + removeBlock(toRemove, block); + numRemoved++; + + /* Ensure threads never work on more than a full set of blocks (the equivalent + * of removing one Pack) */ + boolean isLast = numRemoved >= numRemovalBlocks; + synchronized (this) { + if (numRemovers >= idealNumRemovers) { + isLast = true; + } + } + return !isLast; + } + + /** Remove a block of entries for a Set of files + * @param packs + * the files to purge all entries of + * @param block + * the specific block to process removals for + */ + private void removeBlock(Set<Pack> packs, int block) { + int starting = block * removalBlockSize; + for (int s = starting; s < starting + removalBlockSize && s < tableSize; s++) { final Entry e1 = table.get(s); boolean hasDead = false; for (Entry e = e1; e != null; e = e.next) { - if (e.ref.getPack() == pack) { + if (packs.contains(e.ref.getPack())) { e.kill(); hasDead = true; } else if (e.dead) @@ -751,6 +887,10 @@ public class WindowCache { return n == top.next ? top : new Entry(n, top.ref); } + boolean isPackedIndexGitUseStrongRefs() { + return useStrongIndexRefs; + } + private static class Entry { /** Next entry in the hash table's chain list. */ final Entry next; @@ -841,6 +981,7 @@ public class WindowCache { * Whether this is a strong reference. * @return {@code true} if this is a strong reference */ + @SuppressWarnings("unused") boolean isStrongRef(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java index fa743babe7..33459ef18d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java @@ -36,6 +36,7 @@ import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.BitmapIndex; import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder; import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.InflaterCache; import org.eclipse.jgit.lib.ObjectId; @@ -49,12 +50,16 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { /** Temporary buffer large enough for at least one raw object id. */ final byte[] tempId = new byte[Constants.OBJECT_ID_LENGTH]; + private final boolean useObjectSizeIndex; + private Inflater inf; private ByteWindow window; private DeltaBaseCache baseCache; + private Pack lastPack; + @Nullable private final ObjectInserter createdFromInserter; @@ -64,6 +69,10 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { this.db = db; this.createdFromInserter = null; this.streamFileThreshold = WindowCache.getStreamFileThreshold(); + this.useObjectSizeIndex = db == null ? false + : db.getConfig().getBoolean( + ConfigConstants.CONFIG_PACK_SECTION, + ConfigConstants.CONFIG_KEY_USE_OBJECT_SIZE_INDEX, false); } WindowCursor(FileObjectDatabase db, @@ -71,6 +80,10 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { this.db = db; this.createdFromInserter = createdFromInserter; this.streamFileThreshold = WindowCache.getStreamFileThreshold(); + this.useObjectSizeIndex = db == null ? false + : db.getConfig().getBoolean( + ConfigConstants.CONFIG_PACK_SECTION, + ConfigConstants.CONFIG_KEY_USE_OBJECT_SIZE_INDEX, false); } DeltaBaseCache getDeltaBaseCache() { @@ -79,13 +92,11 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { return baseCache; } - /** {@inheritDoc} */ @Override public ObjectReader newReader() { return new WindowCursor(db); } - /** {@inheritDoc} */ @Override public BitmapIndex getBitmapIndex() throws IOException { for (Pack pack : db.getPacks()) { @@ -96,13 +107,11 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { return null; } - /** {@inheritDoc} */ @Override public Optional<CommitGraph> getCommitGraph() { return db.getCommitGraph(); } - /** {@inheritDoc} */ @Override public Collection<CachedPack> getCachedPacksAndUpdate( BitmapBuilder needBitmap) throws IOException { @@ -115,7 +124,6 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { return Collections.emptyList(); } - /** {@inheritDoc} */ @Override public Collection<ObjectId> resolve(AbbreviatedObjectId id) throws IOException { @@ -126,13 +134,11 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { return matches; } - /** {@inheritDoc} */ @Override public boolean has(AnyObjectId objectId) throws IOException { return db.has(objectId); } - /** {@inheritDoc} */ @Override public ObjectLoader open(AnyObjectId objectId, int typeHint) throws MissingObjectException, IncorrectObjectTypeException, @@ -149,15 +155,12 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { return ldr; } - /** {@inheritDoc} */ @Override public Set<ObjectId> getShallowCommits() throws IOException { return db.getShallowCommits(); } - /** {@inheritDoc} */ - @Override - public long getObjectSize(AnyObjectId objectId, int typeHint) + private long getObjectSizeStorage(AnyObjectId objectId, int typeHint) throws MissingObjectException, IncorrectObjectTypeException, IOException { long sz = db.getObjectSize(this, objectId); @@ -170,13 +173,68 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { return sz; } - /** {@inheritDoc} */ + @Override + public long getObjectSize(AnyObjectId objectId, int typeHint) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + // Async queue uses hint OBJ_ANY + if (typeHint != Constants.OBJ_BLOB) { + return getObjectSizeStorage(objectId, typeHint); + } + + Pack pack = findPack(objectId); + if (pack == null) { + // Non-packed object (e.g. loose or in alternates) + return getObjectSizeStorage(objectId, typeHint); + } + + if (useObjectSizeIndex && pack.hasObjectSizeIndex()) { + long sz = pack.getIndexedObjectSize(objectId); + if (sz >= 0) { + return sz; + } + } + return getObjectSizeStorage(objectId, typeHint); + } + + @Override + public boolean isNotLargerThan(AnyObjectId objectId, int typeHint, + long size) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + if (typeHint != Constants.OBJ_BLOB) { + return getObjectSizeStorage(objectId, typeHint) <= size; + } + + Pack pack = findPack(objectId); + if (pack == null || !pack.hasObjectSizeIndex()) { + // Non-packed object (e.g. loose or in alternates) + return getObjectSizeStorage(objectId, typeHint) <= size; + } + + return pack.getIndexedObjectSize(objectId) <= size; + } + + private Pack findPack(AnyObjectId objectId) throws IOException { + if (lastPack != null && lastPack.hasObject(objectId)) { + return lastPack; + } + + for (Pack p : db.getPacks()) { + if (p.hasObject(objectId)) { + lastPack = p; + return p; + } + } + + return null; + } + @Override public LocalObjectToPack newObjectToPack(AnyObjectId objectId, int type) { return new LocalObjectToPack(objectId, type); } - /** {@inheritDoc} */ @Override public void selectObjectRepresentation(PackWriter packer, ProgressMonitor monitor, Iterable<ObjectToPack> objects) @@ -187,7 +245,6 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { } } - /** {@inheritDoc} */ @Override public void copyObjectAsIs(PackOutputStream out, ObjectToPack otp, boolean validate) throws IOException, @@ -196,7 +253,6 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { src.pack.copyAsIs(out, src, validate, this); } - /** {@inheritDoc} */ @Override public void writeObjects(PackOutputStream out, List<ObjectToPack> list) throws IOException { @@ -218,7 +274,7 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { * @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>. + * <code>position</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. @@ -240,7 +296,6 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { return cnt - need; } - /** {@inheritDoc} */ @Override public void copyPackAsIs(PackOutputStream out, CachedPack pack) throws IOException { @@ -336,7 +391,6 @@ final class WindowCursor extends ObjectReader implements ObjectReuseAsIs { } } - /** {@inheritDoc} */ @Override @Nullable public ObjectInserter getCreatedFromInserter() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/BlockSource.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/BlockSource.java index 82d07e6bde..58695fc9ab 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/BlockSource.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/BlockSource.java @@ -147,7 +147,6 @@ public abstract class BlockSource implements AutoCloseable { // Do nothing by default. } - /** {@inheritDoc} */ @Override public abstract void close(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStream.java index ca2095feec..ce86eabe90 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStream.java @@ -80,7 +80,6 @@ public class CancellableDigestOutputStream extends OutputStream { return count; } - /** {@inheritDoc} */ @Override public final void write(int b) throws IOException { if (checkCancelAt <= count) { @@ -95,7 +94,6 @@ public class CancellableDigestOutputStream extends OutputStream { count++; } - /** {@inheritDoc} */ @Override public final void write(byte[] b, int off, int len) throws IOException { while (0 < len) { @@ -116,7 +114,6 @@ public class CancellableDigestOutputStream extends OutputStream { } } - /** {@inheritDoc} */ @Override public void flush() throws IOException { out.flush(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/memory/TernarySearchTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/memory/TernarySearchTree.java index 1ac6627360..72434dbffe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/memory/TernarySearchTree.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/memory/TernarySearchTree.java @@ -9,9 +9,9 @@ */ package org.eclipse.jgit.internal.storage.memory; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -300,6 +300,7 @@ public final class TernarySearchTree<Value> { * Find the key which is the longest prefix of the given query string. * * @param query + * the query string * @return the key which is the longest prefix of the given query string or * {@code null} if none exists. */ @@ -339,7 +340,7 @@ public final class TernarySearchTree<Value> { * @return all keys */ public Iterable<String> getKeys() { - Queue<String> queue = new LinkedList<>(); + Queue<String> queue = new ArrayDeque<>(); lock.readLock().lock(); try { findKeysWithPrefix(root, new StringBuilder(), queue); @@ -357,7 +358,7 @@ public final class TernarySearchTree<Value> { * @return keys starting with given prefix */ public Iterable<String> getKeysWithPrefix(String prefix) { - Queue<String> keys = new LinkedList<>(); + Queue<String> keys = new ArrayDeque<>(); if (prefix == null) { return keys; } @@ -485,7 +486,7 @@ public final class TernarySearchTree<Value> { * @return keys matching given pattern. */ public Iterable<String> getKeysMatching(String pattern) { - Queue<String> keys = new LinkedList<>(); + Queue<String> keys = new ArrayDeque<>(); lock.readLock().lock(); try { findKeysWithPrefix(root, new StringBuilder(), 0, pattern, keys); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndex.java new file mode 100644 index 0000000000..15b52391b8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndex.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024, GerritForge Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.midx; + +import java.util.Set; + +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; + +/** + * An index over multiple packs + */ +public interface MultiPackIndex { + + /** + * Obtain the array of packfiles in the MultiPackIndex. + * <p> + * The pack ids correspond to positions in this list. + * + * @return array of packnames refered in this multipak index + */ + String[] getPackNames(); + + /** + * Does this index contains the object + * + * @param oid + * object id + * @return true of the index knows this the object + */ + boolean hasObject(AnyObjectId oid); + + /** + * Obtain the location of the object. + * <p> + * The returned object can be reused by the implementations. Callers + * must create a #copy() if they want to keep a reference. + * + * @param objectId + * objectId to read. + * @return mutable instance with the location or null if not found. + */ + PackOffset find(AnyObjectId objectId); + + /** + * Find objects matching the prefix abbreviation. + * + * @param matches + * set to add any located ObjectIds to. This is an output + * parameter. + * @param id + * prefix to search for. + * @param matchLimit + * maximum number of results to return. At most this many + * ObjectIds should be added to matches before returning. + */ + void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, int matchLimit); + + /** + * Memory size of this multipack index + * + * @return size of this multipack index in memory, in bytes + */ + long getMemorySize(); + + /** + * (packId, offset) coordinates of an object + */ + class PackOffset { + + int packId; + + long offset; + + protected PackOffset setValues(int packId, long offset) { + this.packId = packId; + this.offset = offset; + return this; + } + + public int getPackId() { + return packId; + } + + public long getOffset() { + return offset; + } + + public PackOffset copy() { + PackOffset copy = new PackOffset(); + return copy.setValues(this.packId, this.offset); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexConstants.java new file mode 100644 index 0000000000..5d86f44baf --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexConstants.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.midx; + +class MultiPackIndexConstants { + static final int MIDX_SIGNATURE = 0x4d494458; /* MIDX */ + + static final byte MIDX_VERSION = 1; + + /** + * We infer the length of object IDs (OIDs) from this value: + * + * <pre> + * 1 => SHA-1 + * 2 => SHA-256 + * </pre> + */ + static final byte OID_HASH_VERSION = 1; + + static final int MULTIPACK_INDEX_FANOUT_SIZE = 4 * 256; + + /** + * First 4 bytes describe the chunk id. Value 0 is a terminating label. + * Other 8 bytes provide the byte-offset in current file for chunk to start. + */ + static final int CHUNK_LOOKUP_WIDTH = 12; + + /** "PNAM" chunk */ + static final int MIDX_CHUNKID_PACKNAMES = 0x504e414d; + + /** "OIDF" chunk */ + static final int MIDX_CHUNKID_OIDFANOUT = 0x4f494446; + + /** "OIDL" chunk */ + static final int MIDX_CHUNKID_OIDLOOKUP = 0x4f49444c; + + /** "OOFF" chunk */ + static final int MIDX_CHUNKID_OBJECTOFFSETS = 0x4f4f4646; + + /** "LOFF" chunk */ + static final int MIDX_CHUNKID_LARGEOFFSETS = 0x4c4f4646; + + /** "RIDX" chunk */ + static final int MIDX_CHUNKID_REVINDEX = 0x52494458; + + /** "BTMP" chunk */ + static final int MIDX_CHUNKID_BITMAPPEDPACKS = 0x42544D50; + + private MultiPackIndexConstants() { + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoader.java new file mode 100644 index 0000000000..61caddc221 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoader.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2024, GerritForge Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.midx; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_BITMAPPEDPACKS; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_LARGEOFFSETS; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_REVINDEX; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_SIGNATURE; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.io.SilentFileInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The loader returns the representation of the MultiPackIndex file content. + */ +public class MultiPackIndexLoader { + private final static Logger LOG = LoggerFactory + .getLogger(MultiPackIndexLoader.class); + + /** + * Open an existing MultiPackIndex 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. + * + * @param midxFile + * existing multi-pack-index to read. + * @return a copy of the multi-pack-index file in memory + * @throws FileNotFoundException + * the file does not exist. + * @throws MultiPackIndexFormatException + * MultiPackIndex file's format is different from we expected. + * @throws java.io.IOException + * the file exists but could not be read due to security errors + * or unexpected data corruption. + */ + public static MultiPackIndex open(File midxFile) + throws FileNotFoundException, MultiPackIndexFormatException, + IOException { + try (SilentFileInputStream fd = new SilentFileInputStream(midxFile)) { + try { + return read(fd); + } catch (MultiPackIndexFormatException fe) { + throw fe; + } catch (IOException ioe) { + throw new IOException( + MessageFormat.format(JGitText.get().unreadableMIDX, + midxFile.getAbsolutePath()), + ioe); + } + } + } + + /** + * Read an existing MultiPackIndex file from a buffered stream. + * <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. + * + * @param fd + * stream to read the multipack-index file from. The stream must be + * buffered as some small IOs are performed against the stream. + * The caller is responsible for closing the stream. + * @return a copy of the MultiPackIndex file in memory + * @throws MultiPackIndexFormatException + * the MultiPackIndex file's format is different from we + * expected. + * @throws java.io.IOException + * the stream cannot be read. + */ + public static MultiPackIndex read(InputStream fd) + throws MultiPackIndexFormatException, IOException { + byte[] hdr = new byte[12]; + IO.readFully(fd, hdr, 0, hdr.length); + + int magic = NB.decodeInt32(hdr, 0); + + if (magic != MIDX_SIGNATURE) { + throw new MultiPackIndexFormatException(JGitText.get().notAMIDX); + } + + // Check MultiPackIndex version + int v = hdr[4]; + if (v != 1) { + throw new MultiPackIndexFormatException(MessageFormat + .format(JGitText.get().unsupportedMIDXVersion, v)); + } + + // Read the object Id version (1 byte) + // 1 => SHA-1 + // 2 => SHA-256 + // TODO: If the hash type does not match the repository's hash + // algorithm, + // the multi-pack-index file should be ignored with a warning + // presented to the user. + int commitIdVersion = hdr[5]; + if (commitIdVersion != 1) { + throw new MultiPackIndexFormatException( + JGitText.get().incorrectOBJECT_ID_LENGTH); + } + + // Read the number of "chunkOffsets" (1 byte) + int chunkCount = hdr[6]; + + // Read the number of multi-pack-index files (1 byte) + // This value is currently always zero. + // TODO populate this + // int numberOfMultiPackIndexFiles = hdr[7]; + + // Number of packfiles (4 bytes) + int packCount = NB.decodeInt32(hdr, 8); + + byte[] lookupBuffer = new byte[CHUNK_LOOKUP_WIDTH * (chunkCount + 1)]; + + IO.readFully(fd, lookupBuffer, 0, lookupBuffer.length); + + List<ChunkSegment> chunks = new ArrayList<>(chunkCount + 1); + for (int i = 0; i <= chunkCount; i++) { + // chunks[chunkCount] is just a marker, in order to record the + // length of the last chunk. + int id = NB.decodeInt32(lookupBuffer, i * 12); + long offset = NB.decodeInt64(lookupBuffer, i * 12 + 4); + chunks.add(new ChunkSegment(id, offset)); + } + + MultiPackIndexBuilder builder = MultiPackIndexBuilder.builder(); + builder.setPackCount(packCount); + for (int i = 0; i < chunkCount; i++) { + long chunkOffset = chunks.get(i).offset; + int chunkId = chunks.get(i).id; + long len = chunks.get(i + 1).offset - chunkOffset; + + if (len > Integer.MAX_VALUE - 8) { // http://stackoverflow.com/a/8381338 + throw new MultiPackIndexFormatException( + JGitText.get().multiPackIndexFileIsTooLargeForJgit); + } + + byte[] buffer = new byte[(int) len]; + IO.readFully(fd, buffer, 0, buffer.length); + + switch (chunkId) { + case MIDX_CHUNKID_OIDFANOUT: + builder.addOidFanout(buffer); + break; + case MIDX_CHUNKID_OIDLOOKUP: + builder.addOidLookUp(buffer); + break; + case MIDX_CHUNKID_PACKNAMES: + builder.addPackNames(buffer); + break; + case MIDX_CHUNKID_BITMAPPEDPACKS: + builder.addBitmappedPacks(buffer); + break; + case MIDX_CHUNKID_OBJECTOFFSETS: + builder.addObjectOffsets(buffer); + break; + case MIDX_CHUNKID_LARGEOFFSETS: + builder.addObjectLargeOffsets(buffer); + break; + default: + LOG.warn(MessageFormat.format(JGitText.get().midxChunkUnknown, + Integer.toHexString(chunkId))); + } + } + return builder.build(); + } + + private record ChunkSegment(int id, long offset) {} + + /** + * Accumulate byte[] of the different chunks, to build a multipack index + */ + // Visible for testing + static class MultiPackIndexBuilder { + + private final int hashLength; + + private int packCount; + + private byte[] oidFanout; + + private byte[] oidLookup; + + private String[] packNames; + + private byte[] bitmappedPackfiles; + + private byte[] objectOffsets; + + // Optional + private byte[] largeObjectOffsets; + + // Optional + private byte[] bitmapPackOrder; + + private MultiPackIndexBuilder(int hashLength) { + this.hashLength = hashLength; + } + + /** + * Create builder + * + * @return A builder of {@link MultiPackIndex}. + */ + static MultiPackIndexBuilder builder() { + return new MultiPackIndexBuilder(OBJECT_ID_LENGTH); + } + + MultiPackIndexBuilder setPackCount(int packCount) { + this.packCount = packCount; + return this; + } + + MultiPackIndexBuilder addOidFanout(byte[] buffer) + throws MultiPackIndexFormatException { + assertChunkNotSeenYet(oidFanout, MIDX_CHUNKID_OIDFANOUT); + oidFanout = buffer; + return this; + } + + MultiPackIndexBuilder addOidLookUp(byte[] buffer) + throws MultiPackIndexFormatException { + assertChunkNotSeenYet(oidLookup, MIDX_CHUNKID_OIDLOOKUP); + oidLookup = buffer; + return this; + } + + MultiPackIndexBuilder addPackNames(byte[] buffer) + throws MultiPackIndexFormatException { + assertChunkNotSeenYet(packNames, MIDX_CHUNKID_PACKNAMES); + packNames = new String(buffer, UTF_8).split("\u0000"); //$NON-NLS-1$ + return this; + } + + MultiPackIndexBuilder addBitmappedPacks(byte[] buffer) + throws MultiPackIndexFormatException { + assertChunkNotSeenYet(bitmappedPackfiles, + MIDX_CHUNKID_BITMAPPEDPACKS); + bitmappedPackfiles = buffer; + return this; + } + + MultiPackIndexBuilder addObjectOffsets(byte[] buffer) + throws MultiPackIndexFormatException { + assertChunkNotSeenYet(objectOffsets, MIDX_CHUNKID_OBJECTOFFSETS); + objectOffsets = buffer; + return this; + } + + MultiPackIndexBuilder addObjectLargeOffsets(byte[] buffer) + throws MultiPackIndexFormatException { + assertChunkNotSeenYet(largeObjectOffsets, + MIDX_CHUNKID_LARGEOFFSETS); + largeObjectOffsets = buffer; + return this; + } + + MultiPackIndexBuilder addReverseIndex(byte[] buffer) + throws MultiPackIndexFormatException { + assertChunkNotSeenYet(bitmapPackOrder, MIDX_CHUNKID_REVINDEX); + bitmapPackOrder = buffer; + return this; + } + + MultiPackIndex build() throws MultiPackIndexFormatException { + assertChunkNotNull(oidFanout, MIDX_CHUNKID_OIDFANOUT); + assertChunkNotNull(oidLookup, MIDX_CHUNKID_OIDLOOKUP); + assertChunkNotNull(packNames, MIDX_CHUNKID_PACKNAMES); + assertChunkNotNull(objectOffsets, MIDX_CHUNKID_OBJECTOFFSETS); + + assertPackCounts(packCount, packNames.length); + return new MultiPackIndexV1(hashLength, oidFanout, oidLookup, + packNames, bitmappedPackfiles, objectOffsets, largeObjectOffsets); + } + + private static void assertChunkNotNull(Object object, int chunkId) + throws MultiPackIndexFormatException { + if (object == null) { + throw new MultiPackIndexFormatException( + MessageFormat.format(JGitText.get().midxChunkNeeded, + Integer.toHexString(chunkId))); + } + } + + private static void assertChunkNotSeenYet(Object object, int chunkId) + throws MultiPackIndexFormatException { + if (object != null) { + throw new MultiPackIndexFormatException( + MessageFormat.format(JGitText.get().midxChunkRepeated, + Integer.toHexString(chunkId))); + } + } + + private static void assertPackCounts(int headerCount, + int packfileNamesCount) throws MultiPackIndexFormatException { + if (headerCount != packfileNamesCount) { + throw new MultiPackIndexFormatException(MessageFormat.format( + JGitText.get().multiPackIndexPackCountMismatch, + headerCount, packfileNamesCount)); + } + } + } + + /** + * Thrown when a MultiPackIndex file's format is different from we expected + */ + public static class MultiPackIndexFormatException extends IOException { + + private static final long serialVersionUID = 1L; + + /** + * Construct an exception. + * + * @param why + * description of the type of error. + */ + MultiPackIndexFormatException(String why) { + super(why); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java new file mode 100644 index 0000000000..948b7bc174 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2025, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.midx; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.util.NB; + +/** + * Prints a multipack index file in a human-readable format. + * + * @since 7.2 + */ +@SuppressWarnings({ "boxing", "nls" }) +public class MultiPackIndexPrettyPrinter { + + /** + * Writes to out, in human-readable format, the multipack index in rawMidx + * + * @param rawMidx the bytes of a multipack index + * @param out a writer + */ + public static void prettyPrint(byte[] rawMidx, PrintWriter out) { + // Header (12 bytes) + out.println("[ 0] Magic: " + new String(rawMidx, 0, 4, UTF_8)); + out.println("[ 4] Version number: " + (int) rawMidx[4]); + out.println("[ 5] OID version: " + (int) rawMidx[5]); + int chunkCount = rawMidx[6]; + out.println("[ 6] # of chunks: " + chunkCount); + out.println("[ 7] # of bases: " + (int) rawMidx[7]); + int numberOfPacks = NB.decodeInt32(rawMidx, 8); + out.println("[ 8] # of packs: " + numberOfPacks); + + // Chunk lookup table + List<ChunkSegment> chunkSegments = new ArrayList<>(); + int current = printChunkLookup(out, rawMidx, chunkCount, chunkSegments); + + for (int i = 0; i < chunkSegments.size() - 1; i++) { + ChunkSegment segment = chunkSegments.get(i); + if (current != segment.startOffset()) { + throw new IllegalStateException(String.format( + "We are at byte %d, but segment should start at %d", + current, segment.startOffset())); + } + out.printf("Starting chunk: %s @ %d%n", segment.chunkName(), + segment.startOffset()); + switch (segment.chunkName()) { + case "OIDF" -> current = printOIDF(out, rawMidx, current); + case "OIDL" -> current = printOIDL(out, rawMidx, current, + chunkSegments.get(i + 1).startOffset); + case "OOFF" -> current = printOOFF(out, rawMidx, current, + chunkSegments.get(i + 1).startOffset); + case "PNAM" -> current = printPNAM(out, rawMidx, current, + chunkSegments.get(i + 1).startOffset); + case "RIDX" -> current = printRIDX(out, rawMidx, current, + chunkSegments.get(i + 1).startOffset); + default -> { + out.printf( + "Skipping %s (don't know how to print it yet)%n", + segment.chunkName()); + current = (int) chunkSegments.get(i + 1).startOffset(); + } + } + } + // Checksum is a SHA-1, use ObjectId to parse it + out.printf("[ %d] Checksum %s%n", current, + ObjectId.fromRaw(rawMidx, current).name()); + out.printf("Total size: " + (current + 20)); + } + + private static int printChunkLookup(PrintWriter out, byte[] rawMidx, int chunkCount, + List<ChunkSegment> chunkSegments) { + out.println("Starting chunk lookup @ 12"); + int current = 12; + for (int i = 0; i < chunkCount; i++) { + String chunkName = new String(rawMidx, current, 4, UTF_8); + long offset = NB.decodeInt64(rawMidx, current + 4); + out.printf("[ %d] |%8s|%8d|%n", current, chunkName, offset); + current += CHUNK_LOOKUP_WIDTH; + chunkSegments.add(new ChunkSegment(chunkName, offset)); + } + String chunkName = "0000"; + long offset = NB.decodeInt64(rawMidx, current + 4); + out.printf("[ %d] |%8s|%8d|%n", current, chunkName, offset); + current += CHUNK_LOOKUP_WIDTH; + chunkSegments.add(new ChunkSegment(chunkName, offset)); + return current; + } + + private static int printOIDF(PrintWriter out, byte[] rawMidx, int start) { + int current = start; + for (short i = 0; i < 256; i++) { + out.printf("[ %d] (%02X) %d%n", current, i, + NB.decodeInt32(rawMidx, current)); + current += 4; + } + return current; + } + + private static int printOIDL(PrintWriter out, byte[] rawMidx, int start, long end) { + int i = start; + while (i < end) { + out.printf("[ %d] %s%n", i, + ObjectId.fromRaw(rawMidx, i).name()); + i += 20; + } + return i; + } + + private static int printOOFF(PrintWriter out, byte[] rawMidx, int start, long end) { + int i = start; + while (i < end) { + out.printf("[ %d] %d %d%n", i, NB.decodeInt32(rawMidx, i), + NB.decodeInt32(rawMidx, i + 4)); + i += 8; + } + return i; + } + + private static int printRIDX(PrintWriter out, byte[] rawMidx, int start, long end) { + int i = start; + while (i < end) { + out.printf("[ %d] %d%n", i, NB.decodeInt32(rawMidx, i)); + i += 4; + } + return (int) end; + } + + private static int printPNAM(PrintWriter out, byte[] rawMidx, int start, long end) { + int nameStart = start; + for (int i = start; i < end; i++) { + if (rawMidx[i] == 0) { + out + .println(new String(rawMidx, nameStart, i - nameStart)); + nameStart = i + 1; + } + } + return (int) end; + } + + private record ChunkSegment(String chunkName, long startOffset) { + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexV1.java new file mode 100644 index 0000000000..be752cc4b5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexV1.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2024, GerritForge Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.midx; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Set; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.midx.MultiPackIndexLoader.MultiPackIndexFormatException; +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.util.NB; + +/** + * Support for the MultiPackIndex v1 format. + * + * @see MultiPackIndex + */ +class MultiPackIndexV1 implements MultiPackIndex { + + private final OidLookup idx; + + private final String[] packNames; + + private final byte[] bitmappedPackfiles; + + private final OffsetLookup offsets; + + private final PackOffset result = new PackOffset(); + + MultiPackIndexV1(int hashLength, @NonNull byte[] oidFanout, + @NonNull byte[] oidLookup, String[] packNames, + byte[] bitmappedPackfiles, byte[] objectOffsets, + byte[] largeObjectOffsets) throws MultiPackIndexFormatException { + this.bitmappedPackfiles = bitmappedPackfiles; + this.idx = new OidLookup(hashLength, oidFanout, oidLookup); + this.offsets = new OffsetLookup(objectOffsets, largeObjectOffsets); + this.packNames = packNames; + } + + @Override + public String[] getPackNames() { + return packNames; + } + + @Override + public boolean hasObject(AnyObjectId oid) { + return idx.findMultiPackIndexPosition(oid) != -1; + } + + @Override + @Nullable + public PackOffset find(AnyObjectId objectId) { + int position = idx.findMultiPackIndexPosition(objectId); + if (position == -1) { + return null; + } + offsets.getObjectOffset(position, result); + return result; + } + + @Override + public void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, + int matchLimit) { + idx.resolve(matches, id, matchLimit); + } + + @Override + public long getMemorySize() { + int packNamesSize = Arrays.stream(packNames) + .mapToInt(s -> s.getBytes(StandardCharsets.UTF_8).length).sum(); + return packNamesSize + byteArrayLengh(bitmappedPackfiles) + + idx.getMemorySize() + offsets.getMemorySize(); + } + + @Override + public String toString() { + return "MultiPackIndexV1 {idx=" + idx + ", packfileNames=" //$NON-NLS-1$ //$NON-NLS-2$ + + Arrays.toString(packNames) + ", bitmappedPackfiles=" //$NON-NLS-1$ + + byteArrayToString(bitmappedPackfiles) + ", objectOffsets=" //$NON-NLS-1$ + + offsets + '}'; + } + + private static String byteArrayToString(byte[] array) { + return array == null ? "null" : new String(array); //$NON-NLS-1$ + } + + private static int byteArrayLengh(byte[] array) { + return array == null ? 0 : array.length; + } + + /** + * Wraps the small and large offset chunks (if exists), to lookup offsets. + */ + private static class OffsetLookup { + private static final int OBJECT_OFFSETS_DATA_WIDTH = 8; + + private static final int BIT_31_ON = 0x80000000; + + private static final int TOGGLE_BIT_31 = 0x7fff_ffff; + + private final byte[] offsets; + + private final byte[] largeOffsets; + + /** + * Initialize the ObjectOffsets. + * + * @param offsets + * content of ObjectOffset Chunk. + * @param largeOffsets + * content of largo offsets chunks (can be null). + */ + OffsetLookup(@NonNull byte[] offsets, byte[] largeOffsets) { + this.offsets = offsets; + this.largeOffsets = largeOffsets; + } + + /** + * Get the metadata of a commit。 + * + * @param position + * the position in the multi-pack-index of the object. + * @param result + * an instance of PackOffset to populate with the result. + */ + void getObjectOffset(int position, PackOffset result) { + int offsetInChunk = position * OBJECT_OFFSETS_DATA_WIDTH; + int packId = NB.decodeInt32(offsets, offsetInChunk); + int offset = NB.decodeInt32(offsets, offsetInChunk + 4); + if ((offset & BIT_31_ON) != 0) { + long bigOffset; + if (largeOffsets == null) { + bigOffset = NB.decodeUInt32(offsets, offsetInChunk + 4); + } else { + int bigOffsetPos = (offset & TOGGLE_BIT_31); + bigOffset = NB.decodeInt64(largeOffsets, bigOffsetPos * 8); + } + result.setValues(packId, bigOffset); + return; + } + result.setValues(packId, offset); + } + + long getMemorySize() { + return (long) byteArrayLengh(offsets) + + byteArrayLengh(largeOffsets); + } + } + + /** + * Combines the fanout and oid list chunks, to lookup Oids with an efficient + * binary search + */ + private static class OidLookup { + + private static final int FANOUT = 256; + + private final int hashLength; + + private final int[] fanoutTable; + + private final byte[] oidLookup; + + /** + * Initialize the MultiPackIndexIndex. + * + * @param hashLength + * length of object hash. + * @param oidFanout + * content of OID Fanout Chunk. + * @param oidLookup + * content of OID Lookup Chunk. + * @throws MultiPackIndexFormatException + * MultiPackIndex file's format is different from we + * expected. + */ + OidLookup(int hashLength, @NonNull byte[] oidFanout, + @NonNull byte[] oidLookup) + throws MultiPackIndexFormatException { + this.hashLength = hashLength; + this.oidLookup = oidLookup; + + int[] table = new int[FANOUT]; + long uint32; + for (int k = 0; k < table.length; k++) { + uint32 = NB.decodeUInt32(oidFanout, k * 4); + if (uint32 > Integer.MAX_VALUE) { + throw new MultiPackIndexFormatException( + JGitText.get().multiPackIndexFileIsTooLargeForJgit); + } + table[k] = (int) uint32; + } + this.fanoutTable = table; + } + + /** + * Find the position in the MultiPackIndex file of the specified id. + * + * @param id + * the id for which the multi-pack-index position will be + * found. + * @return the MultiPackIndex position or -1 if the object was not + * found. + */ + int findMultiPackIndexPosition(AnyObjectId id) { + int levelOne = id.getFirstByte(); + int high = fanoutTable[levelOne]; + int low = 0; + if (levelOne > 0) { + low = fanoutTable[levelOne - 1]; + } + while (low < high) { + int mid = (low + high) >>> 1; + int cmp = id.compareTo(oidLookup, hashLength * mid); + if (cmp < 0) { + high = mid; + } else if (cmp == 0) { + return mid; + } else { + low = mid + 1; + } + } + return -1; + } + + void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, + int matchLimit) { + if (matches.size() >= matchLimit) { + return; + } + + if (oidLookup.length == 0) { + return; + } + + int high = fanoutTable[id.getFirstByte()]; + int low = id.getFirstByte() == 0 ? 0 + : fanoutTable[id.getFirstByte() - 1]; + do { + int p = (low + high) >>> 1; + int cmp = id.prefixCompare(oidLookup, idOffset(p)); + if (cmp < 0) { + high = p; + continue; + } + + if (cmp > 0) { + low = p + 1; + continue; + } + + // Got a match. + // We may have landed in the middle of the matches. Move + // backwards to the start of matches, then walk forwards. + while (0 < p + && id.prefixCompare(oidLookup, idOffset(p - 1)) == 0) { + p--; + } + while (p < high && id.prefixCompare(oidLookup, idOffset(p)) == 0 + && matches.size() < matchLimit) { + matches.add(ObjectId.fromRaw(oidLookup, idOffset(p))); + p++; + } + return; + } while (low < high); + } + + private int idOffset(int position) { + return position * hashLength; + } + + long getMemorySize() { + return 4L + byteArrayLengh(oidLookup) + (FANOUT * 4); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriter.java new file mode 100644 index 0000000000..b42c821a44 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriter.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2025, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.midx; + +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_LARGEOFFSETS; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_REVINDEX; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_SIGNATURE; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_VERSION; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MULTIPACK_INDEX_FANOUT_SIZE; +import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.OID_HASH_VERSION; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.file.PackIndex; +import org.eclipse.jgit.internal.storage.io.CancellableDigestOutputStream; +import org.eclipse.jgit.internal.storage.midx.PackIndexMerger.MidxMutableEntry; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.util.NB; + +/** + * Writes a collection of indexes as a multipack index. + * <p> + * See <a href= + * "https://git-scm.com/docs/pack-format#_multi_pack_index_midx_files_have_the_following_format">multipack + * index format spec</a> + * + * @since 7.2 + */ +public class MultiPackIndexWriter { + + private static final int LIMIT_31_BITS = (1 << 31) - 1; + + private static final int MIDX_HEADER_SIZE = 12; + + /** + * Writes the inputs in the multipack index format in the outputStream. + * + * @param monitor + * progress monitor + * @param outputStream + * stream to write the multipack index file + * @param inputs + * pairs of name and index for each pack to include in the + * multipack index. + * @return bytes written into the stream + * @throws IOException + * Error writing to the stream + */ + public long write(ProgressMonitor monitor, OutputStream outputStream, + Map<String, PackIndex> inputs) throws IOException { + PackIndexMerger data = new PackIndexMerger(inputs); + + // List of chunks in the order they need to be written + List<ChunkHeader> chunkHeaders = createChunkHeaders(data); + long expectedSize = calculateExpectedSize(chunkHeaders); + try (CancellableDigestOutputStream out = new CancellableDigestOutputStream( + monitor, outputStream)) { + writeHeader(out, chunkHeaders.size(), data.getPackCount()); + writeChunkLookup(out, chunkHeaders); + + WriteContext ctx = new WriteContext(out, data); + for (ChunkHeader chunk : chunkHeaders) { + chunk.writerFn.write(ctx); + } + writeCheckSum(out); + if (expectedSize != out.length()) { + throw new IllegalStateException(String.format( + JGitText.get().multiPackIndexUnexpectedSize, + Long.valueOf(expectedSize), + Long.valueOf(out.length()))); + } + return expectedSize; + } catch (InterruptedIOException e) { + throw new IOException(JGitText.get().multiPackIndexWritingCancelled, + e); + } + } + + private static long calculateExpectedSize(List<ChunkHeader> chunks) { + int chunkLookup = (chunks.size() + 1) * CHUNK_LOOKUP_WIDTH; + long chunkContent = chunks.stream().mapToLong(c -> c.size).sum(); + return /* header */ 12 + chunkLookup + chunkContent + /* CRC */ 20; + } + + private List<ChunkHeader> createChunkHeaders(PackIndexMerger data) { + List<ChunkHeader> chunkHeaders = new ArrayList<>(); + chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_OIDFANOUT, + MULTIPACK_INDEX_FANOUT_SIZE, this::writeFanoutTable)); + chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_OIDLOOKUP, + (long) data.getUniqueObjectCount() * OBJECT_ID_LENGTH, + this::writeOidLookUp)); + chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_OBJECTOFFSETS, + 8L * data.getUniqueObjectCount(), this::writeObjectOffsets)); + if (data.needsLargeOffsetsChunk()) { + chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_LARGEOFFSETS, + 8L * data.getOffsetsOver31BitsCount(), + this::writeObjectLargeOffsets)); + } + chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_REVINDEX, + 4L * data.getUniqueObjectCount(), this::writeRidx)); + + int packNamesSize = data.getPackNames().stream() + .mapToInt(String::length).map(i -> i + 1 /* null at the end */) + .sum(); + chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_PACKNAMES, packNamesSize, + this::writePackfileNames)); + return chunkHeaders; + } + + /** + * Write the first 12 bytes of the multipack index. + * <p> + * These bytes include things like magic number, version, number of + * chunks... + * + * @param out + * output stream to write + * @param numChunks + * number of chunks this multipack index is going to have + * @param packCount + * number of packs covered by this multipack index + * @throws IOException + * error writing to the output stream + */ + private void writeHeader(CancellableDigestOutputStream out, int numChunks, + int packCount) throws IOException { + byte[] headerBuffer = new byte[MIDX_HEADER_SIZE]; + NB.encodeInt32(headerBuffer, 0, MIDX_SIGNATURE); + byte[] buff = { MIDX_VERSION, OID_HASH_VERSION, (byte) numChunks, + (byte) 0 }; + System.arraycopy(buff, 0, headerBuffer, 4, 4); + NB.encodeInt32(headerBuffer, 8, packCount); + out.write(headerBuffer, 0, headerBuffer.length); + out.flush(); + } + + /** + * Write a table of "chunkId, start-offset", with a special value "0, + * end-of-previous_chunk", to mark the end. + * + * @param out + * output stream to write + * @param chunkHeaders + * list of chunks in the order they are expected to be written + * @throws IOException + * error writing to the output stream + */ + private void writeChunkLookup(CancellableDigestOutputStream out, + List<ChunkHeader> chunkHeaders) throws IOException { + + // first chunk will start at header + this lookup block + long chunkStart = MIDX_HEADER_SIZE + + (long) (chunkHeaders.size() + 1) * CHUNK_LOOKUP_WIDTH; + byte[] chunkEntry = new byte[CHUNK_LOOKUP_WIDTH]; + for (ChunkHeader chunkHeader : chunkHeaders) { + NB.encodeInt32(chunkEntry, 0, chunkHeader.chunkId); + NB.encodeInt64(chunkEntry, 4, chunkStart); + out.write(chunkEntry); + chunkStart += chunkHeader.size; + } + // Terminating label for the block + // (chunkid 0, offset where the next block would start) + NB.encodeInt32(chunkEntry, 0, 0); + NB.encodeInt64(chunkEntry, 4, chunkStart); + out.write(chunkEntry); + } + + /** + * Write the fanout table for the object ids + * <p> + * Table with 256 entries (one byte), where the ith entry, F[i], stores the + * number of OIDs with first byte at most i. Thus, F[255] stores the total + * number of objects. + * + * @param ctx + * write context + * @throws IOException + * error writing to the output stream + */ + + private void writeFanoutTable(WriteContext ctx) throws IOException { + byte[] tmp = new byte[4]; + int[] fanout = new int[256]; + Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator(); + while (iterator.hasNext()) { + MidxMutableEntry e = iterator.next(); + fanout[e.getObjectId().getFirstByte() & 0xff]++; + } + for (int i = 1; i < fanout.length; i++) { + fanout[i] += fanout[i - 1]; + } + for (int n : fanout) { + NB.encodeInt32(tmp, 0, n); + ctx.out.write(tmp, 0, 4); + } + } + + /** + * Write the OID lookup chunk + * <p> + * A list of OIDs in sha1 order. + * + * @param ctx + * write context + * @throws IOException + * error writing to the output stream + */ + private void writeOidLookUp(WriteContext ctx) throws IOException { + byte[] tmp = new byte[OBJECT_ID_LENGTH]; + + Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator(); + while (iterator.hasNext()) { + MidxMutableEntry e = iterator.next(); + e.getObjectId().copyRawTo(tmp, 0); + ctx.out.write(tmp, 0, OBJECT_ID_LENGTH); + } + } + + /** + * Write the object offsets chunk + * <p> + * A list of offsets, parallel to the list of OIDs. If the offset is too + * large (see {@link #fitsIn31bits(long)}), this contains the position in + * the large offsets list (marked with a 1 in the most significant bit). + * + * @param ctx + * write context + * @throws IOException + * error writing to the output stream + */ + private void writeObjectOffsets(WriteContext ctx) throws IOException { + byte[] entry = new byte[8]; + Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator(); + while (iterator.hasNext()) { + MidxMutableEntry e = iterator.next(); + NB.encodeInt32(entry, 0, e.getPackId()); + if (!ctx.data.needsLargeOffsetsChunk() + || fitsIn31bits(e.getOffset())) { + NB.encodeInt32(entry, 4, (int) e.getOffset()); + } else { + int offloadedPosition = ctx.largeOffsets.append(e.getOffset()); + NB.encodeInt32(entry, 4, offloadedPosition | (1 << 31)); + } + ctx.out.write(entry); + } + } + + /** + * Writes the reverse index chunk + * <p> + * This stores the position of the objects in the main index, ordered first + * by pack and then by offset + * + * @param ctx + * write context + * @throws IOException + * erorr writing to the output stream + */ + private void writeRidx(WriteContext ctx) throws IOException { + Map<Integer, List<OffsetPosition>> packOffsets = new HashMap<>( + ctx.data.getPackCount()); + // TODO(ifrade): Brute force solution loading all offsets/packs in + // memory. We could also iterate reverse indexes looking up + // their position in the midx (and discarding if the pack doesn't + // match). + Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator(); + int midxPosition = 0; + while (iterator.hasNext()) { + MidxMutableEntry e = iterator.next(); + OffsetPosition op = new OffsetPosition(e.getOffset(), midxPosition); + midxPosition++; + packOffsets.computeIfAbsent(Integer.valueOf(e.getPackId()), + k -> new ArrayList<>()).add(op); + } + + for (int i = 0; i < ctx.data.getPackCount(); i++) { + List<OffsetPosition> offsetsForPack = packOffsets + .get(Integer.valueOf(i)); + if (offsetsForPack == null) { + continue; + } + offsetsForPack.sort(Comparator.comparing(OffsetPosition::offset)); + byte[] ridxForPack = new byte[4 * offsetsForPack.size()]; + for (int j = 0; j < offsetsForPack.size(); j++) { + NB.encodeInt32(ridxForPack, j * 4, + offsetsForPack.get(j).position); + } + ctx.out.write(ridxForPack); + } + } + + /** + * Write the large offset chunk + * <p> + * A list of large offsets (long). The regular offset chunk will point to a + * position here. + * + * @param ctx + * writer context + * @throws IOException + * error writing to the output stream + */ + private void writeObjectLargeOffsets(WriteContext ctx) throws IOException { + ctx.out.write(ctx.largeOffsets.offsets, 0, + ctx.largeOffsets.bytePosition); + } + + /** + * Write the list of packfiles chunk + * <p> + * List of packfiles (in lexicographical order) with an \0 at the end + * + * @param ctx + * writer context + * @throws IOException + * error writing to the output stream + */ + private void writePackfileNames(WriteContext ctx) throws IOException { + for (String packName : ctx.data.getPackNames()) { + // Spec doesn't talk about encoding. + ctx.out.write(packName.getBytes(StandardCharsets.UTF_8)); + ctx.out.write(0); + } + } + + /** + * Write final checksum of the data written to the stream + * + * @param out + * output stream used to write + * @throws IOException + * error writing to the output stream + */ + private void writeCheckSum(CancellableDigestOutputStream out) + throws IOException { + out.write(out.getDigest()); + out.flush(); + } + + private record OffsetPosition(long offset, int position) { + } + + /** + * If there is at least one offset value larger than 2^32-1, then the large + * offset chunk must exist, and offsets larger than 2^31-1 must be stored in + * it instead + * + * @param offset + * object offset + * + * @return true if the offset fits in 31 bits + */ + private static boolean fitsIn31bits(long offset) { + return offset <= LIMIT_31_BITS; + } + + private static class LargeOffsets { + private final byte[] offsets; + + private int bytePosition; + + LargeOffsets(int largeOffsetsCount) { + offsets = new byte[largeOffsetsCount * 8]; + bytePosition = 0; + } + + /** + * Add an offset to the large offset chunk + * + * @param largeOffset + * a large offset + * @return the position of the just inserted offset (as in number of + * offsets, NOT in bytes) + */ + int append(long largeOffset) { + int at = bytePosition; + NB.encodeInt64(offsets, at, largeOffset); + bytePosition += 8; + return at / 8; + } + } + + private record ChunkHeader(int chunkId, long size, ChunkWriter writerFn) { + } + + @FunctionalInterface + private interface ChunkWriter { + void write(WriteContext ctx) throws IOException; + } + + private static class WriteContext { + final CancellableDigestOutputStream out; + + final PackIndexMerger data; + + final LargeOffsets largeOffsets; + + WriteContext(CancellableDigestOutputStream out, PackIndexMerger data) { + this.out = out; + this.data = data; + this.largeOffsets = new LargeOffsets( + data.getOffsetsOver31BitsCount()); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/PackIndexMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/PackIndexMerger.java new file mode 100644 index 0000000000..f23665849e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/PackIndexMerger.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2025, Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.storage.midx; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +import org.eclipse.jgit.internal.storage.file.PackIndex; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.MutableObjectId; + +/** + * Collect the stats and offers an iterator over the union of n-pack indexes. + * <p> + * The multipack index is a list of (sha1, packid, offset) ordered by sha1. We + * can build it from the individual pack indexes (sha1, offset) ordered by sha1, + * with a simple merge ignoring duplicates. + * <p> + * This class encapsulates the merging logic and precalculates the stats that + * the index needs (like total count of objects). To limit memory consumption, + * it does the merge as it goes during the iteration and iterators use mutable + * entries. The stats of the combined index are calculated in an iteration at + * construction time. + */ +class PackIndexMerger { + + private static final int LIMIT_31_BITS = (1 << 31) - 1; + + private static final long LIMIT_32_BITS = (1L << 32) - 1; + + /** + * Object returned by the iterator. + * <p> + * The iterator returns (on each next()) the same instance with different + * values, to avoid allocating many short-lived objects. Callers should not + * keep a reference to that returned value. + */ + static class MidxMutableEntry { + // The object id + private final MutableObjectId oid = new MutableObjectId(); + + // Position of the pack in the ordered list of pack in this merger + private int packId; + + // Offset in its pack + private long offset; + + public AnyObjectId getObjectId() { + return oid; + } + + public int getPackId() { + return packId; + } + + public long getOffset() { + return offset; + } + + /** + * Copy values from another mutable entry + * + * @param packId + * packId + * @param other + * another mutable entry + */ + private void fill(int packId, PackIndex.MutableEntry other) { + other.copyOidTo(oid); + this.packId = packId; + this.offset = other.getOffset(); + } + } + + private final List<String> packNames; + + private final List<PackIndex> indexes; + + private final boolean needsLargeOffsetsChunk; + + private final int offsetsOver31BitsCount; + + private final int uniqueObjectCount; + + PackIndexMerger(Map<String, PackIndex> packs) { + this.packNames = packs.keySet().stream().sorted() + .collect(Collectors.toUnmodifiableList()); + + this.indexes = packNames.stream().map(packs::get) + .collect(Collectors.toUnmodifiableList()); + + // Iterate for duplicates + int objectCount = 0; + boolean hasLargeOffsets = false; + int over31bits = 0; + MutableObjectId lastSeen = new MutableObjectId(); + MultiIndexIterator it = new MultiIndexIterator(indexes); + while (it.hasNext()) { + MidxMutableEntry entry = it.next(); + if (lastSeen.equals(entry.oid)) { + continue; + } + // If there is at least one offset value larger than 2^32-1, then + // the large offset chunk must exist, and offsets larger than + // 2^31-1 must be stored in it instead + if (entry.offset > LIMIT_32_BITS) { + hasLargeOffsets = true; + } + if (entry.offset > LIMIT_31_BITS) { + over31bits++; + } + + lastSeen.fromObjectId(entry.oid); + objectCount++; + } + uniqueObjectCount = objectCount; + offsetsOver31BitsCount = over31bits; + needsLargeOffsetsChunk = hasLargeOffsets; + } + + /** + * Object count of the merged index (i.e. without duplicates) + * + * @return object count of the merged index + */ + int getUniqueObjectCount() { + return uniqueObjectCount; + } + + /** + * If any object in any of the indexes has an offset over 2^32-1 + * + * @return true if there is any object with offset > 2^32 -1 + */ + boolean needsLargeOffsetsChunk() { + return needsLargeOffsetsChunk; + } + + /** + * How many object have offsets over 2^31-1 + * <p> + * Per multipack index spec, IF there is large offset chunk, all this + * offsets should be there. + * + * @return number of objects with offsets over 2^31-1 + */ + int getOffsetsOver31BitsCount() { + return offsetsOver31BitsCount; + } + + /** + * List of pack names in alphabetical order. + * <p> + * Order matters: In case of duplicates, the multipack index prefers the + * first package with it. This is in the same order we are using to + * prioritize duplicates. + * + * @return List of pack names, in the order used by the merge. + */ + List<String> getPackNames() { + return packNames; + } + + /** + * How many packs are being merged + * + * @return count of packs merged + */ + int getPackCount() { + return packNames.size(); + } + + /** + * Iterator over the merged indexes in sha1 order without duplicates + * <p> + * The returned entry in the iterator is mutable, callers should NOT keep a + * reference to it. + * + * @return an iterator in sha1 order without duplicates. + */ + Iterator<MidxMutableEntry> bySha1Iterator() { + return new DedupMultiIndexIterator(new MultiIndexIterator(indexes), + getUniqueObjectCount()); + } + + /** + * For testing. Iterate all entries, not skipping duplicates (stable order) + * + * @return an iterator of all objects in sha1 order, including duplicates. + */ + Iterator<MidxMutableEntry> rawIterator() { + return new MultiIndexIterator(indexes); + } + + /** + * Iterator over n-indexes in ObjectId order. + * <p> + * It returns duplicates if the same object id is in different indexes. Wrap + * it with {@link DedupMultiIndexIterator (Iterator, int)} to avoid + * duplicates. + */ + private static final class MultiIndexIterator + implements Iterator<MidxMutableEntry> { + + private final List<PackIndexPeekIterator> indexIterators; + + private final MidxMutableEntry mutableEntry = new MidxMutableEntry(); + + MultiIndexIterator(List<PackIndex> indexes) { + this.indexIterators = new ArrayList<>(indexes.size()); + for (int i = 0; i < indexes.size(); i++) { + PackIndexPeekIterator it = new PackIndexPeekIterator(i, + indexes.get(i)); + // Position in the first element + if (it.next() != null) { + indexIterators.add(it); + } + } + } + + @Override + public boolean hasNext() { + return !indexIterators.isEmpty(); + } + + @Override + public MidxMutableEntry next() { + PackIndexPeekIterator winner = null; + for (int index = 0; index < indexIterators.size(); index++) { + PackIndexPeekIterator current = indexIterators.get(index); + if (winner == null + || current.peek().compareBySha1To(winner.peek()) < 0) { + winner = current; + } + } + + if (winner == null) { + throw new NoSuchElementException(); + } + + mutableEntry.fill(winner.getPackId(), winner.peek()); + if (winner.next() == null) { + indexIterators.remove(winner); + }; + return mutableEntry; + } + } + + private static class DedupMultiIndexIterator + implements Iterator<MidxMutableEntry> { + private final MultiIndexIterator src; + + private int remaining; + + private final MutableObjectId lastOid = new MutableObjectId(); + + DedupMultiIndexIterator(MultiIndexIterator src, int totalCount) { + this.src = src; + this.remaining = totalCount; + } + + @Override + public boolean hasNext() { + return remaining > 0; + } + + @Override + public MidxMutableEntry next() { + MidxMutableEntry next = src.next(); + while (next != null && lastOid.equals(next.oid)) { + next = src.next(); + } + + if (next == null) { + throw new NoSuchElementException(); + } + + lastOid.fromObjectId(next.oid); + remaining--; + return next; + } + } + + /** + * Convenience around the PackIndex iterator to read the current value + * multiple times without consuming it. + * <p> + * This is used to merge indexes in the multipack index, where we need to + * compare the current value between indexes multiple times to find the + * next. + * <p> + * We could also implement this keeping the position (int) and + * MutableEntry#getObjectId, but that would create an ObjectId per entry. + * This implementation reuses the MutableEntry and avoid instantiations. + */ + // Visible for testing + static class PackIndexPeekIterator { + private final Iterator<PackIndex.MutableEntry> it; + + private final int packId; + + PackIndex.MutableEntry current; + + PackIndexPeekIterator(int packId, PackIndex index) { + it = index.iterator(); + this.packId = packId; + } + + PackIndex.MutableEntry next() { + if (it.hasNext()) { + current = it.next(); + } else { + current = null; + } + return current; + } + + PackIndex.MutableEntry peek() { + return current; + } + + int getPackId() { + return packId; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/CachedPackUriProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/CachedPackUriProvider.java index 0b419da467..32cd635edf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/CachedPackUriProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/CachedPackUriProvider.java @@ -21,14 +21,18 @@ import org.eclipse.jgit.annotations.Nullable; public interface CachedPackUriProvider { /** - * @param pack the cached pack for which to check if a corresponding URI - * exists - * @param protocolsSupported the protocols that the client has declared - * support for; if a URI is returned, it must be of one of these - * protocols - * @throws IOException implementations may throw this - * @return if a URI corresponds to the cached pack, an object - * containing the URI and some other information; null otherwise + * Get pack info + * + * @param pack + * the cached pack for which to check if a corresponding URI + * exists + * @param protocolsSupported + * the protocols that the client has declared support for; if a + * URI is returned, it must be of one of these protocols + * @throws IOException + * implementations may throw this + * @return if a URI corresponds to the cached pack, an object containing the + * URI and some other information; null otherwise * @since 5.5 */ @Nullable @@ -62,6 +66,8 @@ public interface CachedPackUriProvider { } /** + * Get the hash of the packfile + * * @return the hash of the packfile as a hexadecimal string */ public String getHash() { @@ -69,6 +75,8 @@ public interface CachedPackUriProvider { } /** + * Get the URI corresponding to the packfile + * * @return the URI corresponding to the packfile */ public String getUri() { @@ -76,6 +84,8 @@ public interface CachedPackUriProvider { } /** + * Get the size of the packfile in bytes + * * @return the size of the packfile in bytes (-1 if unknown) */ public long getSize() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaIndex.java index 7ed5defbe4..0f71c7fe39 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaIndex.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaIndex.java @@ -48,7 +48,7 @@ public class DeltaIndex { * @return estimated size. Approximately {@code 1.75 * sourceLength}. */ public static long estimateIndexSize(int sourceLength) { - return sourceLength + (sourceLength * 3 / 4); + return sourceLength + (sourceLength * 3L / 4); } /** @@ -393,7 +393,6 @@ public class DeltaIndex { return start - resPtr; } - /** {@inheritDoc} */ @Override @SuppressWarnings("nls") public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaTask.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaTask.java index 132eb5095b..03d6f16733 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaTask.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaTask.java @@ -11,10 +11,10 @@ package org.eclipse.jgit.internal.storage.pack; import java.io.IOException; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.concurrent.Callable; @@ -232,14 +232,15 @@ final class DeltaTask implements Callable<Object> { } private final Block block; - final LinkedList<Slice> slices; + + final ArrayDeque<Slice> slices; private ObjectReader or; private DeltaWindow dw; DeltaTask(Block b) { this.block = b; - this.slices = new LinkedList<>(); + this.slices = new ArrayDeque<>(); } void add(Slice s) { @@ -254,7 +255,6 @@ final class DeltaTask implements Callable<Object> { slices.add(s); } - /** {@inheritDoc} */ @Override public Object call() throws Exception { or = block.templateReader.newReader(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaWindowEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaWindowEntry.java index 7f8504d6c0..b3f0574164 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaWindowEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaWindowEntry.java @@ -27,22 +27,38 @@ final class DeltaWindowEntry { this.buffer = null; } - /** @return current delta chain depth of this object. */ + /** + * Get delta chain depth + * + * @return current delta chain depth of this object. + */ final int depth() { return object.getDeltaDepth(); } - /** @return type of the object in this window entry. */ + /** + * Get object type in this window entry + * + * @return type of the object in this window entry. + */ final int type() { return object.getType(); } - /** @return estimated unpacked size of the object, in bytes . */ + /** + * Get estimated unpacked object size + * + * @return estimated unpacked size of the object, in bytes . + */ final int size() { return object.getWeight(); } - /** @return true if there is no object stored in this entry. */ + /** + * Whether the entry is empty + * + * @return true if there is no object stored in this entry. + */ final boolean empty() { return object == null; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectToPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectToPack.java index d9013bff26..ea5b9461ca 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectToPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectToPack.java @@ -156,7 +156,6 @@ public class ObjectToPack extends PackedObjectInfo { return 1 < getOffset(); // markWantWrite sets 1. } - /** {@inheritDoc} */ @Override public final int getType() { return (flags >> TYPE_SHIFT) & 0x7; @@ -357,7 +356,6 @@ public class ObjectToPack extends PackedObjectInfo { // Empty by default. } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackBitmapIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackBitmapIndexWriter.java new file mode 100644 index 0000000000..9cf8c7f2b5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackBitmapIndexWriter.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024, Google Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.pack; + +import java.io.IOException; + +import org.eclipse.jgit.internal.storage.file.PackBitmapIndexBuilder; + +/** + * Represents a function that accepts a collection of bitmaps and write them + * into storage. + */ +@FunctionalInterface +public interface PackBitmapIndexWriter { + /** + * @param bitmaps + * list of bitmaps to be written to a bitmap index + * @param packChecksum + * checksum of the pack that the bitmap index refers to + * @throws IOException + * thrown in case of IO errors while writing the bitmap index + */ + public void write(PackBitmapIndexBuilder bitmaps, byte[] packChecksum) + throws IOException; +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java index adad411c6f..d5bb5f2e2f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java @@ -36,7 +36,10 @@ public enum PackExt { COMMIT_GRAPH("graph"), //$NON-NLS-1$ /** An object size index. */ - OBJECT_SIZE_INDEX("objsize"); //$NON-NLS-1$ + OBJECT_SIZE_INDEX("objsize"), //$NON-NLS-1$ + + /** Multi pack index */ + MULTI_PACK_INDEX("midx"); //$NON-NLS-1$ private final String ext; @@ -71,7 +74,15 @@ public enum PackExt { return 1 << getPosition(); } - /** {@inheritDoc} */ + /** + * Format a temporary file extension for this PackExt. + * + * @return a temporary file extension + */ + public String getTmpExtension() { + return String.format(".%s_tmp", ext); //$NON-NLS-1$ + } + @Override public String toString() { return String.format("PackExt[%s, bit=0x%s]", getExtension(), //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackIndexWriter.java new file mode 100644 index 0000000000..f69e68d4ba --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackIndexWriter.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024, Google LLC. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.pack; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jgit.transport.PackedObjectInfo; + +/** + * Represents a function that accepts a collection of objects to write into a + * primary pack index storage format. + */ +public interface PackIndexWriter { + /** + * Write all object entries to the index stream. + * + * @param toStore + * sorted list of objects to store in the index. The caller must + * have previously sorted the list using + * {@link org.eclipse.jgit.transport.PackedObjectInfo}'s native + * {@link java.lang.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 java.io.IOException + * an error occurred while writing to the output stream, or the + * underlying format cannot store the object data supplied. + */ + void write(List<? extends PackedObjectInfo> toStore, + byte[] packDataChecksum) throws IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java index bad572459a..27fb814f64 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java @@ -58,10 +58,10 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.SearchForReuseTimeout; import org.eclipse.jgit.errors.StoredObjectRepresentationNotAvailableException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter; import org.eclipse.jgit.internal.storage.file.PackBitmapIndexBuilder; -import org.eclipse.jgit.internal.storage.file.PackBitmapIndexWriterV1; -import org.eclipse.jgit.internal.storage.file.PackIndexWriter; import org.eclipse.jgit.internal.storage.file.PackObjectSizeIndexWriter; +import org.eclipse.jgit.internal.storage.file.PackReverseIndexWriter; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.AsyncObjectSizeQueue; import org.eclipse.jgit.lib.BatchingProgressMonitor; @@ -118,9 +118,9 @@ import org.eclipse.jgit.util.TemporaryBuffer; * {@link #preparePack(ProgressMonitor, Set, Set)}, and streaming with * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}. If the * pack is being stored as a file the matching index can be written out after - * writing the pack by {@link #writeIndex(OutputStream)}. An optional bitmap + * writing the pack by {@link #writeIndex(PackIndexWriter)}. An optional bitmap * index can be made by calling {@link #prepareBitmapIndex(ProgressMonitor)} - * followed by {@link #writeBitmapIndex(OutputStream)}. + * followed by {@link #writeBitmapIndex(PackBitmapIndexWriter)}. * </p> * <p> * Class provide set of configurable options and @@ -145,44 +145,46 @@ public class PackWriter implements AutoCloseable { private static final Map<WeakReference<PackWriter>, Boolean> instances = new ConcurrentHashMap<>(); - private static final Iterable<PackWriter> instancesIterable = () -> new Iterator<>() { + private static Iterator<PackWriter> instancesIterable() { + return new Iterator<>() { - private final Iterator<WeakReference<PackWriter>> it = instances - .keySet().iterator(); + private final Iterator<WeakReference<PackWriter>> it = instances + .keySet().iterator(); - private PackWriter next; + private PackWriter next; - @Override - public boolean hasNext() { - if (next != null) { - return true; - } - while (it.hasNext()) { - WeakReference<PackWriter> ref = it.next(); - next = ref.get(); + @Override + public boolean hasNext() { if (next != null) { return true; } - it.remove(); + while (it.hasNext()) { + WeakReference<PackWriter> ref = it.next(); + next = ref.get(); + if (next != null) { + return true; + } + it.remove(); + } + return false; } - return false; - } - @Override - public PackWriter next() { - if (hasNext()) { - PackWriter result = next; - next = null; - return result; + @Override + public PackWriter next() { + if (hasNext()) { + PackWriter result = next; + next = null; + return result; + } + throw new NoSuchElementException(); } - throw new NoSuchElementException(); - } - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } /** * Get all allocated, non-released PackWriters instances. @@ -190,7 +192,7 @@ public class PackWriter implements AutoCloseable { * @return all allocated, non-released PackWriters instances. */ public static Iterable<PackWriter> getInstances() { - return instancesIterable; + return PackWriter::instancesIterable; } @SuppressWarnings("unchecked") @@ -255,6 +257,8 @@ public class PackWriter implements AutoCloseable { private boolean useBitmaps; + private boolean createBitmaps = true; + private boolean ignoreMissingUninteresting = true; private boolean pruneCurrentObjectList; @@ -299,19 +303,6 @@ public class PackWriter implements AutoCloseable { } /** - * Create a writer to load objects from the specified reader. - * <p> - * Objects for packing are specified in {@link #preparePack(Iterator)} or - * {@link #preparePack(ProgressMonitor, Set, Set)}. - * - * @param reader - * reader to read from the repository with. - */ - public PackWriter(ObjectReader reader) { - this(new PackConfig(), reader); - } - - /** * Create writer for specified repository. * <p> * Objects for packing are specified in {@link #preparePack(Iterator)} or @@ -576,6 +567,26 @@ public class PackWriter implements AutoCloseable { } /** + * Whether to generate bitmaps. + * + * @param createBitmaps + * if set to true, bitmaps will be generated when creating a pack. + */ + public void setCreateBitmaps(boolean createBitmaps) { + this.createBitmaps = createBitmaps; + } + + /** + * Whether the bitmap file is to be created by this PackWriter. + * + * @return {@code true} if the bitmap file is to be created by this + * PackWriter. + */ + public boolean isCreateBitmaps() { + return createBitmaps; + } + + /** * Whether the index file cannot be created by this PackWriter. * * @return {@code true} if the index file cannot be created by this @@ -656,7 +667,10 @@ public class PackWriter implements AutoCloseable { } /** - * @param filter the filter which indicates what and what not this writer + * Set filter spec + * + * @param filter + * the filter which indicates what and what not this writer * should include */ public void setFilterSpec(@NonNull FilterSpec filter) { @@ -664,7 +678,10 @@ public class PackWriter implements AutoCloseable { } /** - * @param config configuration related to packfile URIs + * Set packfile URI config + * + * @param config + * configuration related to packfile URIs * @since 5.5 */ public void setPackfileUriConfig(PackfileUriConfig config) { @@ -1061,7 +1078,7 @@ public class PackWriter implements AutoCloseable { if (indexVersion <= 0) { for (BlockList<ObjectToPack> objs : objectsLists) indexVersion = Math.max(indexVersion, - PackIndexWriter.oldestPossibleFormat(objs)); + BasePackIndexWriter.oldestPossibleFormat(objs)); } return indexVersion; } @@ -1082,12 +1099,28 @@ public class PackWriter implements AutoCloseable { * the index data could not be written to the supplied stream. */ public void writeIndex(OutputStream indexStream) throws IOException { + writeIndex(BasePackIndexWriter.createVersion(indexStream, + getIndexVersion())); + } + + /** + * Create an index file to match the pack file just written. + * <p> + * Called after + * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}. + * <p> + * Writing an index is only required for local pack storage. Packs sent on + * the network do not need to create an index. + * + * @param iw + * an {@link PackIndexWriter} instance to write the index + * @throws java.io.IOException + * the index data could not be written to the supplied stream. + */ + public void writeIndex(PackIndexWriter iw) throws IOException { if (isIndexDisabled()) throw new IOException(JGitText.get().cachedPacksPreventsIndexCreation); - long writeStart = System.currentTimeMillis(); - final PackIndexWriter iw = PackIndexWriter.createVersion( - indexStream, getIndexVersion()); iw.write(sortByName(), packcsum); stats.timeWriting += System.currentTimeMillis() - writeStart; } @@ -1099,7 +1132,7 @@ public class PackWriter implements AutoCloseable { * Called after * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)} that * populates the list of objects to pack and before - * {@link #writeBitmapIndex(OutputStream)} that destroys it. + * {@link #writeBitmapIndex(PackBitmapIndexWriter)} that destroys it. * <p> * Writing this index is only required for local pack storage. Packs sent on * the network do not need to create an object size index. @@ -1137,24 +1170,54 @@ public class PackWriter implements AutoCloseable { } /** + * Whether the writer will write a reverse index file. The configuration + * flag must be on and the writer must be able to write corresponding + * forward index. + * + * @return whether the writer will write a reverse index file + */ + public boolean isReverseIndexEnabled() { + // Only write the reverse index if the writer is configured to and the + // forward index that it would correspond to will be written. + return config.isWriteReverseIndex() && !isIndexDisabled(); + } + + /** + * Write the pack's reverse index file to the output stream. + * + * @param stream + * where to write the file contents to + * @throws IOException + * if writing to the stream fails + */ + public void writeReverseIndex(OutputStream stream) throws IOException { + if (!isReverseIndexEnabled()) { + return; + } + long writeStart = System.currentTimeMillis(); + PackReverseIndexWriter writer = PackReverseIndexWriter + .createWriter(stream); + writer.write(sortByName(), packcsum); + stats.timeWriting += System.currentTimeMillis() - writeStart; + } + + /** * Create a bitmap index file to match the pack file just written. * <p> * Called after {@link #prepareBitmapIndex(ProgressMonitor)}. * - * @param bitmapIndexStream - * output for the bitmap index data. Caller is responsible for - * closing this stream. + * @param bitmapIndexWriter + * a writer to store the bitmap index in this object database * @throws java.io.IOException - * the index data could not be written to the supplied stream. + * the index data could not be written using the supplied writer */ - public void writeBitmapIndex(OutputStream bitmapIndexStream) + public void writeBitmapIndex(PackBitmapIndexWriter bitmapIndexWriter) throws IOException { if (writeBitmaps == null) throw new IOException(JGitText.get().bitmapsMustBePrepared); long writeStart = System.currentTimeMillis(); - final PackBitmapIndexWriterV1 iw = new PackBitmapIndexWriterV1(bitmapIndexStream); - iw.write(writeBitmaps, packcsum); + bitmapIndexWriter.write(writeBitmaps, packcsum); stats.timeWriting += System.currentTimeMillis() - writeStart; } @@ -1690,6 +1753,11 @@ public class PackWriter implements AutoCloseable { } throw new IOException(JGitText .get().packingCancelledDuringObjectsWriting, e); + } catch (Throwable e) { + if (e1 != null) { + e.addSuppressed(e1); + } + throw e; } } } @@ -1948,19 +2016,21 @@ public class PackWriter implements AutoCloseable { final long countingStart = System.currentTimeMillis(); beginPhase(PackingPhase.COUNTING, countingMonitor, ProgressMonitor.UNKNOWN); - stats.interestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(want)); - stats.uninterestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(have)); + stats.interestingObjects = Collections + .unmodifiableSet(new HashSet<>(want)); + stats.uninterestingObjects = Collections + .unmodifiableSet(new HashSet<>(have)); excludeFromBitmapSelection = noBitmaps; canBuildBitmaps = config.isBuildBitmaps() && !shallowPack && have.isEmpty() - && (excludeInPacks == null || excludeInPacks.length == 0); + && createBitmaps; if (!shallowPack && useBitmaps) { BitmapIndex bitmapIndex = reader.getBitmapIndex(); if (bitmapIndex != null) { - BitmapWalker bitmapWalker = new BitmapWalker( - walker, bitmapIndex, countingMonitor); + BitmapWalker bitmapWalker = new BitmapWalker(walker, + bitmapIndex, countingMonitor); findObjectsToPackUsingBitmaps(bitmapWalker, want, have); endPhase(countingMonitor); stats.timeCounting = System.currentTimeMillis() - countingStart; @@ -2248,7 +2318,7 @@ public class PackWriter implements AutoCloseable { /** * Determines if the object should be omitted from the pack as a result of - * its depth (probably because of the tree:<depth> filter). + * its depth (probably because of the tree:<depth> filter). * <p> * Causes {@code walker} to skip traversing the current tree, which ought to * have just started traversal, assuming this method is called as soon as a @@ -2324,17 +2394,46 @@ public class PackWriter implements AutoCloseable { * this method once for each representation available for an object, to * allow the writer to find the most suitable one for the output. * + * This method tries to take a very simple approach to avoiding delta chains + * during object reuse by selecting from the oldest pack that contains them. + * This helps select many objects from the same pack which helps make good + * use of any WindowCache caching, and it helps prevent cycles because a + * pack must not have a cycle in the delta chain. If both objects A and B + * are chosen out of the same source pack then there cannot be an A->B->A + * cycle. + * + * The oldest pack is also the most likely to have the smallest deltas. It + * generally is the biggest pack in the system and probably came from the + * clone (or last GC) of this repository, where all objects were previously + * considered and packed tightly together. If an object appears again (for + * example due to a revert and a push into this repository) the newer copy + * won't be nearly as small as the older delta version of it, even if the + * newer one is also itself a delta. + * + * Thus this method is optimized for being called in an order that presumes + * that earlier representations are better than later ones, and it expects + * representations from older pack files to be tested first, and it will + * shortcut any searching once it is satisfied with the selected + * representation. Perhaps ideally representation testing ordering should be + * based on packfile object count instead of age since file age can be + * altered, and be deceiving for other reasons. Perhaps the presence of a + * bitmap file for a pack file should prioritize it to be tested even + * earlier than object count? + * * @param otp * the object being packed. * @param next * the next available representation from the repository. + * @return whether the search should continue in the hopes of finding a + * better representation */ - public void select(ObjectToPack otp, StoredObjectRepresentation next) { + public boolean select(ObjectToPack otp, StoredObjectRepresentation next) { int nFmt = next.getFormat(); if (!cachedPacks.isEmpty()) { - if (otp.isEdge()) - return; + if (otp.isEdge()) { + return false; + } if (nFmt == PACK_WHOLE || nFmt == PACK_DELTA) { for (CachedPack pack : cachedPacks) { if (pack.hasObject(otp, next)) { @@ -2342,7 +2441,7 @@ public class PackWriter implements AutoCloseable { otp.clearDeltaBase(); otp.clearReuseAsIs(); pruneCurrentObjectList = true; - return; + return false; } } } @@ -2352,23 +2451,22 @@ public class PackWriter implements AutoCloseable { ObjectId baseId = next.getDeltaBase(); ObjectToPack ptr = objectsMap.get(baseId); if (ptr != null && !ptr.isEdge()) { - otp.setDeltaBase(ptr); - otp.setReuseAsIs(); - } else if (thin && have(ptr, baseId)) { - otp.setDeltaBase(baseId); - otp.setReuseAsIs(); - } else { - otp.clearDeltaBase(); - otp.clearReuseAsIs(); + return useDelta(otp, next, ptr); + } + if (thin && have(ptr, baseId)) { + return useDelta(otp, next, baseId); } + otp.clearDeltaBase(); + otp.clearReuseAsIs(); } else if (nFmt == PACK_WHOLE && config.isReuseObjects()) { int nWeight = next.getWeight(); if (otp.isReuseAsIs() && !otp.isDeltaRepresentation()) { // We've chosen another PACK_WHOLE format for this object, // choose the one that has the smaller compressed size. // - if (otp.getWeight() <= nWeight) - return; + if (otp.getWeight() < nWeight) { + return true; + } } otp.clearDeltaBase(); otp.setReuseAsIs(); @@ -2380,6 +2478,15 @@ public class PackWriter implements AutoCloseable { otp.setDeltaAttempted(reuseDeltas && next.wasDeltaAttempted()); otp.select(next); + return true; + } + + private boolean useDelta(ObjectToPack otp, StoredObjectRepresentation rep, ObjectId base) { + otp.setDeltaBase(base); + otp.setReuseAsIs(); + otp.setDeltaAttempted(reuseDeltas && rep.wasDeltaAttempted()); + otp.select(rep); + return false; } private final boolean have(ObjectToPack ptr, AnyObjectId objectId) { @@ -2394,11 +2501,12 @@ public class PackWriter implements AutoCloseable { * object graph at selected commits. Writing a bitmap index is an optional * feature that not all pack users may require. * <p> - * Called after {@link #writeIndex(OutputStream)}. + * Called after {@link #writeIndex(PackIndexWriter)}. * <p> * To reduce memory internal state is cleared during this method, rendering * the PackWriter instance useless for anything further than a call to write - * out the new bitmaps with {@link #writeBitmapIndex(OutputStream)}. + * out the new bitmaps with + * {@link #writeBitmapIndex(PackBitmapIndexWriter)}. * * @param pm * progress monitor to report bitmap building work. @@ -2552,17 +2660,29 @@ public class PackWriter implements AutoCloseable { this.bytesUsed = bytesUsed; } - /** @return the PackConfig used to build the writer. */ + /** + * Get pack config + * + * @return the PackConfig used to build the writer. + */ public PackConfig getConfig() { return config; } - /** @return the current phase of the writer. */ + /** + * Get packing phase + * + * @return the current phase of the writer. + */ public PackingPhase getPhase() { return phase; } - /** @return an estimate of the total memory used by the writer. */ + /** + * Get estimate of memory by the writer + * + * @return an estimate of the total memory used by the writer. + */ public long estimateBytesUsed() { return bytesUsed; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java index 5a18f1e567..bf87c4c9d6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java @@ -14,6 +14,7 @@ import static org.eclipse.jgit.internal.storage.file.PackBitmapIndex.FLAG_REUSE; import static org.eclipse.jgit.revwalk.RevFlag.SEEN; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -77,6 +78,7 @@ class PackWriterBitmapPreparer { private final int recentCommitSpan; private final int distantCommitSpan; private final int excessiveBranchCount; + private final int excessiveBranchTipCount; private final long inactiveBranchTimestamp; PackWriterBitmapPreparer(ObjectReader reader, @@ -96,10 +98,12 @@ class PackWriterBitmapPreparer { this.recentCommitSpan = config.getBitmapRecentCommitSpan(); this.distantCommitSpan = config.getBitmapDistantCommitSpan(); this.excessiveBranchCount = config.getBitmapExcessiveBranchCount(); - long now = SystemReader.getInstance().getCurrentTime(); - long ageInSeconds = config.getBitmapInactiveBranchAgeInDays() + this.excessiveBranchTipCount = Math.max(excessiveBranchCount, + config.getBitmapExcessiveBranchTipCount()); + Instant now = SystemReader.getInstance().now(); + long ageInSeconds = (long) config.getBitmapInactiveBranchAgeInDays() * DAY_IN_SECONDS; - this.inactiveBranchTimestamp = (now / 1000) - ageInSeconds; + this.inactiveBranchTimestamp = now.getEpochSecond() - ageInSeconds; } /** @@ -163,11 +167,17 @@ class PackWriterBitmapPreparer { rw2.setRetainBody(false); rw2.setRevFilter(new NotInBitmapFilter(seen)); + int newWantsCount = selectionHelper.newWantsByNewest.size(); + int maxBranches = Math.min(excessiveBranchTipCount, newWantsCount); + Set<RevCommit> excessiveBranches = new HashSet<>( + selectionHelper.newWantsByNewest.subList(maxBranches, + newWantsCount)); // For each branch, do a revwalk to enumerate its commits. Exclude // both reused commits and any commits seen in a previous branch. // Then iterate through all new commits from oldest to newest, // selecting well-spaced commits in this branch. - for (RevCommit rc : selectionHelper.newWantsByNewest) { + for (RevCommit rc : selectionHelper.newWantsByNewest.subList(0, + maxBranches)) { BitmapBuilder tipBitmap = commitBitmapIndex.newBitmapBuilder(); rw2.markStart((RevCommit) rw2.peel(rw2.parseAny(rc))); RevCommit rc2; @@ -223,7 +233,7 @@ class PackWriterBitmapPreparer { pm.update(1); // Always pick the items in wants, prefer merge commits. - if (selectionHelper.newWants.remove(c)) { + if (!excessiveBranches.contains(c) && selectionHelper.newWants.remove(c)) { if (nextIn > 0) { nextFlg = 0; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java index d07713db8e..e9ff02700d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java @@ -32,6 +32,8 @@ import static org.eclipse.jgit.lib.Ref.Storage.PACKED; import java.io.IOException; import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.zip.DataFormatException; import java.util.zip.Inflater; @@ -245,9 +247,9 @@ class BlockReader { private PersonIdent readPersonIdent() { String name = readValueString(); String email = readValueString(); - long ms = readVarint64() * 1000; - int tz = readInt16(); - return new PersonIdent(name, email, ms, tz); + long epochSeconds = readVarint64(); + ZoneOffset tz = ZoneOffset.ofTotalSeconds(readInt16() * 60); + return new PersonIdent(name, email, Instant.ofEpochSecond(epochSeconds), tz); } void readBlock(BlockSource src, long pos, int fileBlockSize) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockWriter.java index e3c0fc94aa..0ddfa5798a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockWriter.java @@ -92,7 +92,7 @@ class BlockWriter { } } - int cnt = (int) (Math.ceil(blockLimitBytes / avgBytesPerEntry)); + int cnt = (int) Math.ceil(blockLimitBytes / avgBytesPerEntry); return Math.min(cnt, 4096); } @@ -524,8 +524,8 @@ class BlockWriter { this.oldId = oldId; this.newId = newId; - this.timeSecs = who.getWhen().getTime() / 1000L; - this.tz = (short) who.getTimeZoneOffset(); + this.timeSecs = who.getWhenAsInstant().getEpochSecond(); + this.tz = (short) (who.getZoneOffset().getTotalSeconds() / 60); this.name = who.getName().getBytes(UTF_8); this.email = who.getEmailAddress().getBytes(UTF_8); this.msg = message.getBytes(UTF_8); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/EmptyLogCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/EmptyLogCursor.java index 7c06be876b..8700556d48 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/EmptyLogCursor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/EmptyLogCursor.java @@ -16,31 +16,26 @@ import org.eclipse.jgit.lib.ReflogEntry; /** Empty {@link LogCursor} with no results. */ class EmptyLogCursor extends LogCursor { - /** {@inheritDoc} */ @Override public boolean next() throws IOException { return false; } - /** {@inheritDoc} */ @Override public String getRefName() { return null; } - /** {@inheritDoc} */ @Override public long getUpdateIndex() { return 0; } - /** {@inheritDoc} */ @Override public ReflogEntry getReflogEntry() { return null; } - /** {@inheritDoc} */ @Override public void close() { // Do nothing. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/LogCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/LogCursor.java index f78975af34..f9c701ebe2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/LogCursor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/LogCursor.java @@ -51,7 +51,6 @@ public abstract class LogCursor implements AutoCloseable { @Nullable public abstract ReflogEntry getReflogEntry(); - /** {@inheritDoc} */ @Override public abstract void close(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java index e210acf058..aeaf241710 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java @@ -39,7 +39,6 @@ public class MergedReftable extends Reftable { /** * Initialize a merged table reader. - * <p> * * @param tableStack * stack of tables to read from. The base of the stack is at @@ -91,7 +90,6 @@ public class MergedReftable extends Reftable { return minUpdateIndex; } - /** {@inheritDoc} */ @Override public boolean hasObjectMap() throws IOException { boolean has = true; @@ -101,7 +99,6 @@ public class MergedReftable extends Reftable { return has; } - /** {@inheritDoc} */ @Override public RefCursor allRefs() throws IOException { MergedRefCursor m = new MergedRefCursor(); @@ -111,7 +108,6 @@ public class MergedReftable extends Reftable { return m; } - /** {@inheritDoc} */ @Override public RefCursor seekRef(String name) throws IOException { MergedRefCursor m = new MergedRefCursor(); @@ -121,7 +117,6 @@ public class MergedReftable extends Reftable { return m; } - /** {@inheritDoc} */ @Override public RefCursor seekRefsWithPrefix(String prefix) throws IOException { MergedRefCursor m = new MergedRefCursor(); @@ -131,7 +126,6 @@ public class MergedReftable extends Reftable { return m; } - /** {@inheritDoc} */ @Override public RefCursor byObjectId(AnyObjectId name) throws IOException { MergedRefCursor m = new FilteringMergedRefCursor(name); @@ -141,7 +135,6 @@ public class MergedReftable extends Reftable { return m; } - /** {@inheritDoc} */ @Override public LogCursor allLogs() throws IOException { MergedLogCursor m = new MergedLogCursor(); @@ -151,7 +144,6 @@ public class MergedReftable extends Reftable { return m; } - /** {@inheritDoc} */ @Override public LogCursor seekLog(String refName, long updateIdx) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/RefCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/RefCursor.java index 5e2c350883..6cd4f6c81d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/RefCursor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/RefCursor.java @@ -58,7 +58,6 @@ public abstract class RefCursor implements AutoCloseable { return r.getStorage() == Ref.Storage.NEW && r.getObjectId() == null; } - /** {@inheritDoc} */ @Override public abstract void close(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java index 63786dc17e..882e537734 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java @@ -138,8 +138,11 @@ public abstract class Reftable { public abstract RefCursor byObjectId(AnyObjectId id) throws IOException; /** - * @return whether this reftable can do a fast SHA1 => ref lookup. - * @throws IOException on I/O problems. + * Whether this reftable can do a fast SHA1 => ref lookup + * + * @return whether this reftable can do a fast SHA1 => ref lookup. + * @throws IOException + * on I/O problems. */ public abstract boolean hasObjectMap() throws IOException; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableBatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableBatchRefUpdate.java index dbc2ec7dc9..da1b056342 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableBatchRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableBatchRefUpdate.java @@ -64,7 +64,7 @@ public abstract class ReftableBatchRefUpdate extends BatchRefUpdate { /** * Initialize. * - * @param refdb + * @param refDb * The RefDatabase * @param reftableDb * The ReftableDatabase @@ -73,15 +73,14 @@ public abstract class ReftableBatchRefUpdate extends BatchRefUpdate { * @param repository * The repository on which this update will run */ - protected ReftableBatchRefUpdate(RefDatabase refdb, ReftableDatabase reftableDb, Lock lock, - Repository repository) { - super(refdb); + protected ReftableBatchRefUpdate(RefDatabase refDb, + ReftableDatabase reftableDb, Lock lock, Repository repository) { + super(refDb); this.refDb = reftableDb; this.lock = lock; this.repository = repository; } - /** {@inheritDoc} */ @Override public void execute(RevWalk rw, ProgressMonitor pm, List<String> options) { List<ReceiveCommand> pending = getPending(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableCompactor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableCompactor.java index 3c4bc75792..7e5f4ebbd4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableCompactor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableCompactor.java @@ -12,6 +12,7 @@ package org.eclipse.jgit.internal.storage.reftable; import java.io.IOException; import java.io.OutputStream; +import java.time.Instant; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; @@ -28,20 +29,26 @@ import org.eclipse.jgit.lib.ReflogEntry; * to shadow any lower reftable that may have the reference present. * <p> * By default all log entries within the range defined by - * {@link #setReflogExpireMinUpdateIndex(long)} and {@link #setReflogExpireMaxUpdateIndex(long)} are - * copied, even if no references in the output file match the log records. - * Callers may truncate the log to a more recent time horizon with - * {@link #setReflogExpireOldestReflogTimeMillis(long)}, or disable the log altogether with - * {@code setOldestReflogTimeMillis(Long.MAX_VALUE)}. + * {@link #setReflogExpireMinUpdateIndex(long)} and + * {@link #setReflogExpireMaxUpdateIndex(long)} are copied, even if no + * references in the output file match the log records. Callers may truncate the + * log to a more recent time horizon with + * {@link #setReflogExpireOlderThan(Instant)}, or disable the log + * altogether with {@code setReflogExpireOldestReflogTime(Instant.MAX)}. */ public class ReftableCompactor { private final ReftableWriter writer; + private final ArrayDeque<ReftableReader> tables = new ArrayDeque<>(); private boolean includeDeletes; + private long reflogExpireMinUpdateIndex = 0; + private long reflogExpireMaxUpdateIndex = Long.MAX_VALUE; - private long reflogExpireOldestReflogTimeMillis; + + private Instant reflogExpireOldestReflogTime = Instant.EPOCH; + private Stats stats; /** @@ -122,9 +129,29 @@ public class ReftableCompactor { * entries that predate {@code timeMillis} will be discarded. * Specified in Java standard milliseconds since the epoch. * @return {@code this} + * + * @deprecated Use {@link #setReflogExpireOlderThan(Instant)} instead + */ + @Deprecated(since="7.3") + public ReftableCompactor setReflogExpireOldestReflogTimeMillis( + long timeMillis) { + return setReflogExpireOlderThan(timeMillis == Long.MAX_VALUE + ? Instant.MAX + : Instant.ofEpochMilli(timeMillis)); + } + + /** + * Set oldest reflog time to preserve. + * + * @param cutTime + * oldest log time to preserve. Entries whose timestamps are + * {@code >= cutTime} will be copied into the output file. Log + * entries that predate {@code cutTime} will be discarded. + * @return {@code this} */ - public ReftableCompactor setReflogExpireOldestReflogTimeMillis(long timeMillis) { - reflogExpireOldestReflogTimeMillis = timeMillis; + public ReftableCompactor setReflogExpireOlderThan( + Instant cutTime) { + reflogExpireOldestReflogTime = cutTime; return this; } @@ -182,14 +209,15 @@ public class ReftableCompactor { } private void mergeLogs(MergedReftable mr) throws IOException { - if (reflogExpireOldestReflogTimeMillis == Long.MAX_VALUE) { + if (reflogExpireOldestReflogTime == Instant.MAX) { return; } try (LogCursor lc = mr.allLogs()) { while (lc.next()) { long updateIndex = lc.getUpdateIndex(); - if (updateIndex > reflogExpireMaxUpdateIndex || updateIndex < reflogExpireMinUpdateIndex) { + if (updateIndex > reflogExpireMaxUpdateIndex + || updateIndex < reflogExpireMinUpdateIndex) { continue; } @@ -203,14 +231,9 @@ public class ReftableCompactor { } PersonIdent who = log.getWho(); - if (who.getWhen().getTime() >= reflogExpireOldestReflogTimeMillis) { - writer.writeLog( - refName, - updateIndex, - who, - log.getOldId(), - log.getNewId(), - log.getComment()); + if (who.getWhenAsInstant().compareTo(reflogExpireOldestReflogTime) >= 0) { + writer.writeLog(refName, updateIndex, who, log.getOldId(), + log.getNewId(), log.getComment()); } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java index 0c16828617..bd4e2d6763 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java @@ -52,6 +52,8 @@ public abstract class ReftableDatabase { protected abstract MergedReftable openMergedReftable() throws IOException; /** + * Get next update index + * * @return the next available logical timestamp for an additional reftable * in the stack. * @throws java.io.IOException @@ -67,6 +69,8 @@ public abstract class ReftableDatabase { } /** + * Get ReflogReader + * * @return a ReflogReader for the given ref * @param refname * the name of the ref. @@ -83,6 +87,8 @@ public abstract class ReftableDatabase { } /** + * Create a ReceiveCommand for the change from oldRef to newRef + * * @return a ReceiveCommand for the change from oldRef to newRef * @param oldRef * a ref @@ -136,6 +142,8 @@ public abstract class ReftableDatabase { } /** + * Get lock + * * @return the lock protecting underlying ReftableReaders against concurrent * reads. */ @@ -144,6 +152,8 @@ public abstract class ReftableDatabase { } /** + * Get reader for merged reftable + * * @return the merged reftable that is implemented by the stack of * reftables. Return value must be accessed under lock. * @throws IOException @@ -161,6 +171,8 @@ public abstract class ReftableDatabase { } /** + * Whether refName is conflicting + * * @return whether the given refName would be illegal in a repository that * uses loose refs. * @param refName @@ -268,14 +280,17 @@ public abstract class ReftableDatabase { } /** - * Returns refs whose names start with a given prefix excluding all refs that - * start with one of the given prefixes. + * Returns refs whose names start with a given prefix excluding all refs + * that start with one of the given prefixes. * - * @param include string that names of refs should start with; may be empty. - * @param excludes strings that names of refs can't start with; may be empty. + * @param include + * string that names of refs should start with; may be empty. + * @param excludes + * strings that names of refs can't start with; may be empty. * @return immutable list of refs whose names start with {@code include} and - * none of the strings in {@code exclude}. - * @throws java.io.IOException the reference space cannot be accessed. + * none of the strings in {@code excludes}. + * @throws java.io.IOException + * the reference space cannot be accessed. */ public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes) throws IOException { if (excludes.isEmpty()) { @@ -316,8 +331,11 @@ public abstract class ReftableDatabase { } /** + * Whether there is a fast SHA1 to ref map + * * @return whether there is a fast SHA1 to ref map. - * @throws IOException in case of I/O problems. + * @throws IOException + * in case of I/O problems. */ public boolean hasFastTipsWithSha1() throws IOException { lock.lock(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableOutputStream.java index aa3aba516f..4aea3e684e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableOutputStream.java @@ -59,14 +59,12 @@ class ReftableOutputStream extends OutputStream { blockSize = bs; } - /** {@inheritDoc} */ @Override public void write(int b) { ensureBytesAvailableInBlockBuf(1); blockBuf[cur++] = (byte) b; } - /** {@inheritDoc} */ @Override public void write(byte[] b, int off, int cnt) { ensureBytesAvailableInBlockBuf(cnt); @@ -86,7 +84,11 @@ class ReftableOutputStream extends OutputStream { return paddingUsed; } - /** @return bytes flushed; excludes {@link #bytesWrittenInBlock()}. */ + /** + * Get size + * + * @return bytes flushed; excludes {@link #bytesWrittenInBlock()}. + */ long size() { return out.getCount(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java index cabb2e1842..2a73efd962 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java @@ -127,7 +127,6 @@ public class ReftableReader extends Reftable implements AutoCloseable { return maxUpdateIndex; } - /** {@inheritDoc} */ @Override public RefCursor allRefs() throws IOException { if (blockSize == -1) { @@ -144,7 +143,6 @@ public class ReftableReader extends Reftable implements AutoCloseable { return i; } - /** {@inheritDoc} */ @Override public RefCursor seekRef(String refName) throws IOException { initRefIndex(); @@ -155,7 +153,6 @@ public class ReftableReader extends Reftable implements AutoCloseable { return i; } - /** {@inheritDoc} */ @Override public RefCursor seekRefsWithPrefix(String prefix) throws IOException { initRefIndex(); @@ -166,7 +163,6 @@ public class ReftableReader extends Reftable implements AutoCloseable { return i; } - /** {@inheritDoc} */ @Override public RefCursor byObjectId(AnyObjectId id) throws IOException { initObjIndex(); @@ -179,7 +175,6 @@ public class ReftableReader extends Reftable implements AutoCloseable { return i; } - /** {@inheritDoc} */ @Override public LogCursor allLogs() throws IOException { initLogIndex(); @@ -192,7 +187,6 @@ public class ReftableReader extends Reftable implements AutoCloseable { return new EmptyLogCursor(); } - /** {@inheritDoc} */ @Override public LogCursor seekLog(String refName, long updateIndex) throws IOException { @@ -460,7 +454,6 @@ public class ReftableReader extends Reftable implements AutoCloseable { return src.size(); } - /** {@inheritDoc} */ @Override public void close() throws IOException { src.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReflogReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReflogReader.java index f9f1def958..597303301a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReflogReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReflogReader.java @@ -34,7 +34,6 @@ public class ReftableReflogReader implements ReflogReader { this.refname = refname; } - /** {@inheritDoc} */ @Override public ReflogEntry getLastEntry() throws IOException { lock.lock(); @@ -46,13 +45,11 @@ public class ReftableReflogReader implements ReflogReader { } } - /** {@inheritDoc} */ @Override public List<ReflogEntry> getReverseEntries() throws IOException { return getReverseEntries(Integer.MAX_VALUE); } - /** {@inheritDoc} */ @Override public ReflogEntry getReverseEntry(int number) throws IOException { lock.lock(); @@ -72,7 +69,6 @@ public class ReftableReflogReader implements ReflogReader { } } - /** {@inheritDoc} */ @Override public List<ReflogEntry> getReverseEntries(int max) throws IOException { lock.lock(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableWriter.java index f4df0b28f7..b47f58e1a5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableWriter.java @@ -565,92 +565,163 @@ public class ReftableWriter { objIndexLevels = objIdx != null ? objIdx.levels : 0; } - /** @return number of bytes in a ref block. */ + /** + * Get ref block size + * + * @return number of bytes in a ref block. + */ public int refBlockSize() { return refBlockSize; } - /** @return number of bytes to compress into a log block. */ + /** + * Get log block size + * + * @return number of bytes to compress into a log block. + */ public int logBlockSize() { return logBlockSize; } - /** @return number of references between binary search markers. */ + /** + * Get restart interval + * + * @return number of references between binary search markers. + */ public int restartInterval() { return restartInterval; } - /** @return smallest update index contained in this reftable. */ + /** + * Get smallest update index + * + * @return smallest update index contained in this reftable. + */ public long minUpdateIndex() { return minUpdateIndex; } - /** @return largest update index contained in this reftable. */ + /** + * Get largest updated index + * + * @return largest update index contained in this reftable. + */ public long maxUpdateIndex() { return maxUpdateIndex; } - /** @return total number of references in the reftable. */ + /** + * Get number of refs + * + * @return total number of references in the reftable. + */ public long refCount() { return refCnt; } - /** @return number of unique objects in the reftable. */ + /** + * Get number of unique objects + * + * @return number of unique objects in the reftable. + */ public long objCount() { return objCnt; } - /** @return total number of log records in the reftable. */ + /** + * Get number of log records + * + * @return total number of log records in the reftable. + */ public long logCount() { return logCnt; } - /** @return number of bytes for references, including ref index. */ + /** + * Get number of bytes for references + * + * @return number of bytes for references, including ref index. + */ public long refBytes() { return refBytes; } - /** @return number of bytes for objects, including object index. */ + /** + * Get number of bytes for objects + * + * @return number of bytes for objects, including object index. + */ public long objBytes() { return objBytes; } - /** @return number of bytes for log, including log index. */ + /** + * Get number of bytes for log + * + * @return number of bytes for log, including log index. + */ public long logBytes() { return logBytes; } - /** @return total number of bytes in the reftable. */ + /** + * Get total number of bytes + * + * @return total number of bytes in the reftable. + */ public long totalBytes() { return totalBytes; } - /** @return bytes of padding used to maintain block alignment. */ + /** + * Get number of padding bytes + * + * @return bytes of padding used to maintain block alignment. + */ public long paddingBytes() { return paddingUsed; } - /** @return number of bytes in the ref index; 0 if no index was used. */ + /** + * Get size of ref index + * + * @return number of bytes in the ref index; 0 if no index was used. + */ public int refIndexSize() { return refIndexSize; } - /** @return number of levels in the ref index. */ + /** + * Get number of ref index levels + * + * @return number of levels in the ref index. + */ public int refIndexLevels() { return refIndexLevels; } - /** @return number of bytes in the object index; 0 if no index. */ + /** + * Get size of object index + * + * @return number of bytes in the object index; 0 if no index. + */ public int objIndexSize() { return objIndexSize; } - /** @return number of levels in the object index. */ + /** + * Get number of levels in object index + * + * @return number of levels in the object index. + */ public int objIndexLevels() { return objIndexLevels; } /** + * Get number of bytes required to uniquely identify all objects in the + * reftable + * * @return number of bytes required to uniquely identify all objects in * the reftable. Unique abbreviations in hex would be * {@code 2 * objIdLength()}. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/submodule/SubmoduleValidator.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/submodule/SubmoduleValidator.java index d0e24413bb..a4740c9bf6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/submodule/SubmoduleValidator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/submodule/SubmoduleValidator.java @@ -45,8 +45,8 @@ public class SubmoduleValidator { * @param message * Description of the problem * @param fsckMessageId - * Error identifier, following the git fsck fsck.<msg-id> - * format + * Error identifier, following the git fsck + * fsck.<msg-id> format */ SubmoduleValidationException(String message, ObjectChecker.ErrorType fsckMessageId) { @@ -56,6 +56,8 @@ public class SubmoduleValidator { /** + * Get the error identifier + * * @return the error identifier */ public ObjectChecker.ErrorType getFsckMessageId() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java index 69a8273158..930bcea75b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java @@ -39,6 +39,8 @@ public class NetscapeCookieFileCache { } /** + * Get singleton instance + * * @param config * the config which defines the limit for this cache * @return the singleton instance of the cookie file cache. If the cache has @@ -54,6 +56,8 @@ public class NetscapeCookieFileCache { } /** + * Get a cache entry + * * @param path * the path of the cookie file to retrieve * @return the cache entry belonging to the requested file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstCommand.java index c75cf5d618..a196fbe418 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstCommand.java @@ -69,13 +69,21 @@ public final class FirstCommand { this.capabilities = capabilities; } - /** @return non-capabilities part of the line. */ + /** + * Get line + * + * @return non-capabilities part of the line. + */ @NonNull public String getLine() { return line; } - /** @return capabilities parsed from the line, as an immutable map. */ + /** + * Get capabilities + * + * @return capabilities parsed from the line, as an immutable map. + */ @NonNull public Map<String, String> getCapabilities() { return capabilities; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstWant.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstWant.java index 30d629665f..e214cdae8b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstWant.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstWant.java @@ -102,12 +102,18 @@ public class FirstWant { this.clientSID = clientSID; } - /** @return non-capabilities part of the line. */ + /** + * Get line + * + * @return non-capabilities part of the line. + */ public String getLine() { return line; } /** + * Get capabilities + * * @return capabilities parsed from the line as an immutable set (excluding * agent and session-id). */ @@ -115,13 +121,21 @@ public class FirstWant { return capabilities; } - /** @return client user agent parsed from the line. */ + /** + * Get agent + * + * @return client user agent parsed from the line. + */ @Nullable public String getAgent() { return agent; } - /** @return client session-id parsed from the line. */ + /** + * Get client session-id + * + * @return client session-id parsed from the line. + */ @Nullable public String getClientSID() { return clientSID; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java index 659ccb8c55..542d6e94f3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -100,7 +99,7 @@ public class OpenSshConfigFile implements SshConfigStore { * fully resolved entries created from that. */ private static class State { - List<HostEntry> entries = new LinkedList<>(); + List<HostEntry> entries = new ArrayList<>(); // Previous lookups, keyed by user@hostname:port Map<String, HostEntry> hosts = new HashMap<>(); @@ -218,7 +217,7 @@ public class OpenSshConfigFile implements SshConfigStore { private List<HostEntry> parse(BufferedReader reader) throws IOException { - final List<HostEntry> entries = new LinkedList<>(); + final List<HostEntry> entries = new ArrayList<>(); // The man page doesn't say so, but the openssh parser (readconf.c) // starts out in active mode and thus always applies any lines that @@ -428,7 +427,35 @@ public class OpenSshConfigFile implements SshConfigStore { return value; } - private static boolean patternMatchesHost(String pattern, String name) { + /** + * Tells whether a given {@code name} matches the given list of patterns, + * accounting for negative matches. + * + * @param patterns + * to test {@code name} against; any pattern starting with an + * exclamation mark is a negative pattern + * @param name + * to test + * @return {@code true} if the {@code name} matches at least one of the + * non-negative patterns and none of the negative patterns, + * {@code false} otherwise + * @since 7.1 + */ + public static boolean patternMatch(Iterable<String> patterns, String name) { + boolean doesMatch = false; + for (String pattern : patterns) { + if (pattern.startsWith("!")) { //$NON-NLS-1$ + if (patternMatches(pattern.substring(1), name)) { + return false; + } + } else if (!doesMatch && patternMatches(pattern, name)) { + doesMatch = true; + } + } + return doesMatch; + } + + private static boolean patternMatches(String pattern, String name) { if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) { final FileNameMatcher fn; try { @@ -681,18 +708,7 @@ public class OpenSshConfigFile implements SshConfigStore { } boolean matches(String hostName) { - boolean doesMatch = false; - for (String pattern : patterns) { - if (pattern.startsWith("!")) { //$NON-NLS-1$ - if (patternMatchesHost(pattern.substring(1), hostName)) { - return false; - } - } else if (!doesMatch - && patternMatchesHost(pattern, hostName)) { - doesMatch = true; - } - } - return doesMatch; + return patternMatch(patterns, hostName); } private static String toKey(String key) { @@ -1154,7 +1170,6 @@ public class OpenSshConfigFile implements SshConfigStore { } } - /** {@inheritDoc} */ @Override @SuppressWarnings("nls") public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/CleanupService.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/CleanupService.java new file mode 100644 index 0000000000..29ed7564d3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/CleanupService.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.util; + +import org.eclipse.jgit.internal.JGitText; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A class that is registered as an OSGi service via the manifest. If JGit runs + * in OSGi, OSGi will instantiate a singleton as soon as the bundle is activated + * since this class is an immediate OSGi component with no dependencies. OSGi + * will then call its {@link #start()} method. If JGit is not running in OSGi, + * {@link #getInstance()} will lazily create an instance. + * <p> + * An OSGi-created {@link CleanupService} will run the registered cleanup when + * the {@code org.eclipse.jgit} bundle is deactivated. A lazily created instance + * will register the cleanup as a JVM shutdown hook. + * </p> + */ +public final class CleanupService { + + private static final Logger LOG = LoggerFactory + .getLogger(CleanupService.class); + + private static final Object LOCK = new Object(); + + private static CleanupService INSTANCE; + + private final boolean isOsgi; + + private JGitText jgitText; + + private Runnable cleanup; + + /** + * Public component constructor for OSGi DS. Do <em>not</em> call this + * explicitly! (Unfortunately this constructor must be public because of + * OSGi requirements.) + */ + public CleanupService() { + this.isOsgi = true; + setInstance(this); + } + + private CleanupService(boolean isOsgi) { + this.isOsgi = isOsgi; + } + + private static void setInstance(CleanupService service) { + synchronized (LOCK) { + INSTANCE = service; + } + } + + /** + * Obtains the singleton instance of the {@link CleanupService} that knows + * whether or not it is running on OSGi. + * + * @return the {@link CleanupService} singleton instance + */ + public static CleanupService getInstance() { + synchronized (LOCK) { + if (INSTANCE == null) { + INSTANCE = new CleanupService(false); + } + return INSTANCE; + } + } + + void start() { + // Nothing to do + } + + void register(Runnable cleanUp) { + if (isOsgi) { + cleanup = cleanUp; + } else { + // Ensure the JGitText class is loaded. Depending on the framework + // JGit runs in, it may not be possible anymore to load classes when + // the hook runs. For instance when run in a maven plug-in: the + // Plexus class world that loaded JGit may already have been + // disposed by the time the JVM shutdown hook runs when the whole + // maven build terminates. + jgitText = JGitText.get(); + assert jgitText != null; + try { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + cleanUp.run(); + // Don't catch exceptions; let the JVM do the problem + // reporting. + } finally { + jgitText = null; + } + })); + } catch (IllegalStateException e) { + // Ignore -- the JVM is already shutting down. + } + } + } + + void shutDown() { + if (isOsgi && cleanup != null) { + Runnable r = cleanup; + cleanup = null; + try { + r.run(); + } catch (RuntimeException e) { + LOG.error(JGitText.get().shutdownCleanupFailed, e); + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java new file mode 100644 index 0000000000..270b760562 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023 Qualcomm Innovation Center, 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 v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.util; + +import java.lang.ref.SoftReference; +import java.util.Optional; + +/** + * Interface representing a reference to a potentially mutable optional object. + * + * @param <T> + * type of the mutable optional object + * + * @since 6.7 + */ +public interface Optionally<T> { + /** + * A permanently empty Optionally + * + * @param <T> + * type of the mutable optional object + * + */ + public class Empty<T> implements Optionally<T> { + @Override + public void clear() { + // empty + } + + @Override + public Optional<T> getOptional() { + return Optional.empty(); + } + } + + /** + * A permanent(hard) reference to an object + * + * @param <T> + * type of the mutable optional object + * + */ + public class Hard<T> implements Optionally<T> { + /** + * The mutable optional object + */ + protected Optional<T> optional; + + /** + * @param element + * the mutable optional object + */ + public Hard(T element) { + optional = Optional.ofNullable(element); + } + + @Override + public void clear() { + optional = Optional.empty(); + } + + @Override + public Optional<T> getOptional() { + return optional; + } + } + + /** + * A SoftReference Optionally + * + * @param <T> + * type of the mutable optional object + * + */ + public class Soft<T> extends SoftReference<T> implements Optionally<T> { + /** + * @param t + * the mutable optional object + */ + public Soft(T t) { + super(t); + } + + @Override + public Optional<T> getOptional() { + return Optional.ofNullable(get()); + } + } + + /** + * The empty Optionally + */ + public static final Optionally<?> EMPTY = new Empty<>(); + + /** + * Get empty Optionally + * + * @param <T> + * type of the empty Optionally + * @return the empty Optionally + */ + @SuppressWarnings("unchecked") + public static <T> Optionally<T> empty() { + return (Optionally<T>) EMPTY; + } + + /** + * Clear the object + */ + void clear(); + + /** + * Get an Optional representing the current state of the object + * + * @return the mutable optional object + */ + Optional<T> getOptional(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java new file mode 100644 index 0000000000..f6b4723489 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2023, SAP SE and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.util; + +import java.text.MessageFormat; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jgit.internal.JGitText; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The singleton {@link ShutdownHook} provides a means to register + * {@link Listener}s that are run when JGit is uninstalled, either + * <ul> + * <li>in an OSGi framework when this bundle is deactivated, or</li> + * <li>otherwise, when the JVM as a whole shuts down.</li> + * </ul> + */ +@SuppressWarnings("ImmutableEnumChecker") +public enum ShutdownHook { + /** + * Singleton + */ + INSTANCE; + + /** + * Object that needs to cleanup on shutdown. + */ + public interface Listener { + /** + * Cleanup resources when JGit is shut down. + * <p> + * Implementations should be coded defensively + * <ul> + * <li>they should finish their work quickly + * <li>they should be written to be thread-safe and to avoid deadlocks + * insofar as possible + * <li>they should not rely blindly upon services that may have + * registered their own shutdown hooks and therefore may themselves be + * in the process of shutting down + * <li>attempts to use other thread-based services may lead to + * deadlocks. + * </ul> + * See {@link Runtime#addShutdownHook} for more details. + */ + public void onShutdown(); + } + + private static final Logger LOG = LoggerFactory + .getLogger(ShutdownHook.class); + + private final Set<Listener> listeners = ConcurrentHashMap.newKeySet(); + + private final AtomicBoolean shutdownInProgress = new AtomicBoolean(); + + private ShutdownHook() { + CleanupService.getInstance().register(this::cleanup); + } + + private void cleanup() { + if (!shutdownInProgress.getAndSet(true)) { + ExecutorService runner = Executors.newWorkStealingPool(); + try { + runner.submit(() -> { + this.doCleanup(); + return null; + }).get(30L, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException + | TimeoutException e) { + throw new RuntimeException(e.getMessage(), e); + } finally { + runner.shutdownNow(); + } + } + } + + private void doCleanup() { + listeners.parallelStream().forEach(this::notify); + } + + private void notify(Listener l) { + LOG.debug(JGitText.get().shutdownCleanup, l); + try { + l.onShutdown(); + } catch (RuntimeException e) { + LOG.error(MessageFormat.format( + JGitText.get().shutdownCleanupListenerFailed, l), e); + } + } + + /** + * Register object that needs cleanup during JGit shutdown if it is not + * already registered. Registration is disabled when JGit shutdown is + * already in progress. + * + * @param l + * the object to call {@link Listener#onShutdown} on when JGit + * shuts down + * @return {@code true} if this object has been registered + */ + public boolean register(Listener l) { + if (shutdownInProgress.get()) { + return listeners.contains(l); + } + LOG.debug("register {} with shutdown hook", l); //$NON-NLS-1$ + listeners.add(l); + return true; + } + + /** + * Unregister object that no longer needs cleanup during JGit shutdown if it + * is still registered. Unregistration is disabled when JGit shutdown is + * already in progress. + * + * @param l + * the object registered to be notified for cleanup when the JVM + * shuts down + * @return {@code true} if this object is no longer registered + */ + public boolean unregister(Listener l) { + if (shutdownInProgress.get()) { + return !listeners.contains(l); + } + LOG.debug("unregister {} from shutdown hook", l); //$NON-NLS-1$ + listeners.remove(l); + return true; + } + + /** + * Whether a JGit shutdown is in progress + * + * @return {@code true} if a JGit shutdown is in progress + */ + public boolean isShutdownInProgress() { + return shutdownInProgress.get(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java index 9109cfd769..c3dffdfe79 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java @@ -19,7 +19,7 @@ import org.eclipse.jgit.internal.JGitText; /** * Git configuration option <a - * href=https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreabbrev"> + * href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreabbrev"> * core.abbrev</a> * * @since 6.1 @@ -100,6 +100,7 @@ public final class AbbrevConfig { * object names to stay unique for some time. * * @param repo + * the repository the AbbrevConfig shall be computed for * @return appropriate value computed based on the approximate number of * packed objects in a repository */ @@ -128,7 +129,10 @@ public final class AbbrevConfig { private int abbrev; /** + * Create an {@code AbbrevConfig} + * * @param abbrev + * abbreviation length */ private AbbrevConfig(int abbrev) { this.abbrev = capAbbrev(abbrev); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java index dc5e5cc20f..a13136b9f1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java @@ -321,13 +321,11 @@ public final class AbbreviatedObjectId implements Serializable { return mask(nibbles, word, v); } - /** {@inheritDoc} */ @Override public int hashCode() { return w1; } - /** {@inheritDoc} */ @Override public boolean equals(Object o) { if (o instanceof AbbreviatedObjectId) { @@ -366,7 +364,6 @@ public final class AbbreviatedObjectId implements Serializable { return new String(b, 0, nibbles); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java index 7685b30f8e..f742e993a0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java @@ -35,23 +35,6 @@ public abstract class AnyObjectId implements Comparable<AnyObjectId> { * @param secondObjectId * the second identifier to compare. Must not be null. * @return true if the two identifiers are the same. - * @deprecated use {@link #isEqual(AnyObjectId, AnyObjectId)} instead - */ - @Deprecated - @SuppressWarnings("AmbiguousMethodReference") - public static boolean equals(final AnyObjectId firstObjectId, - final AnyObjectId secondObjectId) { - return isEqual(firstObjectId, secondObjectId); - } - - /** - * Compare two 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. * @since 5.4 */ public static boolean isEqual(final AnyObjectId firstObjectId, @@ -248,7 +231,6 @@ public abstract class AnyObjectId implements Comparable<AnyObjectId> { return abbr.prefixCompare(this) == 0; } - /** {@inheritDoc} */ @Override public final int hashCode() { return w2; @@ -266,7 +248,6 @@ public abstract class AnyObjectId implements Comparable<AnyObjectId> { return other != null ? isEqual(this, other) : false; } - /** {@inheritDoc} */ @Override public final boolean equals(Object o) { if (o instanceof AnyObjectId) { @@ -477,7 +458,6 @@ public abstract class AnyObjectId implements Comparable<AnyObjectId> { dst[o--] = '0'; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java index b2242a11ca..0c1da83dfb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java @@ -13,19 +13,23 @@ package org.eclipse.jgit.lib; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BARE; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WORKTREE; +import static org.eclipse.jgit.lib.Constants.CONFIG; import static org.eclipse.jgit.lib.Constants.DOT_GIT; +import static org.eclipse.jgit.lib.Constants.GITDIR_FILE; import static org.eclipse.jgit.lib.Constants.GIT_ALTERNATE_OBJECT_DIRECTORIES_KEY; import static org.eclipse.jgit.lib.Constants.GIT_CEILING_DIRECTORIES_KEY; +import static org.eclipse.jgit.lib.Constants.GIT_COMMON_DIR_KEY; import static org.eclipse.jgit.lib.Constants.GIT_DIR_KEY; import static org.eclipse.jgit.lib.Constants.GIT_INDEX_FILE_KEY; import static org.eclipse.jgit.lib.Constants.GIT_OBJECT_DIRECTORY_KEY; import static org.eclipse.jgit.lib.Constants.GIT_WORK_TREE_KEY; +import static org.eclipse.jgit.lib.Constants.OBJECTS; import java.io.File; import java.io.IOException; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import org.eclipse.jgit.annotations.NonNull; @@ -70,7 +74,21 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re && ref[7] == ' '; } - private static File getSymRef(File workTree, File dotGit, FS fs) + /** + * Read symbolic reference file + * + * @param workTree + * the work tree path + * @param dotGit + * the .git file + * @param fs + * th FS util + * @return the file read from symbolic reference file + * @throws java.io.IOException + * the dotGit file is invalid reference + * @since 7.0 + */ + static File getSymRef(File workTree, File dotGit, FS fs) throws IOException { byte[] content = IO.readFully(dotGit); if (!isSymRef(content)) { @@ -102,6 +120,8 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re private File gitDir; + private File gitCommonDir; + private File objectDirectory; private List<File> alternateObjectDirectories; @@ -172,6 +192,30 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re } /** + * Set common dir. + * + * @param gitCommonDir + * {@code GIT_COMMON_DIR}, the common repository meta directory. + * @return {@code this} (for chaining calls). + * @since 7.0 + */ + public B setGitCommonDir(File gitCommonDir) { + this.gitCommonDir = gitCommonDir; + this.config = null; + return self(); + } + + /** + * Get common dir. + * + * @return common dir; null if not set. + * @since 7.0 + */ + public File getGitCommonDir() { + return gitCommonDir; + } + + /** * Set the directory storing the repository's objects. * * @param objectDirectory @@ -205,8 +249,9 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re */ public B addAlternateObjectDirectory(File other) { if (other != null) { - if (alternateObjectDirectories == null) - alternateObjectDirectories = new LinkedList<>(); + if (alternateObjectDirectories == null) { + alternateObjectDirectories = new ArrayList<>(); + } alternateObjectDirectories.add(other); } return self(); @@ -395,9 +440,9 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re * Read standard Git environment variables and configure from those. * <p> * This method tries to read the standard Git environment variables, such as - * {@code GIT_DIR} and {@code GIT_WORK_TREE} to configure this builder - * instance. If an environment variable is set, it overrides the value - * already set in this builder. + * {@code GIT_DIR}, {@code GIT_COMMON_DIR}, {@code GIT_WORK_TREE} etc. to + * configure this builder instance. If an environment variable is set, it + * overrides the value already set in this builder. * * @return {@code this} (for chaining calls). */ @@ -409,9 +454,9 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re * Read standard Git environment variables and configure from those. * <p> * This method tries to read the standard Git environment variables, such as - * {@code GIT_DIR} and {@code GIT_WORK_TREE} to configure this builder - * instance. If a property is already set in the builder, the environment - * variable is not used. + * {@code GIT_DIR}, {@code GIT_COMMON_DIR}, {@code GIT_WORK_TREE} etc. to + * configure this builder instance. If a property is already set in the + * builder, the environment variable is not used. * * @param sr * the SystemReader abstraction to access the environment. @@ -424,6 +469,13 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re setGitDir(new File(val)); } + if (getGitCommonDir() == null) { + String val = sr.getenv(GIT_COMMON_DIR_KEY); + if (val != null) { + setGitCommonDir(new File(val)); + } + } + if (getObjectDirectory() == null) { String val = sr.getenv(GIT_OBJECT_DIRECTORY_KEY); if (val != null) @@ -433,7 +485,7 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re if (getAlternateObjectDirectories() == null) { String val = sr.getenv(GIT_ALTERNATE_OBJECT_DIRECTORIES_KEY); if (val != null) { - for (String path : val.split(File.pathSeparator)) + for (String path : val.split(File.pathSeparator, -1)) addAlternateObjectDirectory(new File(path)); } } @@ -453,7 +505,7 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re if (ceilingDirectories == null) { String val = sr.getenv(GIT_CEILING_DIRECTORIES_KEY); if (val != null) { - for (String path : val.split(File.pathSeparator)) + for (String path : val.split(File.pathSeparator, -1)) addCeilingDirectory(new File(path)); } } @@ -473,8 +525,9 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re */ public B addCeilingDirectory(File root) { if (root != null) { - if (ceilingDirectories == null) - ceilingDirectories = new LinkedList<>(); + if (ceilingDirectories == null) { + ceilingDirectories = new ArrayList<>(); + } ceilingDirectories.add(root); } return self(); @@ -599,6 +652,7 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re public B setup() throws IllegalArgumentException, IOException { requireGitDirOrWorkTree(); setupGitDir(); + setupCommonDir(); setupWorkTree(); setupInternals(); return self(); @@ -656,6 +710,20 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re } /** + * Perform standard common dir initialization. + * + * @throws java.io.IOException + * the repository could not be accessed + * @since 7.0 + */ + protected void setupCommonDir() throws IOException { + // no gitCommonDir? Try to get it from gitDir + if (getGitCommonDir() == null) { + setGitCommonDir(safeFS().getCommonDir(getGitDir())); + } + } + + /** * Perform standard work-tree initialization. * <p> * This is a method typically invoked inside of {@link #setup()}, near the @@ -693,8 +761,12 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re * the repository could not be accessed */ protected void setupInternals() throws IOException { - if (getObjectDirectory() == null && getGitDir() != null) - setObjectDirectory(safeFS().resolve(getGitDir(), Constants.OBJECTS)); + if (getObjectDirectory() == null) { + File commonDir = getGitCommonDir(); + if (commonDir != null) { + setObjectDirectory(safeFS().resolve(commonDir, OBJECTS)); + } + } } /** @@ -721,12 +793,13 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re * the configuration is not available. */ protected Config loadConfig() throws IOException { - if (getGitDir() != null) { + File commonDir = getGitCommonDir(); + if (commonDir != null) { // We only want the repository's configuration file, and not // the user file, as these parameters must be unique to this // repository and not inherited from other files. // - File path = safeFS().resolve(getGitDir(), Constants.CONFIG); + File path = safeFS().resolve(commonDir, CONFIG); FileBasedConfig cfg = new FileBasedConfig(path, safeFS()); try { cfg.load(); @@ -747,8 +820,29 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re // String path = cfg.getString(CONFIG_CORE_SECTION, null, CONFIG_KEY_WORKTREE); - if (path != null) + if (path != null) { return safeFS().resolve(getGitDir(), path).getCanonicalFile(); + } + + /* + * We are in worktree's $GIT_DIR folder + * ".git/worktrees/<worktree-name>" and want to get the working + * tree (checkout) path; so here we have an opposite link in file + * "gitdir" showing to the ".git" file located in the working tree read + * it and convert it to absolute path if it's relative + */ + File gitDirFile = new File(getGitDir(), GITDIR_FILE); + if (gitDirFile.isFile()) { + String workDirPath = new String(IO.readFully(gitDirFile)).trim(); + File workTreeDotGitFile = new File(workDirPath); + if (!workTreeDotGitFile.isAbsolute()) { + workTreeDotGitFile = new File(getGitDir(), workDirPath) + .getCanonicalFile(); + } + if (workTreeDotGitFile != null) { + return workTreeDotGitFile.getParentFile(); + } + } // If core.bare is set, honor its value. Assume workTree is // the parent directory of the repository. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java index d2367cc3d5..f9952501c3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java @@ -716,7 +716,6 @@ public class BatchRefUpdate { : isForceRefLog(); } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder r = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java index f826057370..8e92f20cb0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java @@ -47,13 +47,11 @@ public abstract class BatchingProgressMonitor implements ProgressMonitor { delayStartUnit = unit; } - /** {@inheritDoc} */ @Override public void start(int totalTasks) { // Ignore the number of tasks. } - /** {@inheritDoc} */ @Override public void beginTask(String title, int work) { endTask(); @@ -62,14 +60,12 @@ public abstract class BatchingProgressMonitor implements ProgressMonitor { task.delay(delayStartTime, delayStartUnit); } - /** {@inheritDoc} */ @Override public void update(int completed) { if (task != null) task.update(this, completed); } - /** {@inheritDoc} */ @Override public void endTask() { if (task != null) { @@ -78,7 +74,6 @@ public abstract class BatchingProgressMonitor implements ProgressMonitor { } } - /** {@inheritDoc} */ @Override public boolean isCancelled() { return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BitmapIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BitmapIndex.java index b11b230a8f..acaa6335d4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BitmapIndex.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BitmapIndex.java @@ -40,6 +40,58 @@ public interface BitmapIndex { BitmapBuilder newBitmapBuilder(); /** + * Report the results of {@link #getBitmap(AnyObjectId)} + * + * @since 6.8 + */ + interface BitmapLookupListener { + + /** + * This object has a bitmap in the index + * + * @param oid + * object id + */ + void onBitmapFound(AnyObjectId oid); + + /** + * This object does not have a bitmap in the index + * + * @param oid + * object id + */ + void onBitmapNotFound(AnyObjectId oid); + + /** + * No-op instance + */ + BitmapLookupListener NOOP = new BitmapLookupListener() { + @Override + public void onBitmapFound(AnyObjectId oid) { + // Nothing to do + } + + @Override + public void onBitmapNotFound(AnyObjectId oid) { + // Nothing to do + } + }; + } + + /** + * Report to this listener whether {@link #getBitmap(AnyObjectId)} finds a + * commit. + * + * @param listener + * instance listening to lookup events in the index. Never null. + * Set to {@link BitmapLookupListener#NOOP} to disable. + * @since 6.8 + */ + default void addBitmapLookupListener(BitmapLookupListener listener) { + // Empty implementation for API compatibility + } + + /** * A bitmap representation of ObjectIds that can be iterated to return the * underlying {@code ObjectId}s or operated on with other {@code Bitmap}s. */ @@ -159,7 +211,11 @@ public interface BitmapIndex { @Override BitmapBuilder xor(Bitmap other); - /** @return the fully built immutable bitmap */ + /** + * Build the bitmap + * + * @return the fully built immutable bitmap + */ Bitmap build(); /** @@ -174,7 +230,11 @@ public interface BitmapIndex { */ boolean removeAllOrNone(PackBitmapIndex bitmapIndex); - /** @return the number of elements in the bitmap. */ + /** + * Get number of elements in the bitmap + * + * @return the number of elements in the bitmap. + */ int cardinality(); /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java index e15c7af932..7921052aaa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java @@ -187,8 +187,7 @@ public class BranchConfig { * @since 4.5 */ public BranchRebaseMode getRebaseMode() { - return config.getEnum(BranchRebaseMode.values(), - ConfigConstants.CONFIG_BRANCH_SECTION, branchName, + return config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchTrackingStatus.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchTrackingStatus.java index 6c625bb73a..39fc566034 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchTrackingStatus.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchTrackingStatus.java @@ -33,6 +33,7 @@ public class BranchTrackingStatus { * the local branch * @return the tracking status, or null if it is not known * @throws java.io.IOException + * if an IO error occurred */ public static BranchTrackingStatus of(Repository repository, String branchName) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java index 1665f051e9..ad3c2c091d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java @@ -194,19 +194,6 @@ public class CommitBuilder extends ObjectBuilder { } } - /** - * Set the encoding for the commit information. - * - * @param encodingName - * the encoding name. See - * {@link java.nio.charset.Charset#forName(String)}. - * @deprecated use {@link #setEncoding(Charset)} instead. - */ - @Deprecated - public void setEncoding(String encodingName) { - setEncoding(Charset.forName(encodingName)); - } - @Override public byte[] build() throws UnsupportedEncodingException { ByteArrayOutputStream os = new ByteArrayOutputStream(); @@ -275,7 +262,6 @@ public class CommitBuilder extends ObjectBuilder { return build(); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java index 6a9b45b065..b1ba5dfa28 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java @@ -119,7 +119,7 @@ public class CommitConfig { if (!StringUtils.isEmptyOrNull(comment)) { if ("auto".equalsIgnoreCase(comment)) { //$NON-NLS-1$ autoCommentChar = true; - } else { + } else if (comment != null) { char first = comment.charAt(0); if (first > ' ' && first < 127) { commentCharacter = first; @@ -403,7 +403,7 @@ public class CommitConfig { for (int i = 0; i < len; i++) { char ch = line.charAt(i); if (!Character.isWhitespace(ch)) { - if (ch >= 0 && ch < inUse.length) { + if (ch < inUse.length) { inUse[ch] = true; } break; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java index d1d66d280e..345cb22f80 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -30,9 +30,11 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.events.ConfigChangedEvent; import org.eclipse.jgit.events.ConfigChangedListener; @@ -253,9 +255,8 @@ public class Config { * 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 typedGetter.getInt(this, section, null, name, defaultValue); + public int getInt(String section, String name, int defaultValue) { + return getInt(section, null, name, defaultValue); } /** @@ -263,6 +264,23 @@ public class Config { * * @param section * section the key is grouped within. + * @param name + * name of the key to get. + * @return an integer value from the configuration, or {@code null} if not + * set. + * @since 7.2 + */ + @Nullable + public Integer getInt(String section, String name) { + return getInt(section, null, name); + } + + + /** + * 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 @@ -271,10 +289,30 @@ public class Config { * 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) { + public int getInt(String section, String subsection, String name, + int defaultValue) { + Integer v = typedGetter.getInt(this, section, subsection, name, + Integer.valueOf(defaultValue)); + return v == null ? defaultValue : v.intValue(); + } + + /** + * 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. + * @return an integer value from the configuration, or {@code null} if not + * set. + * @since 7.2 + */ + @Nullable + public Integer getInt(String section, String subsection, String name) { return typedGetter.getInt(this, section, subsection, name, - defaultValue); + null); } /** @@ -296,8 +334,30 @@ public class Config { */ public int getIntInRange(String section, String name, int minValue, int maxValue, int defaultValue) { - return typedGetter.getIntInRange(this, section, null, name, minValue, - maxValue, defaultValue); + return getIntInRange(section, null, name, + minValue, maxValue, defaultValue); + } + + /** + * Obtain an integer value from the configuration which must be inside given + * range. + * + * @param section + * section the key is grouped within. + * @param name + * name of the key to get. + * @param minValue + * minimum value + * @param maxValue + * maximum value + * @return an integer value from the configuration, or {@code null} if not + * set. + * @since 7.2 + */ + @Nullable + public Integer getIntInRange(String section, String name, int minValue, + int maxValue) { + return getIntInRange(section, null, name, minValue, maxValue); } /** @@ -321,8 +381,34 @@ public class Config { */ public int getIntInRange(String section, String subsection, String name, int minValue, int maxValue, int defaultValue) { + Integer v = typedGetter.getIntInRange(this, section, subsection, name, + minValue, maxValue, Integer.valueOf(defaultValue)); + return v == null ? defaultValue : v.intValue(); + } + + /** + * Obtain an integer value from the configuration which must be inside given + * range. + * + * @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 minValue + * minimum value + * @param maxValue + * maximum value + * @return an integer value from the configuration, or {@code null} if not + * set. + * @since 7.2 + */ + @Nullable + public Integer getIntInRange(String section, String subsection, String name, + int minValue, int maxValue) { return typedGetter.getIntInRange(this, section, subsection, name, - minValue, maxValue, defaultValue); + minValue, maxValue, null); } /** @@ -337,7 +423,23 @@ public class Config { * @return an integer value from the configuration, or defaultValue. */ public long getLong(String section, String name, long defaultValue) { - return typedGetter.getLong(this, section, null, name, defaultValue); + return getLong(section, null, name, defaultValue); + } + + /** + * Obtain an integer value from the configuration. + * + * @param section + * section the key is grouped within. + * @param name + * name of the key to get. + * @return an integer value from the configuration, or {@code null} if not + * set. + * @since 7.2 + */ + @Nullable + public Long getLong(String section, String name) { + return getLong(section, null, name); } /** @@ -354,9 +456,28 @@ public class Config { * @return an integer value from the configuration, or defaultValue. */ public long getLong(final String section, String subsection, - final String name, final long defaultValue) { - return typedGetter.getLong(this, section, subsection, name, - defaultValue); + String name, long defaultValue) { + Long v = typedGetter.getLong(this, section, subsection, name, + Long.valueOf(defaultValue)); + return v == null ? defaultValue : v.longValue(); + } + + /** + * 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. + * @return an integer value from the configuration, or {@code null} if not + * set. + * @since 7.2 + */ + @Nullable + public Long getLong(String section, String subsection, String name) { + return typedGetter.getLong(this, section, subsection, name, null); } /** @@ -371,9 +492,26 @@ public class Config { * @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 typedGetter.getBoolean(this, section, null, name, defaultValue); + public boolean getBoolean(String section, String name, + boolean defaultValue) { + Boolean v = typedGetter.getBoolean(this, section, null, name, + Boolean.valueOf(defaultValue)); + return v == null ? defaultValue : v.booleanValue(); + } + + /** + * Get a boolean value from the git config + * + * @param section + * section the key is grouped within. + * @param name + * name of the key to get. + * @return configured boolean value, or {@code null} if not set. + * @since 7.2 + */ + @Nullable + public Boolean getBoolean(String section, String name) { + return getBoolean(section, null, name); } /** @@ -390,15 +528,35 @@ public class Config { * @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) { - return typedGetter.getBoolean(this, section, subsection, name, - defaultValue); + public boolean getBoolean(String section, String subsection, String name, + boolean defaultValue) { + Boolean v = typedGetter.getBoolean(this, section, subsection, name, + Boolean.valueOf(defaultValue)); + return v == null ? defaultValue : v.booleanValue(); + } + + /** + * 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. + * @return configured boolean value, or {@code null} if not set. + * @since 7.2 + */ + @Nullable + public Boolean getBoolean(String section, String subsection, String name) { + return typedGetter.getBoolean(this, section, subsection, name, null); } /** * Parse an enumeration from the configuration. * + * @param <T> + * type of the returned enum * @param section * section the key is grouped within. * @param subsection @@ -409,8 +567,8 @@ public class Config { * default value to return if no value was present. * @return the selected enumeration value, or {@code defaultValue}. */ - public <T extends Enum<?>> T getEnum(final String section, - final String subsection, final String name, final T defaultValue) { + public <T extends Enum<?>> T getEnum(String section, String subsection, + String name, @NonNull T defaultValue) { final T[] all = allValuesOf(defaultValue); return typedGetter.getEnum(this, all, section, subsection, name, defaultValue); @@ -431,6 +589,8 @@ public class Config { /** * Parse an enumeration from the configuration. * + * @param <T> + * type of the returned enum * @param all * all possible values in the enumeration which should be * recognized. Typically {@code EnumType.values()}. @@ -443,14 +603,41 @@ public class Config { * @param defaultValue * default value to return if no value was present. * @return the selected enumeration value, or {@code defaultValue}. + * @deprecated use {@link #getEnum(String, String, String, Enum)} or + * {{@link #getEnum(Enum[], String, String, String)}} instead. */ - public <T extends Enum<?>> T getEnum(final T[] all, final String section, - final String subsection, final String name, final T defaultValue) { + @Nullable + @Deprecated + public <T extends Enum<?>> T getEnum(T[] all, String section, + String subsection, String name, @Nullable T defaultValue) { return typedGetter.getEnum(this, all, section, subsection, name, defaultValue); } /** + * Parse an enumeration from the configuration. + * + * @param <T> + * type of the returned enum + * @param all + * all possible values in the enumeration which should be + * recognized. Typically {@code EnumType.values()}. + * @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. + * @return the selected enumeration value, or {@code null} if not set. + * @since 7.2 + */ + @Nullable + public <T extends Enum<?>> T getEnum(T[] all, String section, + String subsection, String name) { + return typedGetter.getEnum(this, all, section, subsection, name, null); + } + + /** * Get string value or null if not found. * * @param section @@ -461,8 +648,8 @@ public class Config { * the key name * @return a String value from the config, <code>null</code> if not found */ - public String getString(final String section, String subsection, - final String name) { + @Nullable + public String getString(String section, String subsection, String name) { return getRawString(section, subsection, name); } @@ -521,8 +708,34 @@ public class Config { */ public long getTimeUnit(String section, String subsection, String name, long defaultValue, TimeUnit wantUnit) { + Long v = typedGetter.getTimeUnit(this, section, subsection, name, + Long.valueOf(defaultValue), wantUnit); + return v == null ? defaultValue : v.longValue(); + + } + + /** + * Parse a numerical time unit, such as "1 minute", from the configuration. + * + * @param section + * section the key is in. + * @param subsection + * subsection the key is in, or null if not in a subsection. + * @param name + * the key name. + * @param wantUnit + * the units of {@code defaultValue} and the return value, as + * well as the units to assume if the value does not contain an + * indication of the units. + * @return the value, or {@code null} if not set, expressed in + * {@code units}. + * @since 7.2 + */ + @Nullable + public Long getTimeUnit(String section, String subsection, String name, + TimeUnit wantUnit) { return typedGetter.getTimeUnit(this, section, subsection, name, - defaultValue, wantUnit); + null, wantUnit); } /** @@ -550,8 +763,9 @@ public class Config { * @return the {@link Path}, or {@code defaultValue} if not set * @since 5.10 */ + @Nullable public Path getPath(String section, String subsection, String name, - @NonNull FS fs, File resolveAgainst, Path defaultValue) { + @NonNull FS fs, File resolveAgainst, @Nullable Path defaultValue) { return typedGetter.getPath(this, section, subsection, name, fs, resolveAgainst, defaultValue); } @@ -722,7 +936,7 @@ public class Config { * responsible for issuing {@link #fireConfigChangedEvent()} calls * themselves. * - * @return <code></code> + * @return whether to issue change events for transient changes */ protected boolean notifyUponTransientChanges() { return true; @@ -735,7 +949,7 @@ public class Config { listeners.dispatch(new ConfigChangedEvent()); } - String getRawString(final String section, final String subsection, + private String getRawString(final String section, final String subsection, final String name) { String[] lst = getRawStringList(section, subsection, name); if (lst != null) { @@ -847,6 +1061,8 @@ public class Config { * name = value * </pre> * + * @param <T> + * type of the enum to set * @param section * section name, e.g "branch" * @param subsection @@ -915,29 +1131,52 @@ public class Config { * optional subsection value, e.g. a branch name */ public void unsetSection(String section, String subsection) { + removeSection(section, subsection); + } + + /** + * Removes all configuration values under a single section. + * + * @param section + * section name, e.g "branch" + * @param subsection + * optional subsection value, e.g. a branch name + * @return {@code true} if a section was present and was removed; + * {@code false} if the config was not changed (i.e., no such + * section was present) + * @since 6.8 + */ + public boolean removeSection(String section, String subsection) { ConfigSnapshot src, res; + AtomicBoolean changed = new AtomicBoolean(); do { src = state.get(); - res = unsetSection(src, section, subsection); + changed.set(false); + res = unsetSection(src, section, subsection, changed); } while (!state.compareAndSet(src, res)); + return changed.get(); } - private ConfigSnapshot unsetSection(final ConfigSnapshot srcState, - final String section, - final String subsection) { + private ConfigSnapshot unsetSection(ConfigSnapshot srcState, String section, + String subsection, AtomicBoolean changed) { final int max = srcState.entryList.size(); final ArrayList<ConfigLine> r = new ArrayList<>(max); boolean lastWasMatch = false; for (ConfigLine e : srcState.entryList) { - if (e.includedFrom == null && e.match(section, subsection)) { - // Skip this record, it's for the section we are removing. - lastWasMatch = true; + if (e.includedFrom != null) { + r.add(e); continue; } - - if (lastWasMatch && e.section == null && e.subsection == null) + if (lastWasMatch && e.section == null && e.subsection == null) { continue; // skip this padding line in the section. + } + lastWasMatch = e.match(section, subsection); + if (lastWasMatch) { + // Skip this record, it's for the section we are removing. + changed.set(true); + continue; + } r.add(e); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index c4b6bf955e..c4550329d3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -60,7 +60,8 @@ public final class ConfigConstants { public static final String CONFIG_KEY_PROMPT = "prompt"; /** - * The "trustExitCode" key within "difftool" or "mergetool.<name>." section + * The "trustExitCode" key within "difftool" or "mergetool.<name>." + * section * * @since 6.1 */ @@ -77,6 +78,13 @@ public final class ConfigConstants { public static final String CONFIG_DFS_SECTION = "dfs"; /** + * The dfs cache subsection prefix. + * + * @since 7.0 + */ + public static final String CONFIG_DFS_CACHE_PREFIX = "dfs."; + + /** * The "receive" section * @since 4.6 */ @@ -103,11 +111,18 @@ public final class ConfigConstants { /** The "gc" section */ public static final String CONFIG_GC_SECTION = "gc"; + /** + * The "repack" section + * @since 5.13.3 + */ + public static final String CONFIG_REPACK_SECTION = "repack"; + /** The "pack" section */ public static final String CONFIG_PACK_SECTION = "pack"; /** * The "fetch" section + * * @since 3.3 */ public static final String CONFIG_FETCH_SECTION = "fetch"; @@ -191,7 +206,36 @@ public final class ConfigConstants { public static final String CONFIG_KEY_SIGNINGKEY = "signingKey"; /** + * The "ssh" subsection key. + * + * @since 7.1 + */ + public static final String CONFIG_SSH_SUBSECTION = "ssh"; + + /** + * The "defaultKeyCommand" key. + * + * @since 7.1 + */ + public static final String CONFIG_KEY_SSH_DEFAULT_KEY_COMMAND = "defaultKeyCommand"; + + /** + * The "allowedSignersFile" key. + * + * @since 7.1 + */ + public static final String CONFIG_KEY_SSH_ALLOWED_SIGNERS_FILE = "allowedSignersFile"; + + /** + * The "revocationFile" key, + * + * @since 7.1 + */ + public static final String CONFIG_KEY_SSH_REVOCATION_FILE = "revocationFile"; + + /** * The "commit" section + * * @since 5.2 */ public static final String CONFIG_COMMIT_SECTION = "commit"; @@ -324,13 +368,24 @@ public final class ConfigConstants { public static final String CONFIG_KEY_DELTA_BASE_CACHE_LIMIT = "deltaBaseCacheLimit"; /** + * The "packExtensions" key + * + * @since 7.0 + **/ + public static final String CONFIG_KEY_PACK_EXTENSIONS = "packExtensions"; + + /** * The "symlinks" key * @since 3.3 */ public static final String CONFIG_KEY_SYMLINKS = "symlinks"; - /** The "streamFileThreshold" key */ - public static final String CONFIG_KEY_STREAM_FILE_TRESHOLD = "streamFileThreshold"; + /** + * The "streamFileThreshold" key + * + * @since 6.8 + */ + public static final String CONFIG_KEY_STREAM_FILE_THRESHOLD = "streamFileThreshold"; /** * The "packedGitMmap" key @@ -362,6 +417,12 @@ public final class ConfigConstants { */ public static final String CONFIG_KEY_PACKED_GIT_USE_STRONGREFS = "packedgitusestrongrefs"; + /** + * The "packedIndexGitUseStrongRefs" key + * @since 6.7 + */ + public static final String CONFIG_KEY_PACKED_INDEX_GIT_USE_STRONGREFS = "packedindexgitusestrongrefs"; + /** The "remote" key */ public static final String CONFIG_KEY_REMOTE = "remote"; @@ -385,6 +446,13 @@ public final class ConfigConstants { /** The "rebase" key */ public static final String CONFIG_KEY_REBASE = "rebase"; + /** + * The "checkout" key + * + * @since 7.2 + */ + public static final String CONFIG_KEY_CHECKOUT = "checkout"; + /** The "url" key */ public static final String CONFIG_KEY_URL = "url"; @@ -532,11 +600,21 @@ public final class ConfigConstants { /** * The "trustfolderstat" key in the "core" section + * * @since 3.6 + * @deprecated use {CONFIG_KEY_TRUST_STAT} instead */ + @Deprecated(since = "7.2", forRemoval = true) public static final String CONFIG_KEY_TRUSTFOLDERSTAT = "trustfolderstat"; /** + * The "trustfilestat" key in the "core"section + * + * @since 7.2 + */ + public static final String CONFIG_KEY_TRUST_STAT = "truststat"; + + /** * The "supportsAtomicFileCreation" key in the "core" section * * @since 4.5 @@ -727,6 +805,13 @@ public final class ConfigConstants { public static final String CONFIG_KEY_BITMAP_EXCESSIVE_BRANCH_COUNT = "bitmapexcessivebranchcount"; /** + * The "pack.bitmapExcessiveBranchTipCount" key + * + * @since 6.9 + */ + public static final String CONFIG_KEY_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT = "bitmapexcessivebranchtipcount"; + + /** * The "pack.bitmapExcludedRefsPrefixes" key * @since 5.13.2 */ @@ -745,6 +830,13 @@ public final class ConfigConstants { public static final String CONFIG_KEY_BITMAP_RECENT_COMMIT_COUNT = "bitmaprecentcommitspan"; /** + * The "pack.writeReverseIndex" key + * + * @since 6.6 + */ + public static final String CONFIG_KEY_WRITE_REVERSE_INDEX = "writeReverseIndex"; + + /** * The "pack.buildBitmaps" key * @since 5.8 */ @@ -836,6 +928,13 @@ public final class ConfigConstants { public static final String CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX = "minBytesForObjSizeIndex"; /** + * The "repack.packKeptObjects" key + * + * @since 5.13.3 + */ + public static final String CONFIG_KEY_PACK_KEPT_OBJECTS = "packkeptobjects"; + + /** * The "feature" section * * @since 5.9 @@ -927,6 +1026,34 @@ public final class ConfigConstants { public static final String CONFIG_KEY_TRUST_PACKED_REFS_STAT = "trustPackedRefsStat"; /** + * The "trustLooseRefStat" key + * + * @since 6.9 + */ + public static final String CONFIG_KEY_TRUST_LOOSE_REF_STAT = "trustLooseRefStat"; + + /** + * The "trustLooseRefStat" key + * + * @since 7.2 + */ + public static final String CONFIG_KEY_TRUST_PACK_STAT = "trustPackStat"; + + /** + * The "trustLooseObjectFileStat" key + * + * @since 7.2 + */ + public static final String CONFIG_KEY_TRUST_LOOSE_OBJECT_STAT = "trustLooseObjectStat"; + + /** + * The "trustTablesListStat" key + * + * @since 7.2 + */ + public static final String CONFIG_KEY_TRUST_TABLESLIST_STAT = "trustTablesListStat"; + + /** * The "pack.preserveOldPacks" key * * @since 5.13.2 @@ -939,4 +1066,53 @@ public final class ConfigConstants { * @since 5.13.2 */ public static final String CONFIG_KEY_PRUNE_PRESERVED = "prunepreserved"; + + /** + * The "commitGraph" section + * + * @since 6.7 + */ + public static final String CONFIG_COMMIT_GRAPH_SECTION = "commitGraph"; + + /** + * The "writeChangedPaths" key + * + * @since 6.7 + */ + public static final String CONFIG_KEY_WRITE_CHANGED_PATHS = "writeChangedPaths"; + + /** + * The "readChangedPaths" key + * + * @since 6.7 + */ + public static final String CONFIG_KEY_READ_CHANGED_PATHS = "readChangedPaths"; + + /** + * The "useObjectSizeIndex" key + * + * @since 7.0 + */ + public static final String CONFIG_KEY_USE_OBJECT_SIZE_INDEX = "useObjectSizeIndex"; + + /** + * The "loadRevIndexInParallel" key + * + * @since 7.1 + */ + public static final String CONFIG_KEY_LOAD_REV_INDEX_IN_PARALLEL = "loadRevIndexInParallel"; + + /** + * The "reftable" section + * + * @since 7.2 + */ + public static final String CONFIG_REFTABLE_SECTION = "reftable"; + + /** + * The "autorefresh" key + * + * @since 7.2 + */ + public static final String CONFIG_KEY_AUTOREFRESH = "autorefresh"; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigLine.java index a9235ebcdf..e9a8b44271 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigLine.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigLine.java @@ -83,7 +83,6 @@ class ConfigLine { return a.equals(b); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index 0b8bf8c6c5..9de8392690 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -12,10 +12,13 @@ package org.eclipse.jgit.lib; +import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; @@ -203,24 +206,6 @@ public final class Constants { */ public static final byte[] PACK_SIGNATURE = { 'P', 'A', 'C', 'K' }; - /** - * Native character encoding for commit messages, file names... - * - * @deprecated Use {@link java.nio.charset.StandardCharsets#UTF_8} directly - * instead. - */ - @Deprecated - public static final Charset CHARSET; - - /** - * Native character encoding for commit messages, file names... - * - * @deprecated Use {@link java.nio.charset.StandardCharsets#UTF_8} directly - * instead. - */ - @Deprecated - public static final String CHARACTER_ENCODING; - /** Default main branch name */ public static final String MASTER = "master"; @@ -273,6 +258,20 @@ public final class Constants { public static final String INFO_REFS = "info/refs"; /** + * Name of heads folder or file in refs. + * + * @since 7.0 + */ + public static final String HEADS = "heads"; + + /** + * Prefix for any log. + * + * @since 7.0 + */ + public static final String L_LOGS = LOGS + "/"; + + /** * Info alternates file (goes under OBJECTS) * @since 5.5 */ @@ -337,15 +336,24 @@ public final class Constants { public static final String GIT_CONFIG_NOSYSTEM_KEY = "GIT_CONFIG_NOSYSTEM"; /** - * The key of the XDG_CONFIG_HOME directory defined in the XDG base - * directory specification, see - * {@link "https://wiki.archlinux.org/index.php/XDG_Base_Directory"} + * The key of the XDG_CONFIG_HOME directory defined in the + * <a href="https://wiki.archlinux.org/index.php/XDG_Base_Directory"> + * XDG Base Directory specification</a>. * * @since 5.5.2 */ public static final String XDG_CONFIG_HOME = "XDG_CONFIG_HOME"; /** + * The key of the XDG_CACHE_HOME directory defined in the + * <a href="https://wiki.archlinux.org/index.php/XDG_Base_Directory"> + * XDG Base Directory specification</a>. + * + * @since 7.3 + */ + public static final String XDG_CACHE_HOME = "XDG_CACHE_HOME"; + + /** * The environment variable that limits how close to the root of the file * systems JGit will traverse when looking for a repository root. */ @@ -358,6 +366,14 @@ public final class Constants { public static final String GIT_DIR_KEY = "GIT_DIR"; /** + * The environment variable that tells us which directory is the common + * ".git" directory. + * + * @since 7.0 + */ + public static final String GIT_COMMON_DIR_KEY = "GIT_COMMON_DIR"; + + /** * The environment variable that tells us which directory is the working * directory. */ @@ -459,6 +475,36 @@ public final class Constants { public static final String GITDIR = "gitdir: "; /** + * Name of the file (inside gitDir) that references the worktree's .git + * file (opposite link). + * + * .git/worktrees/<worktree-name>/gitdir + * + * A text file containing the absolute path back to the .git file that + * points here. This file is used to verify if the linked repository has been + * manually removed in which case this directory is no longer needed. + * The modification time (mtime) of this file should be updated each time + * the linked repository is accessed. + * + * @since 7.0 + */ + public static final String GITDIR_FILE = "gitdir"; + + /** + * Name of the file (inside gitDir) that has reference to $GIT_COMMON_DIR. + * + * .git/worktrees/<worktree-name>/commondir + * + * If this file exists, $GIT_COMMON_DIR will be set to the path specified in + * this file unless it is explicitly set. If the specified path is relative, + * it is relative to $GIT_DIR. The repository with commondir is incomplete + * without the repository pointed by "commondir". + * + * @since 7.0 + */ + public static final String COMMONDIR_FILE = "commondir"; + + /** * Name of the folder (inside gitDir) where submodules are stored * * @since 3.6 @@ -494,6 +540,34 @@ public final class Constants { public static final String ATTR_BUILTIN_BINARY_MERGER = "binary"; //$NON-NLS-1$ /** + * Prefix of a GPG signature. + * + * @since 7.0 + */ + public static final String GPG_SIGNATURE_PREFIX = "-----BEGIN PGP SIGNATURE-----"; //$NON-NLS-1$ + + /** + * Prefix of a CMS signature (X.509, S/MIME). + * + * @since 7.0 + */ + public static final String CMS_SIGNATURE_PREFIX = "-----BEGIN SIGNED MESSAGE-----"; //$NON-NLS-1$ + + /** + * Prefix of an SSH signature. + * + * @since 7.0 + */ + public static final String SSH_SIGNATURE_PREFIX = "-----BEGIN SSH SIGNATURE-----"; //$NON-NLS-1$ + + /** + * Union built-in merge driver + * + * @since 6.10.1 + */ + public static final String ATTR_BUILTIN_UNION_MERGE_DRIVER = "union"; //$NON-NLS-1$ + + /** * Create a new digest function for objects. * * @return a new digest object. @@ -661,44 +735,32 @@ public final class Constants { * the 7-bit ASCII character space. */ public static byte[] encodeASCII(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(MessageFormat.format(JGitText.get().notASCIIString, s)); - r[k] = (byte) c; + try { + CharsetEncoder encoder = US_ASCII.newEncoder() + .onUnmappableCharacter(CodingErrorAction.REPORT) + .onMalformedInput(CodingErrorAction.REPORT); + return encoder.encode(CharBuffer.wrap(s)).array(); + } catch (CharacterCodingException e) { + throw new IllegalArgumentException( + MessageFormat.format(JGitText.get().notASCIIString, s), e); } - return r; } /** - * Convert a string to a byte array in the standard character encoding. + * Convert a string to a byte array in the standard character encoding UTF8. * * @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(String str) { - final ByteBuffer bb = UTF_8.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; + return str.getBytes(UTF_8); } static { if (OBJECT_ID_LENGTH != newMessageDigest().getDigestLength()) throw new LinkageError(JGitText.get().incorrectOBJECT_ID_LENGTH); - CHARSET = UTF_8; - CHARACTER_ENCODING = UTF_8.name(); } /** name of the file containing the commit msg for a merge commit */ @@ -766,7 +828,7 @@ public final class Constants { * * @since 6.5 */ - public static int COMMIT_GENERATION_UNKNOWN = Integer.MAX_VALUE; + public static final int COMMIT_GENERATION_UNKNOWN = Integer.MAX_VALUE; /** * If a commit-graph file was written by a version of Git that did not @@ -775,7 +837,7 @@ public final class Constants { * * @since 6.5 */ - public static int COMMIT_GENERATION_NOT_COMPUTED = 0; + public static final int COMMIT_GENERATION_NOT_COMPUTED = 0; 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 index 4de1801d04..0e27b2743c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java @@ -17,12 +17,16 @@ package org.eclipse.jgit.lib; import static java.util.zip.Deflater.DEFAULT_COMPRESSION; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Config.SectionParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * This class keeps git repository core parameters. */ public class CoreConfig { + private static final Logger LOG = LoggerFactory.getLogger(CoreConfig.class); /** Key for {@link Config#get(SectionParser)}. */ public static final Config.SectionParser<CoreConfig> KEY = CoreConfig::new; @@ -127,7 +131,9 @@ public class CoreConfig { * Permissible values for {@code core.trustPackedRefsStat}. * * @since 6.1.1 + * @deprecated use {@link TrustStat} instead */ + @Deprecated(since = "7.2", forRemoval = true) public enum TrustPackedRefsStat { /** Do not trust file attributes of the packed-refs file. */ NEVER, @@ -135,27 +141,82 @@ public class CoreConfig { /** Trust file attributes of the packed-refs file. */ ALWAYS, - /** Open and close the packed-refs file to refresh its file attributes - * and then trust it. */ + /** + * Open and close the packed-refs file to refresh its file attributes + * and then trust it. + */ AFTER_OPEN, - /** {@code core.trustPackedRefsStat} defaults to this when it is - * not set */ + /** + * {@code core.trustPackedRefsStat} defaults to this when it is not set + */ UNSET } + /** + * Permissible values for {@code core.trustLooseRefStat}. + * + * @since 6.9 + * @deprecated use {@link TrustStat} instead + */ + @Deprecated(since = "7.2", forRemoval = true) + public enum TrustLooseRefStat { + + /** Trust file attributes of the loose ref. */ + ALWAYS, + + /** + * Open and close parent directories of the loose ref file until the + * repository root to refresh its file attributes and then trust it. + */ + AFTER_OPEN, + } + + /** + * Values for {@code core.trustXXX} options. + * + * @since 7.2 + */ + public enum TrustStat { + /** Do not trust file attributes of a File. */ + NEVER, + + /** Always trust file attributes of a File. */ + ALWAYS, + + /** Open and close the File to refresh its file attributes + * and then trust it. */ + AFTER_OPEN, + + /** + * Used for specific options to inherit value from value set for + * core.trustStat. + */ + INHERIT + } + private final int compression; private final int packIndexVersion; - private final LogRefUpdates logAllRefUpdates; - private final String excludesfile; private final String attributesfile; private final boolean commitGraph; + private final TrustStat trustStat; + + private final TrustStat trustPackedRefsStat; + + private final TrustStat trustLooseRefStat; + + private final TrustStat trustPackStat; + + private final TrustStat trustLooseObjectStat; + + private final TrustStat trustTablesListStat; + /** * Options for symlink handling * @@ -185,14 +246,17 @@ public class CoreConfig { DOTGITONLY } - private CoreConfig(Config rc) { + /** + * Create a new core configuration from the passed configuration. + * + * @param rc + * git configuration + */ + CoreConfig(Config rc) { compression = rc.getInt(ConfigConstants.CONFIG_CORE_SECTION, ConfigConstants.CONFIG_KEY_COMPRESSION, DEFAULT_COMPRESSION); packIndexVersion = rc.getInt(ConfigConstants.CONFIG_PACK_SECTION, ConfigConstants.CONFIG_KEY_INDEXVERSION, 2); - logAllRefUpdates = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, - ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, - LogRefUpdates.TRUE); excludesfile = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_EXCLUDESFILE); attributesfile = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, @@ -200,6 +264,68 @@ public class CoreConfig { commitGraph = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION, ConfigConstants.CONFIG_COMMIT_GRAPH, DEFAULT_COMMIT_GRAPH_ENABLE); + + trustStat = parseTrustStat(rc); + trustPackedRefsStat = parseTrustPackedRefsStat(rc); + trustLooseRefStat = parseTrustLooseRefStat(rc); + trustPackStat = parseTrustPackFileStat(rc); + trustLooseObjectStat = parseTrustLooseObjectFileStat(rc); + trustTablesListStat = parseTablesListStat(rc); + } + + private static TrustStat parseTrustStat(Config rc) { + Boolean tfs = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION, + ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT); + TrustStat ts = rc.getEnum(TrustStat.values(), + ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_TRUST_STAT); + if (tfs != null) { + if (ts == null) { + LOG.warn(JGitText.get().deprecatedTrustFolderStat); + return tfs.booleanValue() ? TrustStat.ALWAYS : TrustStat.NEVER; + } + LOG.warn(JGitText.get().precedenceTrustConfig); + } + if (ts == null) { + ts = TrustStat.ALWAYS; + } else if (ts == TrustStat.INHERIT) { + LOG.warn(JGitText.get().invalidTrustStat); + ts = TrustStat.ALWAYS; + } + return ts; + } + + private TrustStat parseTrustPackedRefsStat(Config rc) { + return inheritParseTrustStat(rc, + ConfigConstants.CONFIG_KEY_TRUST_PACKED_REFS_STAT); + } + + private TrustStat parseTrustLooseRefStat(Config rc) { + return inheritParseTrustStat(rc, + ConfigConstants.CONFIG_KEY_TRUST_LOOSE_REF_STAT); + } + + private TrustStat parseTrustPackFileStat(Config rc) { + return inheritParseTrustStat(rc, + ConfigConstants.CONFIG_KEY_TRUST_PACK_STAT); + } + + private TrustStat parseTrustLooseObjectFileStat(Config rc) { + return inheritParseTrustStat(rc, + ConfigConstants.CONFIG_KEY_TRUST_LOOSE_OBJECT_STAT); + } + + private TrustStat inheritParseTrustStat(Config rc, String key) { + TrustStat t = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, key, + TrustStat.INHERIT); + return t == TrustStat.INHERIT ? trustStat : t; + } + + private TrustStat parseTablesListStat(Config rc) { + TrustStat t = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_TRUST_TABLESLIST_STAT, + TrustStat.INHERIT); + return t == TrustStat.INHERIT ? trustStat : t; } /** @@ -221,20 +347,6 @@ public class CoreConfig { } /** - * Whether to log all refUpdates - * - * @return whether to log all refUpdates - * @deprecated since 5.6; default value depends on whether the repository is - * bare. Use - * {@link Config#getEnum(String, String, String, Enum)} - * directly. - */ - @Deprecated - public boolean isLogAllRefUpdates() { - return !LogRefUpdates.FALSE.equals(logAllRefUpdates); - } - - /** * Get path of excludesfile * * @return path of excludesfile @@ -264,4 +376,70 @@ public class CoreConfig { public boolean enableCommitGraph() { return commitGraph; } + + /** + * Get how far we can trust file attributes of packed-refs file which is + * used to store {@link org.eclipse.jgit.lib.Ref}s in + * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}. + * + * @return how far we can trust file attributes of packed-refs file. + * + * @since 7.2 + */ + public TrustStat getTrustPackedRefsStat() { + return trustPackedRefsStat; + } + + /** + * Get how far we can trust file attributes of loose ref files which are + * used to store {@link org.eclipse.jgit.lib.Ref}s in + * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}. + * + * @return how far we can trust file attributes of loose ref files. + * + * @since 7.2 + */ + public TrustStat getTrustLooseRefStat() { + return trustLooseRefStat; + } + + /** + * Get how far we can trust file attributes of packed-refs file which is + * used to store {@link org.eclipse.jgit.lib.Ref}s in + * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}. + * + * @return how far we can trust file attributes of packed-refs file. + * + * @since 7.2 + */ + public TrustStat getTrustPackStat() { + return trustPackStat; + } + + /** + * Get how far we can trust file attributes of loose ref files which are + * used to store {@link org.eclipse.jgit.lib.Ref}s in + * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}. + * + * @return how far we can trust file attributes of loose ref files. + * + * @since 7.2 + */ + public TrustStat getTrustLooseObjectStat() { + return trustLooseObjectStat; + } + + /** + * Get how far we can trust file attributes of the "tables.list" file which + * is used to store the list of filenames of the files storing + * {@link org.eclipse.jgit.internal.storage.reftable.Reftable}s in + * {@link org.eclipse.jgit.internal.storage.file.FileReftableDatabase}. + * + * @return how far we can trust file attributes of the "tables.list" file. + * + * @since 7.2 + */ + public TrustStat getTrustTablesListStat() { + return trustTablesListStat; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java index 86409403b0..3059f283fe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java @@ -18,6 +18,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Config.ConfigEnum; import org.eclipse.jgit.transport.RefSpec; @@ -31,29 +32,37 @@ import org.eclipse.jgit.util.StringUtils; */ public class DefaultTypedConfigGetter implements TypedConfigGetter { - /** {@inheritDoc} */ + @SuppressWarnings("boxed") @Override public boolean getBoolean(Config config, String section, String subsection, String name, boolean defaultValue) { - String n = config.getRawString(section, subsection, name); + return neverNull(getBoolean(config, section, subsection, name, + Boolean.valueOf(defaultValue))); + } + + @Nullable + @Override + public Boolean getBoolean(Config config, String section, String subsection, + String name, @Nullable Boolean defaultValue) { + String n = config.getString(section, subsection, name); if (n == null) { return defaultValue; } if (Config.isMissing(n)) { - return true; + return Boolean.TRUE; } try { - return StringUtils.toBoolean(n); + return Boolean.valueOf(StringUtils.toBoolean(n)); } catch (IllegalArgumentException err) { throw new IllegalArgumentException(MessageFormat.format( JGitText.get().invalidBooleanValue, section, name, n), err); } } - /** {@inheritDoc} */ + @Nullable @Override public <T extends Enum<?>> T getEnum(Config config, T[] all, String section, - String subsection, String name, T defaultValue) { + String subsection, String name, @Nullable T defaultValue) { String value = config.getString(section, subsection, name); if (value == null) { return defaultValue; @@ -106,62 +115,102 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { JGitText.get().enumValueNotSupported2, section, name, value)); } - /** {@inheritDoc} */ @Override public int getInt(Config config, String section, String subsection, String name, int defaultValue) { - long val = config.getLong(section, subsection, name, defaultValue); + return neverNull(getInt(config, section, subsection, name, + Integer.valueOf(defaultValue))); + } + + @Nullable + @Override + @SuppressWarnings("boxing") + public Integer getInt(Config config, String section, String subsection, + String name, @Nullable Integer defaultValue) { + Long longDefault = defaultValue != null + ? Long.valueOf(defaultValue.longValue()) + : null; + Long val = config.getLong(section, subsection, name); + if (val == null) { + val = longDefault; + } + if (val == null) { + return null; + } if (Integer.MIN_VALUE <= val && val <= Integer.MAX_VALUE) { - return (int) val; + return Integer.valueOf(Math.toIntExact(val)); } throw new IllegalArgumentException(MessageFormat .format(JGitText.get().integerValueOutOfRange, section, name)); } - /** {@inheritDoc} */ @Override public int getIntInRange(Config config, String section, String subsection, String name, int minValue, int maxValue, int defaultValue) { - int val = getInt(config, section, subsection, name, defaultValue); + return neverNull(getIntInRange(config, section, subsection, name, + minValue, maxValue, Integer.valueOf(defaultValue))); + } + + @Override + @SuppressWarnings("boxing") + public Integer getIntInRange(Config config, String section, + String subsection, String name, int minValue, int maxValue, + Integer defaultValue) { + Integer val = getInt(config, section, subsection, name, defaultValue); + if (val == null) { + return null; + } if ((val >= minValue && val <= maxValue) || val == UNSET_INT) { return val; } if (subsection == null) { - throw new IllegalArgumentException(MessageFormat.format( - JGitText.get().integerValueNotInRange, section, name, - Integer.valueOf(val), Integer.valueOf(minValue), - Integer.valueOf(maxValue))); + throw new IllegalArgumentException( + MessageFormat.format(JGitText.get().integerValueNotInRange, + section, name, val, minValue, maxValue)); } throw new IllegalArgumentException(MessageFormat.format( JGitText.get().integerValueNotInRangeSubSection, section, - subsection, name, Integer.valueOf(val), - Integer.valueOf(minValue), Integer.valueOf(maxValue))); + subsection, name, val, minValue, maxValue)); } - /** {@inheritDoc} */ @Override public long getLong(Config config, String section, String subsection, String name, long defaultValue) { - final String str = config.getString(section, subsection, name); + return neverNull(getLong(config, section, subsection, name, + Long.valueOf(defaultValue))); + } + + @Nullable + @Override + public Long getLong(Config config, String section, String subsection, + String name, @Nullable Long defaultValue) { + String str = config.getString(section, subsection, name); if (str == null) { return defaultValue; } try { - return StringUtils.parseLongWithSuffix(str, false); + return Long.valueOf(StringUtils.parseLongWithSuffix(str, false)); } catch (StringIndexOutOfBoundsException e) { // Empty return defaultValue; } catch (NumberFormatException nfe) { - throw new IllegalArgumentException(MessageFormat.format( - JGitText.get().invalidIntegerValue, section, name, str), + throw new IllegalArgumentException( + MessageFormat.format(JGitText.get().invalidIntegerValue, + section, name, str), nfe); } } - /** {@inheritDoc} */ @Override public long getTimeUnit(Config config, String section, String subsection, String name, long defaultValue, TimeUnit wantUnit) { + return neverNull(getTimeUnit(config, section, subsection, name, + Long.valueOf(defaultValue), wantUnit)); + } + + @Override + public Long getTimeUnit(Config config, String section, String subsection, + String name, @Nullable Long defaultValue, TimeUnit wantUnit) { String valueString = config.getString(section, subsection, name); if (valueString == null) { @@ -238,8 +287,8 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { } try { - return wantUnit.convert(Long.parseLong(digits) * inputMul, - inputUnit); + return Long.valueOf(wantUnit + .convert(Long.parseLong(digits) * inputMul, inputUnit)); } catch (NumberFormatException nfe) { IllegalArgumentException iae = notTimeUnit(section, subsection, unitName, valueString); @@ -269,7 +318,6 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { section, name, valueString)); } - /** {@inheritDoc} */ @Override @NonNull public List<RefSpec> getRefSpecs(Config config, String section, @@ -281,4 +329,14 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { } return result; } + + // Trick for the checkers. When we use this, one is never null, but + // they don't know. + @NonNull + private static <T> T neverNull(T one) { + if (one == null) { + throw new IllegalArgumentException(); + } + return one; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java index 94d28eb345..a5410b778c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/EmptyProgressMonitor.java @@ -18,31 +18,26 @@ package org.eclipse.jgit.lib; */ public abstract class EmptyProgressMonitor implements ProgressMonitor { - /** {@inheritDoc} */ @Override public void start(int totalTasks) { // empty } - /** {@inheritDoc} */ @Override public void beginTask(String title, int totalWork) { // empty } - /** {@inheritDoc} */ @Override public void update(int completed) { // empty } - /** {@inheritDoc} */ @Override public void endTask() { // empty } - /** {@inheritDoc} */ @Override public boolean isCancelled() { return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java index bb72e12510..c75d81c8b5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java @@ -176,12 +176,12 @@ public abstract class FileMode { * Test a file mode for equality with this * {@link org.eclipse.jgit.lib.FileMode} object. * - * @param modebits + * @param modeBits * a int. * @return true if the mode bits represent the same mode as this object */ @SuppressWarnings("NonOverridingEquals") - public abstract boolean equals(int modebits); + public abstract boolean equals(int modeBits); /** * Copy this mode as a sequence of octal US-ASCII bytes. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java new file mode 100644 index 0000000000..073bf7a0ca --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileModeCache.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2023, Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; +import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FileUtils; + +/** + * A hierarchical cache of {@link FileMode}s per git path. + * + * @since 6.6.1 + */ +public class FileModeCache { + + @NonNull + private final CacheItem root = new CacheItem(FileMode.TREE); + + @NonNull + private final Repository repo; + + /** + * Creates a new {@link FileModeCache} for a {@link Repository}. + * + * @param repo + * {@link Repository} this cache is for + */ + public FileModeCache(@NonNull Repository repo) { + this.repo = repo; + } + + /** + * Retrieves the {@link Repository}. + * + * @return the {@link Repository} this {@link FileModeCache} was created for + */ + @NonNull + public Repository getRepository() { + return repo; + } + + /** + * Obtains the {@link CacheItem} for the working tree root. + * + * @return the {@link CacheItem} + */ + @NonNull + public CacheItem getRoot() { + return root; + } + + /** + * Ensure that the given parent directory exists, and cache the information + * that gitPath refers to a file. + * + * @param gitPath + * of the file to be written + * @param parentDir + * directory in which the file shall be placed, assumed to be the + * parent of the {@code gitPath} + * @param makeSpace + * whether to delete a possibly existing file at + * {@code parentDir} + * @throws IOException + * if the directory cannot be created, if necessary + */ + public void safeCreateParentDirectory(String gitPath, File parentDir, + boolean makeSpace) throws IOException { + CacheItem cachedParent = safeCreateDirectory(gitPath, parentDir, + makeSpace); + cachedParent.remove(gitPath.substring(gitPath.lastIndexOf('/') + 1)); + } + + /** + * Ensures the given directory {@code dir} with the given git path exists. + * + * @param gitPath + * of a file to be written + * @param dir + * directory in which the file shall be placed, assumed to be the + * parent of the {@code gitPath} + * @param makeSpace + * whether to remove a file that already at that name + * @return A {@link CacheItem} describing the directory, which is guaranteed + * to exist + * @throws IOException + * if the directory cannot be made to exist at the given + * location + */ + public CacheItem safeCreateDirectory(String gitPath, File dir, + boolean makeSpace) throws IOException { + FS fs = repo.getFS(); + int i = gitPath.lastIndexOf('/'); + String parentPath = null; + if (i >= 0) { + if ((makeSpace && dir.isFile()) || fs.isSymLink(dir)) { + FileUtils.delete(dir); + } + parentPath = gitPath.substring(0, i); + deleteSymlinkParent(fs, parentPath, repo.getWorkTree()); + } + FileUtils.mkdirs(dir, true); + CacheItem cachedParent = getRoot(); + if (parentPath != null) { + cachedParent = add(parentPath, FileMode.TREE); + } + return cachedParent; + } + + private void deleteSymlinkParent(FS fs, String gitPath, File workingTree) + throws IOException { + if (!fs.supportsSymlinks()) { + return; + } + String[] parts = gitPath.split("/"); //$NON-NLS-1$ + int n = parts.length; + CacheItem cached = getRoot(); + File p = workingTree; + for (int i = 0; i < n; i++) { + p = new File(p, parts[i]); + CacheItem cachedChild = cached != null ? cached.child(parts[i]) + : null; + boolean delete = false; + if (cachedChild != null) { + if (FileMode.SYMLINK.equals(cachedChild.getMode())) { + delete = true; + } + } else { + try { + Path nioPath = FileUtils.toPath(p); + BasicFileAttributes attributes = nioPath.getFileSystem() + .provider() + .getFileAttributeView(nioPath, + BasicFileAttributeView.class, + LinkOption.NOFOLLOW_LINKS) + .readAttributes(); + if (attributes.isSymbolicLink()) { + delete = p.isDirectory(); + } else if (attributes.isRegularFile()) { + break; + } + } catch (InvalidPathException | IOException e) { + // If we can't get the attributes the path does not exist, + // or if it does a subsequent mkdirs() will also throw an + // exception. + break; + } + } + if (delete) { + // Deletes the symlink + FileUtils.delete(p, FileUtils.SKIP_MISSING); + if (cached != null) { + cached.remove(parts[i]); + } + break; + } + cached = cachedChild; + } + } + + /** + * Records the given {@link FileMode} for the given git path in the cache. + * If an entry already exists for the given path, the previously cached file + * mode is overwritten. + * + * @param gitPath + * to cache the {@link FileMode} for + * @param finalMode + * {@link FileMode} to cache + * @return the {@link CacheItem} for the path + */ + @NonNull + private CacheItem add(String gitPath, FileMode finalMode) { + if (gitPath.isEmpty()) { + throw new IllegalArgumentException(); + } + String[] parts = gitPath.split("/"); //$NON-NLS-1$ + int n = parts.length; + int i = 0; + CacheItem curr = getRoot(); + while (i < n) { + CacheItem next = curr.child(parts[i]); + if (next == null) { + break; + } + curr = next; + i++; + } + if (i == n) { + curr.setMode(finalMode); + } else { + while (i < n) { + curr = curr.insert(parts[i], + i + 1 == n ? finalMode : FileMode.TREE); + i++; + } + } + return curr; + } + + /** + * An item from a {@link FileModeCache}, recording information about a git + * path (known from context). + */ + public static class CacheItem { + + @NonNull + private FileMode mode; + + private Map<String, CacheItem> children; + + /** + * Creates a new {@link CacheItem}. + * + * @param mode + * {@link FileMode} to cache + */ + public CacheItem(@NonNull FileMode mode) { + this.mode = mode; + } + + /** + * Retrieves the cached {@link FileMode}. + * + * @return the {@link FileMode} + */ + @NonNull + public FileMode getMode() { + return mode; + } + + /** + * Retrieves an immediate child of this {@link CacheItem} by name. + * + * @param childName + * name of the child to get + * @return the {@link CacheItem}, or {@code null} if no such child is + * known + */ + public CacheItem child(String childName) { + if (children == null) { + return null; + } + return children.get(childName); + } + + /** + * Inserts a new cached {@link FileMode} as an immediate child of this + * {@link CacheItem}. If there is already a child with the same name, it + * is overwritten. + * + * @param childName + * name of the child to create + * @param childMode + * {@link FileMode} to cache + * @return the new {@link CacheItem} created for the child + */ + public CacheItem insert(String childName, @NonNull FileMode childMode) { + if (!FileMode.TREE.equals(mode)) { + throw new IllegalArgumentException(); + } + if (children == null) { + children = new HashMap<>(); + } + CacheItem newItem = new CacheItem(childMode); + children.put(childName, newItem); + return newItem; + } + + /** + * Removes the immediate child with the given name. + * + * @param childName + * name of the child to remove + * @return the previously cached {@link CacheItem}, if any + */ + public CacheItem remove(String childName) { + if (children == null) { + return null; + } + return children.remove(childName); + } + + void setMode(@NonNull FileMode mode) { + this.mode = mode; + if (!FileMode.TREE.equals(mode)) { + children = null; + } + } + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitmoduleEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitmoduleEntry.java index aa0a9dd663..d03db772ea 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitmoduleEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitmoduleEntry.java @@ -36,6 +36,8 @@ public final class GitmoduleEntry { } /** + * Get Id of a .gitmodules file found in the pack + * * @return Id of a .gitmodules file found in the pack */ public AnyObjectId getBlobId() { @@ -43,6 +45,8 @@ public final class GitmoduleEntry { } /** + * Get Id of a tree object where the .gitmodules file was found + * * @return Id of a tree object where the .gitmodules file was found */ public AnyObjectId getTreeId() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java index 427a235f3b..23d16db39f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java @@ -24,7 +24,13 @@ public class GpgConfig { /** Value for openpgp */ OPENPGP("openpgp"), //$NON-NLS-1$ /** Value for x509 */ - X509("x509"); //$NON-NLS-1$ + X509("x509"), //$NON-NLS-1$ + /** + * Value for ssh. + * + * @since 7.0 + */ + SSH("ssh"); //$NON-NLS-1$ private final String configValue; @@ -55,26 +61,11 @@ public class GpgConfig { private final boolean forceAnnotated; - /** - * Create a {@link GpgConfig} with the given parameters and default - * {@code true} for signing commits and {@code false} for tags. - * - * @param keySpec - * to use - * @param format - * to use - * @param gpgProgram - * to use - * @since 5.11 - */ - public GpgConfig(String keySpec, GpgFormat format, String gpgProgram) { - keyFormat = format; - signingKey = keySpec; - program = gpgProgram; - signCommits = true; - signAllTags = false; - forceAnnotated = false; - } + private final String sshDefaultKeyCommand; + + private final String sshAllowedSignersFile; + + private final String sshRevocationFile; /** * Create a new GPG config that reads the configuration from config. @@ -83,18 +74,18 @@ public class GpgConfig { * the config to read from */ public GpgConfig(Config config) { - keyFormat = config.getEnum(GpgFormat.values(), - ConfigConstants.CONFIG_GPG_SECTION, null, + keyFormat = config.getEnum(ConfigConstants.CONFIG_GPG_SECTION, null, ConfigConstants.CONFIG_KEY_FORMAT, GpgFormat.OPENPGP); signingKey = config.getString(ConfigConstants.CONFIG_USER_SECTION, null, ConfigConstants.CONFIG_KEY_SIGNINGKEY); String exe = config.getString(ConfigConstants.CONFIG_GPG_SECTION, keyFormat.toConfigValue(), ConfigConstants.CONFIG_KEY_PROGRAM); - if (exe == null) { + if (exe == null && GpgFormat.OPENPGP.equals(keyFormat)) { exe = config.getString(ConfigConstants.CONFIG_GPG_SECTION, null, ConfigConstants.CONFIG_KEY_PROGRAM); } + program = exe; signCommits = config.getBoolean(ConfigConstants.CONFIG_COMMIT_SECTION, ConfigConstants.CONFIG_KEY_GPGSIGN, false); @@ -102,6 +93,17 @@ public class GpgConfig { ConfigConstants.CONFIG_KEY_GPGSIGN, false); forceAnnotated = config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION, ConfigConstants.CONFIG_KEY_FORCE_SIGN_ANNOTATED, false); + sshDefaultKeyCommand = config.getString( + ConfigConstants.CONFIG_GPG_SECTION, + ConfigConstants.CONFIG_SSH_SUBSECTION, + ConfigConstants.CONFIG_KEY_SSH_DEFAULT_KEY_COMMAND); + sshAllowedSignersFile = config.getString( + ConfigConstants.CONFIG_GPG_SECTION, + ConfigConstants.CONFIG_SSH_SUBSECTION, + ConfigConstants.CONFIG_KEY_SSH_ALLOWED_SIGNERS_FILE); + sshRevocationFile = config.getString(ConfigConstants.CONFIG_GPG_SECTION, + ConfigConstants.CONFIG_SSH_SUBSECTION, + ConfigConstants.CONFIG_KEY_SSH_REVOCATION_FILE); } /** @@ -165,4 +167,37 @@ public class GpgConfig { public boolean isSignAnnotated() { return forceAnnotated; } + + /** + * Retrieves the value of git config {@code gpg.ssh.defaultKeyCommand}. + * + * @return the value of {@code gpg.ssh.defaultKeyCommand} + * + * @since 7.1 + */ + public String getSshDefaultKeyCommand() { + return sshDefaultKeyCommand; + } + + /** + * Retrieves the value of git config {@code gpg.ssh.allowedSignersFile}. + * + * @return the value of {@code gpg.ssh.allowedSignersFile} + * + * @since 7.1 + */ + public String getSshAllowedSignersFile() { + return sshAllowedSignersFile; + } + + /** + * Retrieves the value of git config {@code gpg.ssh.revocationFile}. + * + * @return the value of {@code gpg.ssh.revocationFile} + * + * @since 7.1 + */ + public String getSshRevocationFile() { + return sshRevocationFile; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java deleted file mode 100644 index 074f46567b..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.eclipse.jgit.lib; - -import org.eclipse.jgit.annotations.NonNull; -import org.eclipse.jgit.annotations.Nullable; -import org.eclipse.jgit.api.errors.CanceledException; -import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; -import org.eclipse.jgit.transport.CredentialsProvider; - -/** - * Creates GPG signatures for Git objects. - * - * @since 5.11 - */ -public interface GpgObjectSigner { - - /** - * Signs the specified object. - * - * <p> - * Implementors should obtain the payload for signing from the specified - * object via {@link ObjectBuilder#build()} and create a proper - * {@link GpgSignature}. The generated signature must be set on the - * specified {@code object} (see - * {@link ObjectBuilder#setGpgSignature(GpgSignature)}). - * </p> - * <p> - * Any existing signature on the object must be discarded prior obtaining - * the payload via {@link ObjectBuilder#build()}. - * </p> - * - * @param object - * the object to sign (must not be {@code null} and must be - * complete to allow proper calculation of payload) - * @param gpgSigningKey - * the signing key to locate (passed as is to the GPG signing - * tool as is; eg., value of <code>user.signingkey</code>) - * @param committer - * the signing identity (to help with key lookup in case signing - * key is not specified) - * @param credentialsProvider - * provider to use when querying for signing key credentials (eg. - * passphrase) - * @param config - * GPG settings from the git config - * @throws CanceledException - * when signing was canceled (eg., user aborted when entering - * passphrase) - * @throws UnsupportedSigningFormatException - * if a config is given and the wanted key format is not - * supported - */ - void signObject(@NonNull ObjectBuilder object, - @Nullable String gpgSigningKey, @NonNull PersonIdent committer, - CredentialsProvider credentialsProvider, GpgConfig config) - throws CanceledException, UnsupportedSigningFormatException; - - /** - * Indicates if a signing key is available for the specified committer - * and/or signing key. - * - * @param gpgSigningKey - * the signing key to locate (passed as is to the GPG signing - * tool as is; eg., value of <code>user.signingkey</code>) - * @param committer - * the signing identity (to help with key lookup in case signing - * key is not specified) - * @param credentialsProvider - * provider to use when querying for signing key credentials (eg. - * passphrase) - * @param config - * GPG settings from the git config - * @return <code>true</code> if a signing key is available, - * <code>false</code> otherwise - * @throws CanceledException - * when signing was canceled (eg., user aborted when entering - * passphrase) - * @throws UnsupportedSigningFormatException - * if a config is given and the wanted key format is not - * supported - */ - public abstract boolean canLocateSigningKey(@Nullable String gpgSigningKey, - @NonNull PersonIdent committer, - CredentialsProvider credentialsProvider, GpgConfig config) - throws CanceledException, UnsupportedSigningFormatException; - -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignature.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignature.java index e78cf16ea4..4018dabebe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignature.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignature.java @@ -49,7 +49,6 @@ public class GpgSignature implements Serializable { return new String(signature, US_ASCII); } - /** {@inheritDoc} */ @Override @SuppressWarnings("nls") public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java deleted file mode 100644 index a7a39c998f..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.eclipse.jgit.lib; - -import java.io.IOException; -import java.util.Date; - -import org.eclipse.jgit.annotations.NonNull; -import org.eclipse.jgit.annotations.Nullable; -import org.eclipse.jgit.api.errors.JGitInternalException; -import org.eclipse.jgit.revwalk.RevObject; - -/** - * A {@code GpgVerifier} can verify GPG signatures on git commits and tags. - * - * @since 5.11 - */ -public interface GpgSignatureVerifier { - - /** - * Verifies the signature on a signed commit or tag. - * - * @param object - * to verify - * @param config - * the {@link GpgConfig} to use - * @return a {@link SignatureVerification} describing the outcome of the - * verification, or {@code null} if the object was not signed - * @throws IOException - * if an error occurs getting a public key - * @throws org.eclipse.jgit.api.errors.JGitInternalException - * if signature verification fails - */ - @Nullable - SignatureVerification verifySignature(@NonNull RevObject object, - @NonNull GpgConfig config) throws IOException; - - - /** - * Verifies a given signature for given data. - * - * @param data - * the signature is for - * @param signatureData - * the ASCII-armored signature - * @return a {@link SignatureVerification} describing the outcome - * @throws IOException - * if the signature cannot be parsed - * @throws JGitInternalException - * if signature verification fails - */ - public SignatureVerification verify(byte[] data, byte[] signatureData) - throws IOException; - - /** - * Retrieves the name of this verifier. This should be a short string - * identifying the engine that verified the signature, like "gpg" if GPG is - * used, or "bc" for a BouncyCastle implementation. - * - * @return the name - */ - @NonNull - String getName(); - - /** - * A {@link GpgSignatureVerifier} may cache public keys to speed up - * verifying signatures on multiple objects. This clears this cache, if any. - */ - void clear(); - - /** - * A {@code SignatureVerification} returns data about a (positively or - * negatively) verified signature. - */ - interface SignatureVerification { - - // Data about the signature. - - @NonNull - Date getCreationDate(); - - // Data from the signature used to find a public key. - - /** - * Obtains the signer as stored in the signature, if known. - * - * @return the signer, or {@code null} if unknown - */ - String getSigner(); - - /** - * Obtains the short or long fingerprint of the public key as stored in - * the signature, if known. - * - * @return the fingerprint, or {@code null} if unknown - */ - String getKeyFingerprint(); - - // Some information about the found public key. - - /** - * Obtains the OpenPGP user ID associated with the key. - * - * @return the user id, or {@code null} if unknown - */ - String getKeyUser(); - - /** - * Tells whether the public key used for this signature verification was - * expired when the signature was created. - * - * @return {@code true} if the key was expired already, {@code false} - * otherwise - */ - boolean isExpired(); - - /** - * Obtains the trust level of the public key used to verify the - * signature. - * - * @return the trust level - */ - @NonNull - TrustLevel getTrustLevel(); - - // The verification result. - - /** - * Tells whether the signature verification was successful. - * - * @return {@code true} if the signature was verified successfully; - * {@code false} if not. - */ - boolean getVerified(); - - /** - * Obtains a human-readable message giving additional information about - * the outcome of the verification. - * - * @return the message, or {@code null} if none set. - */ - String getMessage(); - } - - /** - * The owner's trust in a public key. - */ - enum TrustLevel { - UNKNOWN, NEVER, MARGINAL, FULL, ULTIMATE - } -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java deleted file mode 100644 index 59775c475b..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2021, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.eclipse.jgit.lib; - -import java.util.Iterator; -import java.util.ServiceConfigurationError; -import java.util.ServiceLoader; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A {@code GpgSignatureVerifierFactory} creates {@link GpgSignatureVerifier} instances. - * - * @since 5.11 - */ -public abstract class GpgSignatureVerifierFactory { - - private static final Logger LOG = LoggerFactory - .getLogger(GpgSignatureVerifierFactory.class); - - private static class DefaultFactory { - - private static volatile GpgSignatureVerifierFactory defaultFactory = loadDefault(); - - private static GpgSignatureVerifierFactory loadDefault() { - try { - ServiceLoader<GpgSignatureVerifierFactory> loader = ServiceLoader - .load(GpgSignatureVerifierFactory.class); - Iterator<GpgSignatureVerifierFactory> iter = loader.iterator(); - if (iter.hasNext()) { - return iter.next(); - } - } catch (ServiceConfigurationError e) { - LOG.error(e.getMessage(), e); - } - return null; - } - - private DefaultFactory() { - // No instantiation - } - - public static GpgSignatureVerifierFactory getDefault() { - return defaultFactory; - } - - /** - * Sets the default factory. - * - * @param factory - * the new default factory - */ - public static void setDefault(GpgSignatureVerifierFactory factory) { - defaultFactory = factory; - } - } - - /** - * Retrieves the default factory. - * - * @return the default factory or {@code null} if none set - */ - public static GpgSignatureVerifierFactory getDefault() { - return DefaultFactory.getDefault(); - } - - /** - * Sets the default factory. - * - * @param factory - * the new default factory - */ - public static void setDefault(GpgSignatureVerifierFactory factory) { - DefaultFactory.setDefault(factory); - } - - /** - * Creates a new {@link GpgSignatureVerifier}. - * - * @return the new {@link GpgSignatureVerifier} - */ - public abstract GpgSignatureVerifier getVerifier(); - -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java deleted file mode 100644 index b25a61b506..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2018, 2022 Salesforce and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.eclipse.jgit.lib; - -import java.util.Iterator; -import java.util.ServiceConfigurationError; -import java.util.ServiceLoader; - -import org.eclipse.jgit.annotations.NonNull; -import org.eclipse.jgit.annotations.Nullable; -import org.eclipse.jgit.api.errors.CanceledException; -import org.eclipse.jgit.transport.CredentialsProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Creates GPG signatures for Git objects. - * - * @since 5.3 - */ -public abstract class GpgSigner { - - private static final Logger LOG = LoggerFactory.getLogger(GpgSigner.class); - - private static class DefaultSigner { - - private static volatile GpgSigner defaultSigner = loadGpgSigner(); - - private static GpgSigner loadGpgSigner() { - try { - ServiceLoader<GpgSigner> loader = ServiceLoader - .load(GpgSigner.class); - Iterator<GpgSigner> iter = loader.iterator(); - if (iter.hasNext()) { - return iter.next(); - } - } catch (ServiceConfigurationError e) { - LOG.error(e.getMessage(), e); - } - return null; - } - - private DefaultSigner() { - // No instantiation - } - - public static GpgSigner getDefault() { - return defaultSigner; - } - - public static void setDefault(GpgSigner signer) { - defaultSigner = signer; - } - } - - /** - * Get the default signer, or <code>null</code>. - * - * @return the default signer, or <code>null</code>. - */ - public static GpgSigner getDefault() { - return DefaultSigner.getDefault(); - } - - /** - * Set the default signer. - * - * @param signer - * the new default signer, may be <code>null</code> to select no - * default. - */ - public static void setDefault(GpgSigner signer) { - DefaultSigner.setDefault(signer); - } - - /** - * Signs the specified commit. - * - * <p> - * Implementors should obtain the payload for signing from the specified - * commit via {@link CommitBuilder#build()} and create a proper - * {@link GpgSignature}. The generated signature must be set on the - * specified {@code commit} (see - * {@link CommitBuilder#setGpgSignature(GpgSignature)}). - * </p> - * <p> - * Any existing signature on the commit must be discarded prior obtaining - * the payload via {@link CommitBuilder#build()}. - * </p> - * - * @param commit - * the commit to sign (must not be <code>null</code> and must be - * complete to allow proper calculation of payload) - * @param gpgSigningKey - * the signing key to locate (passed as is to the GPG signing - * tool as is; eg., value of <code>user.signingkey</code>) - * @param committer - * the signing identity (to help with key lookup in case signing - * key is not specified) - * @param credentialsProvider - * provider to use when querying for signing key credentials (eg. - * passphrase) - * @throws CanceledException - * when signing was canceled (eg., user aborted when entering - * passphrase) - */ - public abstract void sign(@NonNull CommitBuilder commit, - @Nullable String gpgSigningKey, @NonNull PersonIdent committer, - CredentialsProvider credentialsProvider) throws CanceledException; - - /** - * Indicates if a signing key is available for the specified committer - * and/or signing key. - * - * @param gpgSigningKey - * the signing key to locate (passed as is to the GPG signing - * tool as is; eg., value of <code>user.signingkey</code>) - * @param committer - * the signing identity (to help with key lookup in case signing - * key is not specified) - * @param credentialsProvider - * provider to use when querying for signing key credentials (eg. - * passphrase) - * @return <code>true</code> if a signing key is available, - * <code>false</code> otherwise - * @throws CanceledException - * when signing was canceled (eg., user aborted when entering - * passphrase) - */ - public abstract boolean canLocateSigningKey(@Nullable String gpgSigningKey, - @NonNull PersonIdent committer, - CredentialsProvider credentialsProvider) throws CanceledException; - -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java index df9fd47efa..a99c64701f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java @@ -122,6 +122,8 @@ public class IndexDiff { } /** + * Whether there is a "base" stage entry + * * @return whether there is a "base" stage entry */ public boolean hasBase() { @@ -129,6 +131,8 @@ public class IndexDiff { } /** + * Whether there is an "ours" stage entry + * * @return whether there is an "ours" stage entry */ public boolean hasOurs() { @@ -136,6 +140,8 @@ public class IndexDiff { } /** + * Whether there is a "theirs" stage entry + * * @return whether there is a "theirs" stage entry */ public boolean hasTheirs() { @@ -265,6 +271,7 @@ public class IndexDiff { * @param workingTreeIterator * iterator for working directory * @throws java.io.IOException + * if an IO error occurred */ public IndexDiff(Repository repository, String revstr, WorkingTreeIterator workingTreeIterator) throws IOException { @@ -281,6 +288,7 @@ public class IndexDiff { * @param workingTreeIterator * iterator for working directory * @throws java.io.IOException + * if an IO error occurred */ public IndexDiff(Repository repository, ObjectId objectId, WorkingTreeIterator workingTreeIterator) throws IOException { @@ -312,6 +320,8 @@ public class IndexDiff { */ public interface WorkingTreeIteratorFactory { /** + * Get working tree iterator + * * @param repo * the repository * @return working tree iterator @@ -325,6 +335,7 @@ public class IndexDiff { * Allows higher layers to set the factory for WorkingTreeIterators. * * @param wTreeIt + * working tree iterator factory * @since 3.6 */ public void setWorkingTreeItFactory(WorkingTreeIteratorFactory wTreeIt) { @@ -349,6 +360,7 @@ public class IndexDiff { * * @return if anything is different between index, tree, and workdir * @throws java.io.IOException + * if an IO error occurred */ public boolean diff() throws IOException { return diff(null); @@ -372,6 +384,7 @@ public class IndexDiff { * {@link RepositoryBuilder}. * @return if anything is different between index, tree, and workdir * @throws java.io.IOException + * if an IO error occurred * @since 5.6 */ public boolean diff(RepositoryBuilderFactory factory) @@ -395,9 +408,11 @@ public class IndexDiff { * number or estimated files in the working tree * @param estIndexSize * number of estimated entries in the cache - * @param title a {@link java.lang.String} object. + * @param title + * a {@link java.lang.String} object. * @return if anything is different between index, tree, and workdir * @throws java.io.IOException + * if an IO error occurred */ public boolean diff(final ProgressMonitor monitor, int estWorkTreeSize, int estIndexSize, final String title) @@ -436,6 +451,7 @@ public class IndexDiff { * {@link RepositoryBuilder}. * @return if anything is different between index, tree, and workdir * @throws java.io.IOException + * if an IO error occurred * @since 5.6 */ public boolean diff(ProgressMonitor monitor, int estWorkTreeSize, @@ -623,7 +639,7 @@ public class IndexDiff { // submodule repository in .git/modules doesn't // exist yet it isn't "missing". File gitDir = new File( - new File(repository.getDirectory(), + new File(repository.getCommonDirectory(), Constants.MODULES), subRepoPath); if (!gitDir.isDirectory()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/MutableObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/MutableObjectId.java index 4a712ba360..6a35dedcf5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/MutableObjectId.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/MutableObjectId.java @@ -242,7 +242,6 @@ public class MutableObjectId extends AnyObjectId { } } - /** {@inheritDoc} */ @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 index 127cca9d1b..0a4db2a5f6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/NullProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/NullProgressMonitor.java @@ -23,31 +23,26 @@ public class NullProgressMonitor implements ProgressMonitor { // Do not let others instantiate } - /** {@inheritDoc} */ @Override public void start(int totalTasks) { // Do not report. } - /** {@inheritDoc} */ @Override public void beginTask(String title, int totalWork) { // Do not report. } - /** {@inheritDoc} */ @Override public void update(int completed) { // Do not report. } - /** {@inheritDoc} */ @Override public boolean isCancelled() { return false; } - /** {@inheritDoc} */ @Override public void endTask() { // Do not report. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java index 4b7054f72b..064d89e084 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java @@ -72,7 +72,6 @@ public abstract class ObjectBuilder { * <li>create {@link GpgSignature} from payload</li> * <li>set {@link GpgSignature}</li> * </ol> - * </p> * * @param gpgSignature * the signature to set or {@code null} to unset diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java index 9e9ef882a3..1a6c8ad0d2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java @@ -116,46 +116,103 @@ public class ObjectChecker { * @since 4.2 */ public enum ErrorType { - // @formatter:off // These names match git-core so that fsck section keys also match. - /***/ NULL_SHA1, - /***/ DUPLICATE_ENTRIES, - /***/ TREE_NOT_SORTED, - /***/ ZERO_PADDED_FILEMODE, - /***/ EMPTY_NAME, - /***/ FULL_PATHNAME, - /***/ HAS_DOT, - /***/ HAS_DOTDOT, - /***/ HAS_DOTGIT, - /***/ BAD_OBJECT_SHA1, - /***/ BAD_PARENT_SHA1, - /***/ BAD_TREE_SHA1, - /***/ MISSING_AUTHOR, - /***/ MISSING_COMMITTER, - /***/ MISSING_OBJECT, - /***/ MISSING_TREE, - /***/ MISSING_TYPE_ENTRY, - /***/ MISSING_TAG_ENTRY, - /***/ BAD_DATE, - /***/ BAD_EMAIL, - /***/ BAD_TIMEZONE, - /***/ MISSING_EMAIL, - /***/ MISSING_SPACE_BEFORE_DATE, - /** @since 5.2 */ GITMODULES_BLOB, - /** @since 5.2 */ GITMODULES_LARGE, - /** @since 5.2 */ GITMODULES_NAME, - /** @since 5.2 */ GITMODULES_PARSE, - /** @since 5.2 */ GITMODULES_PATH, - /** @since 5.2 */ GITMODULES_SYMLINK, - /** @since 5.2 */ GITMODULES_URL, - /***/ UNKNOWN_TYPE, + // See https://git-scm.com/docs/git-fsck#_fsck_messages + /** (WARN) Tree contains entries pointing to a null sha1. */ + NULL_SHA1, + /** (ERROR) A tree contains duplicate file entries. */ + DUPLICATE_ENTRIES, + /** (ERROR) A tree is not properly sorted. */ + TREE_NOT_SORTED, + /** (WARN) Found a zero padded filemode in a tree. */ + ZERO_PADDED_FILEMODE, + /** (WARN) A path contains an empty name. */ + EMPTY_NAME, + /** (WARN) A path contains the full path starting with "/". */ + FULL_PATHNAME, + /** (WARN) A tree contains an entry named . */ + HAS_DOT, + /** (WARN) A tree contains an entry named .. */ + HAS_DOTDOT, + /** (WARN) A tree contains an entry named .git */ + HAS_DOTGIT, + /** (ERROR) An object has a bad sha1. */ + BAD_OBJECT_SHA1, + /** (ERROR) A commit object has a bad parent sha1. */ + BAD_PARENT_SHA1, + /** (ERROR) A tree has an invalid format. */ + BAD_TREE_SHA1, + /** (ERROR) Author is missing. */ + MISSING_AUTHOR, + /** (ERROR) Committer is missing. */ + MISSING_COMMITTER, + /** (ERROR) Missing object line in tag object. */ + MISSING_OBJECT, + /** (ERROR) Missing tree line in a commit object. */ + MISSING_TREE, + /** (ERROR) Missing type line in a tag object. */ + MISSING_TYPE_ENTRY, + /** (ERROR) Missing tag line in a tag object. */ + MISSING_TAG_ENTRY, + /** (ERROR) Invalid date format in an author/committer line. */ + BAD_DATE, + /** (ERROR) Invalid email format in an author/committer line. */ + BAD_EMAIL, + /** (ERROR) Found an invalid time zone in an author/committer line. */ + BAD_TIMEZONE, + /** (ERROR) Email is missing in an author/committer line. */ + MISSING_EMAIL, + /** (ERROR) Missing space before date in an author/committer line. */ + MISSING_SPACE_BEFORE_DATE, + /** + * (ERROR) A non-blob found at .gitmodules. + * @since 5.2 + */ + GITMODULES_BLOB, + /** + * (ERROR) The .gitmodules file is too large to parse. + * @since 5.2 + */ + GITMODULES_LARGE, + /** + * (ERROR) A submodule name is invalid. + * @since 5.2 + */ + GITMODULES_NAME, + /** + * (INFO) Could not parse .gitmodules blob. + * @since 5.2 + */ + GITMODULES_PARSE, + /** + * (ERROR) .gitmodules path is invalid. + * @since 5.2 + */ + GITMODULES_PATH, + /** + * (ERROR) .gitmodules is a symlink. + * @since 5.2 + */ + GITMODULES_SYMLINK, + /** + * (ERROR) Found an invalid submodule url. + * @since 5.2 + */ + GITMODULES_URL, + /** (ERROR) Found an unknown object type. */ + UNKNOWN_TYPE, // These are unique to JGit. - /***/ WIN32_BAD_NAME, - /***/ BAD_UTF8; - // @formatter:on - - /** @return camelCaseVersion of the name. */ + /** (ERROR) Windows: Invalid name */ + WIN32_BAD_NAME, + /** (ERROR) Byte sequence is not a valid UTF-8 character */ + BAD_UTF8; + + /** + * Get camelCaseVersion of the name + * + * @return camelCaseVersion of the name. + */ public String getMessageId() { String n = name(); StringBuilder r = new StringBuilder(n.length()); @@ -1065,6 +1122,7 @@ public class ObjectChecker { * * @return true if the filename in buf could be a ".gitmodules" file * @throws CorruptObjectException + * if an object is corrupt */ private boolean isGitmodules(byte[] buf, int start, int end, @Nullable AnyObjectId id) throws CorruptObjectException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java index a39766cbd0..a8cc9a8529 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java @@ -77,9 +77,12 @@ public abstract class ObjectDatabase implements AutoCloseable { public abstract ObjectReader newReader(); /** + * Get the shallow commits of the current repository + * * @return the shallow commits of the current repository * - * @throws IOException the database could not be read + * @throws IOException + * the database could not be read * * @since 6.3 */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java index 269049f4a2..1b455b974d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import java.nio.ByteBuffer; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.InvalidObjectIdException; @@ -152,6 +153,22 @@ public class ObjectId extends AnyObjectId implements Serializable { } /** + * Convert an ObjectId from raw binary representation + * + * @param bb + * a bytebuffer with the objectid encoded as 5 consecutive ints. + * This is the reverse of {@link ObjectId#copyRawTo(ByteBuffer)} + * + * @return the converted object id. + * + * @since 7.0 + */ + public static final ObjectId fromRaw(ByteBuffer bb) { + return new ObjectId(bb.getInt(), bb.getInt(), bb.getInt(), bb.getInt(), + bb.getInt()); + } + + /** * Convert an ObjectId from raw binary representation. * * @param is @@ -262,7 +279,6 @@ public class ObjectId extends AnyObjectId implements Serializable { w5 = src.w5; } - /** {@inheritDoc} */ @Override public ObjectId toObjectId() { return this; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java index 29039097f1..a74fddff42 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java @@ -128,8 +128,11 @@ public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry> * <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, or use {@link #addIfAbsent(Entry)}. + * mapping prior to adding a new mapping, or use + * {@link #addIfAbsent(Entry)}. * + * @param <Q> + * type of values * @param newValue * the object to store. */ @@ -157,6 +160,8 @@ public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry> * boolean wasNew = map.addIfAbsent(obj) == obj; * </pre> * + * @param <Q> + * type of values * @param newValue * the object to store. * @return {@code newValue} if stored, or the prior value already stored and @@ -199,7 +204,6 @@ public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry> return size == 0; } - /** {@inheritDoc} */ @Override public Iterator<V> iterator() { return new Iterator<>() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java index a04ca6890c..c3c58372b1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java @@ -204,41 +204,35 @@ public abstract class ObjectIdRef implements Ref { this.updateIndex = updateIndex; } - /** {@inheritDoc} */ @Override @NonNull public String getName() { return name; } - /** {@inheritDoc} */ @Override public boolean isSymbolic() { return false; } - /** {@inheritDoc} */ @Override @NonNull public Ref getLeaf() { return this; } - /** {@inheritDoc} */ @Override @NonNull public Ref getTarget() { return this; } - /** {@inheritDoc} */ @Override @Nullable public ObjectId getObjectId() { return objectId; } - /** {@inheritDoc} */ @Override @NonNull public Storage getStorage() { @@ -257,7 +251,6 @@ public abstract class ObjectIdRef implements Ref { return updateIndex; } - /** {@inheritDoc} */ @NonNull @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java index 0e015b658b..d4acfd4714 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java @@ -96,6 +96,8 @@ public class ObjectIdSubclassMap<V extends ObjectId> * mapping prior to adding a new mapping, or use * {@link #addIfAbsent(ObjectId)}. * + * @param <Q> + * type of values * @param newValue * the object to store. */ @@ -117,6 +119,8 @@ public class ObjectIdSubclassMap<V extends ObjectId> * boolean wasNew = map.addIfAbsent(obj) == obj; * </pre> * + * @param <Q> + * type of values * @param newValue * the object to store. * @return {@code newValue} if stored, or the prior value already stored and @@ -162,7 +166,6 @@ public class ObjectIdSubclassMap<V extends ObjectId> return size == 0; } - /** {@inheritDoc} */ @Override public Iterator<V> iterator() { return new Iterator<>() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java index 000899d767..f6c52e292a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java @@ -66,7 +66,11 @@ public abstract class ObjectInserter implements AutoCloseable { /** Wraps a delegate ObjectInserter. */ public abstract static class Filter extends ObjectInserter { - /** @return delegate ObjectInserter to handle all processing. */ + /** + * Get delegate ObjectInserter to handle all processing + * + * @return delegate ObjectInserter to handle all processing. + */ protected abstract ObjectInserter delegate(); @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java index 67b8a2c249..d0564265dc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java @@ -302,6 +302,8 @@ public abstract class ObjectLoader { */ public abstract static class Filter extends ObjectLoader { /** + * Get delegate ObjectLoader to handle all processing. + * * @return delegate ObjectLoader to handle all processing. * @since 4.10 */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java index 69b2b5104e..b916d1e8e9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java @@ -245,12 +245,15 @@ public abstract class ObjectReader implements AutoCloseable { * * @return IDs of shallow commits * @throws java.io.IOException + * if an error occurred */ public abstract Set<ObjectId> getShallowCommits() throws IOException; /** * Asynchronous object opening. * + * @param <T> + * type of {@code ObjectId} * @param objectIds * objects to open from the object store. The supplied collection * must not be modified until the queue has finished. @@ -370,6 +373,8 @@ public abstract class ObjectReader implements AutoCloseable { /** * Asynchronous object size lookup. * + * @param <T> + * type of {@code ObjectId} * @param objectIds * objects to get the size of from the object store. The supplied * collection must not be modified until the queue has finished. @@ -578,6 +583,8 @@ public abstract class ObjectReader implements AutoCloseable { */ public abstract static class Filter extends ObjectReader { /** + * Get delegate ObjectReader to handle all processing + * * @return delegate ObjectReader to handle all processing. * @since 4.4 */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java index 93710299b4..50f4a83b93 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java @@ -12,10 +12,15 @@ package org.eclipse.jgit.lib; +import static java.time.ZoneOffset.UTC; + import java.io.Serializable; -import java.text.SimpleDateFormat; +import java.time.DateTimeException; import java.time.Instant; import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -40,7 +45,9 @@ public class PersonIdent implements Serializable { * timezone offset as in {@link #getTimeZoneOffset()}. * @return time zone object for the given offset. * @since 4.1 + * @deprecated use {@link #getZoneId(int)} instead */ + @Deprecated(since = "7.2") public static TimeZone getTimeZone(int tzOffset) { StringBuilder tzId = new StringBuilder(8); tzId.append("GMT"); //$NON-NLS-1$ @@ -49,6 +56,21 @@ public class PersonIdent implements Serializable { } /** + * Translate a minutes offset into a ZoneId + * + * @param tzOffset as minutes east of UTC + * @return a ZoneId for this offset (UTC if invalid) + * @since 7.1 + */ + public static ZoneId getZoneId(int tzOffset) { + try { + return ZoneOffset.ofHoursMinutes(tzOffset / 60, tzOffset % 60); + } catch (DateTimeException e) { + return UTC; + } + } + + /** * Format a timezone offset. * * @param r @@ -121,13 +143,17 @@ public class PersonIdent implements Serializable { } } + // Write offsets as [+-]HHMM + private static final DateTimeFormatter OFFSET_FORMATTER = DateTimeFormatter + .ofPattern("Z", Locale.US); //$NON-NLS-1$ + private final String name; private final String emailAddress; - private final long when; + private final Instant when; - private final int tzOffset; + private final ZoneId tzOffset; /** * Creates new PersonIdent from config info in repository, with current time. @@ -160,7 +186,7 @@ public class PersonIdent implements Serializable { * a {@link java.lang.String} object. */ public PersonIdent(String aName, String aEmailAddress) { - this(aName, aEmailAddress, SystemReader.getInstance().getCurrentTime()); + this(aName, aEmailAddress, SystemReader.getInstance().now()); } /** @@ -177,7 +203,7 @@ public class PersonIdent implements Serializable { */ public PersonIdent(String aName, String aEmailAddress, ProposedTimestamp when) { - this(aName, aEmailAddress, when.millis()); + this(aName, aEmailAddress, when.instant()); } /** @@ -189,8 +215,25 @@ public class PersonIdent implements Serializable { * local time * @param tz * time zone + * @deprecated Use {@link #PersonIdent(PersonIdent, Instant, ZoneId)} instead. */ + @Deprecated(since = "7.1") public PersonIdent(PersonIdent pi, Date when, TimeZone tz) { + this(pi.getName(), pi.getEmailAddress(), when.toInstant(), tz.toZoneId()); + } + + /** + * Copy a PersonIdent, but alter the clone's time stamp + * + * @param pi + * original {@link org.eclipse.jgit.lib.PersonIdent} + * @param when + * local time + * @param tz + * time zone offset + * @since 7.1 + */ + public PersonIdent(PersonIdent pi, Instant when, ZoneId tz) { this(pi.getName(), pi.getEmailAddress(), when, tz); } @@ -202,9 +245,12 @@ public class PersonIdent implements Serializable { * original {@link org.eclipse.jgit.lib.PersonIdent} * @param aWhen * local time + * @deprecated Use the variant with an Instant instead */ + @Deprecated(since = "7.1") public PersonIdent(PersonIdent pi, Date aWhen) { - this(pi.getName(), pi.getEmailAddress(), aWhen.getTime(), pi.tzOffset); + this(pi.getName(), pi.getEmailAddress(), aWhen.toInstant(), + pi.tzOffset); } /** @@ -218,7 +264,7 @@ public class PersonIdent implements Serializable { * @since 6.1 */ public PersonIdent(PersonIdent pi, Instant aWhen) { - this(pi.getName(), pi.getEmailAddress(), aWhen.toEpochMilli(), pi.tzOffset); + this(pi.getName(), pi.getEmailAddress(), aWhen, pi.tzOffset); } /** @@ -230,11 +276,12 @@ public class PersonIdent implements Serializable { * local time stamp * @param aTZ * time zone + * @deprecated Use the variant with Instant and ZoneId instead */ + @Deprecated(since = "7.1") public PersonIdent(final String aName, final String aEmailAddress, final Date aWhen, final TimeZone aTZ) { - this(aName, aEmailAddress, aWhen.getTime(), aTZ.getOffset(aWhen - .getTime()) / (60 * 1000)); + this(aName, aEmailAddress, aWhen.toInstant(), aTZ.toZoneId()); } /** @@ -252,10 +299,16 @@ public class PersonIdent implements Serializable { */ public PersonIdent(final String aName, String aEmailAddress, Instant aWhen, ZoneId zoneId) { - this(aName, aEmailAddress, aWhen.toEpochMilli(), - TimeZone.getTimeZone(zoneId) - .getOffset(aWhen - .toEpochMilli()) / (60 * 1000)); + if (aName == null) + throw new IllegalArgumentException( + JGitText.get().personIdentNameNonNull); + if (aEmailAddress == null) + throw new IllegalArgumentException( + JGitText.get().personIdentEmailNonNull); + name = aName; + emailAddress = aEmailAddress; + when = aWhen; + tzOffset = zoneId; } /** @@ -267,15 +320,18 @@ public class PersonIdent implements Serializable { * local time stamp * @param aTZ * time zone + * @deprecated Use the variant with Instant and ZoneId instead */ + @Deprecated(since = "7.1") public PersonIdent(PersonIdent pi, long aWhen, int aTZ) { - this(pi.getName(), pi.getEmailAddress(), aWhen, aTZ); + this(pi.getName(), pi.getEmailAddress(), Instant.ofEpochMilli(aWhen), + getZoneId(aTZ)); } private PersonIdent(final String aName, final String aEmailAddress, - long when) { + Instant when) { this(aName, aEmailAddress, when, SystemReader.getInstance() - .getTimezone(when)); + .getTimeZoneAt(when)); } private PersonIdent(UserConfig config) { @@ -298,19 +354,12 @@ public class PersonIdent implements Serializable { * local time stamp * @param aTZ * time zone + * @deprecated Use the variant with Instant and ZoneId instead */ + @Deprecated(since = "7.1") public PersonIdent(final String aName, final String aEmailAddress, final long aWhen, final int aTZ) { - if (aName == null) - throw new IllegalArgumentException( - JGitText.get().personIdentNameNonNull); - if (aEmailAddress == null) - throw new IllegalArgumentException( - JGitText.get().personIdentEmailNonNull); - name = aName; - emailAddress = aEmailAddress; - when = aWhen; - tzOffset = aTZ; + this(aName, aEmailAddress, Instant.ofEpochMilli(aWhen), getZoneId(aTZ)); } /** @@ -335,9 +384,12 @@ public class PersonIdent implements Serializable { * Get timestamp * * @return timestamp + * + * @deprecated Use getWhenAsInstant instead */ + @Deprecated(since = "7.1") public Date getWhen() { - return new Date(when); + return Date.from(when); } /** @@ -347,16 +399,19 @@ public class PersonIdent implements Serializable { * @since 6.1 */ public Instant getWhenAsInstant() { - return Instant.ofEpochMilli(when); + return when; } /** * Get this person's declared time zone * * @return this person's declared time zone; null if time zone is unknown. + * + * @deprecated Use getZoneId instead */ + @Deprecated(since = "7.1") public TimeZone getTimeZone() { - return getTimeZone(tzOffset); + return TimeZone.getTimeZone(tzOffset); } /** @@ -366,7 +421,17 @@ public class PersonIdent implements Serializable { * @since 6.1 */ public ZoneId getZoneId() { - return getTimeZone().toZoneId(); + return tzOffset; + } + + /** + * Return the offset in this timezone at the specific time + * + * @return the offset + * @since 7.1 + */ + public ZoneOffset getZoneOffset() { + return tzOffset.getRules().getOffset(when); } /** @@ -374,9 +439,11 @@ public class PersonIdent implements Serializable { * * @return this person's declared time zone as minutes east of UTC. If the * timezone is to the west of UTC it is negative. + * @deprecated Use {@link #getZoneOffset()} and read minutes from there */ + @Deprecated(since = "7.1") public int getTimeZoneOffset() { - return tzOffset; + return getZoneOffset().getTotalSeconds() / 60; } /** @@ -388,18 +455,19 @@ public class PersonIdent implements Serializable { public int hashCode() { int hc = getEmailAddress().hashCode(); hc *= 31; - hc += (int) (when / 1000L); + hc += when.hashCode(); return hc; } - /** {@inheritDoc} */ @Override public boolean equals(Object o) { if (o instanceof PersonIdent) { final PersonIdent p = (PersonIdent) o; return getName().equals(p.getName()) && getEmailAddress().equals(p.getEmailAddress()) - && when / 1000L == p.when / 1000L; + // commmit timestamps are stored with 1 second precision + && when.truncatedTo(ChronoUnit.SECONDS) + .equals(p.when.truncatedTo(ChronoUnit.SECONDS)); } return false; } @@ -415,30 +483,26 @@ public class PersonIdent implements Serializable { r.append(" <"); //$NON-NLS-1$ appendSanitized(r, getEmailAddress()); r.append("> "); //$NON-NLS-1$ - r.append(when / 1000); + r.append(when.toEpochMilli() / 1000); r.append(' '); - appendTimezone(r, tzOffset); + r.append(OFFSET_FORMATTER.format(getZoneOffset())); return r.toString(); } - /** {@inheritDoc} */ @Override @SuppressWarnings("nls") public String toString() { final StringBuilder r = new StringBuilder(); - final SimpleDateFormat dtfmt; - dtfmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US); - dtfmt.setTimeZone(getTimeZone()); - + DateTimeFormatter dtfmt = DateTimeFormatter + .ofPattern("EEE MMM d HH:mm:ss yyyy Z", Locale.US) //$NON-NLS-1$ + .withZone(tzOffset); r.append("PersonIdent["); r.append(getName()); r.append(", "); r.append(getEmailAddress()); r.append(", "); - r.append(dtfmt.format(Long.valueOf(when))); + r.append(dtfmt.format(when)); r.append("]"); - return r.toString(); } } - diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java index a4cd1d6894..b036a0b6a1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java @@ -17,7 +17,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import org.eclipse.jgit.lib.RebaseTodoLine.Action; @@ -55,13 +55,14 @@ public class RebaseTodoFile { * <code>true</code> if also comments should be reported * @return the list of steps * @throws java.io.IOException + * if an IO error occurred */ public List<RebaseTodoLine> readRebaseTodo(String path, boolean includeComments) throws IOException { byte[] buf = IO.readFully(new File(repo.getDirectory(), path)); int ptr = 0; int tokenBegin = 0; - List<RebaseTodoLine> r = new LinkedList<>(); + List<RebaseTodoLine> r = new ArrayList<>(); while (ptr < buf.length) { tokenBegin = ptr; ptr = RawParseUtils.nextLF(buf, ptr); @@ -126,8 +127,11 @@ public class RebaseTodoFile { * Skip leading space, tab, CR and LF characters * * @param buf + * byte buffer * @param tokenBegin + * index of token begin * @param lineEnd + * index of line end * @return the token within the range of the given {@code buf} that doesn't * need to be skipped, {@code -1} if no such token found within the * range (i.e. empty line) @@ -193,6 +197,7 @@ public class RebaseTodoFile { * @param append * whether to append to an existing file or to write a new file * @throws java.io.IOException + * if an IO error occurred */ public void writeRebaseTodoFile(String path, List<RebaseTodoLine> steps, boolean append) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java index 8b778497fc..082b324d1a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java @@ -57,6 +57,8 @@ public class RebaseTodoLine { } /** + * Get full action token name + * * @return full action token name */ public String toToken() { @@ -69,7 +71,10 @@ public class RebaseTodoLine { } /** + * Parse a token + * * @param token + * token to parse * @return the Action */ public static Action parse(String token) { @@ -245,7 +250,6 @@ public class RebaseTodoLine { return comment; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java index 218c0a8e8e..9c374760b0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java @@ -78,6 +78,8 @@ public interface Ref { } /** + * Whether this storage has a loose file + * * @return true if this storage has a loose file. */ public boolean isLoose() { @@ -85,6 +87,8 @@ public interface Ref { } /** + * Whether this storage is inside the packed file + * * @return true if this storage is inside the packed file. */ public boolean isPacked() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java index 3ab649341f..c4c0001d3e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java @@ -27,7 +27,6 @@ public class RefComparator implements Comparator<Ref> { /** Singleton instance of RefComparator */ public static final RefComparator INSTANCE = new RefComparator(); - /** {@inheritDoc} */ @Override public int compare(Ref o1, Ref o2) { return compareTo(o1, o2); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java index 98089fb8fd..49d5224325 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java @@ -26,6 +26,7 @@ import java.util.stream.Stream; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.api.PackRefsCommand; /** * Abstraction of name to {@link org.eclipse.jgit.lib.ObjectId} mapping. @@ -136,6 +137,7 @@ public abstract class RefDatabase { * with the passed ref name; empty collection when there are no * conflicts * @throws java.io.IOException + * if an error occurred * @since 2.3 * @see #isNameConflicting(String) */ @@ -159,7 +161,7 @@ public abstract class RefDatabase { if (existing.startsWith(prefix)) conflicting.add(existing); - return conflicting; + return Collections.unmodifiableList(conflicting); } /** @@ -237,23 +239,6 @@ public abstract class RefDatabase { } /** - * Compatibility synonym for {@link #findRef(String)}. - * - * @param name - * the name of the reference. May be a short name which must be - * searched for using the standard {@link #SEARCH_PATH}. - * @return the reference (if it exists); else {@code null}. - * @throws IOException - * the reference space cannot be accessed. - * @deprecated Use {@link #findRef(String)} instead. - */ - @Deprecated - @Nullable - public final Ref getRef(String name) throws IOException { - return findRef(name); - } - - /** * Read a single reference. * <p> * Aside from taking advantage of {@link #SEARCH_PATH}, this method may be @@ -371,6 +356,40 @@ public abstract class RefDatabase { } /** + * Get the reflog reader + * + * @param refName + * a {@link java.lang.String} object. + * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied + * refname, or {@code null} if the named ref does not exist. + * @throws java.io.IOException + * the ref could not be accessed. + * @since 7.2 + */ + @Nullable + public ReflogReader getReflogReader(String refName) throws IOException { + Ref ref = exactRef(refName); + if (ref == null) { + return null; + } + return getReflogReader(ref); + } + + /** + * Get the reflog reader. + * + * @param ref + * a Ref + * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied ref. + * @throws IOException + * if an IO error occurred + * @since 7.2 + */ + @NonNull + public abstract ReflogReader getReflogReader(@NonNull Ref ref) + throws IOException; + + /** * Get a section of the reference namespace. * * @param prefix @@ -440,7 +459,7 @@ public abstract class RefDatabase { * @param excludes * strings that names of refs can't start with; may be empty. * @return immutable list of refs whose names start with {@code prefix} and - * none of the strings in {@code exclude}. + * none of the strings in {@code excludes}. * @throws java.io.IOException * the reference space cannot be accessed. * @since 5.11 @@ -609,4 +628,22 @@ public abstract class RefDatabase { } return null; } + + /** + * Optimize pack ref storage. + * + * @param pm + * a progress monitor + * + * @param packRefs + * {@link PackRefsCommand} to control ref packing behavior + * + * @throws java.io.IOException + * if an IO error occurred + * @since 7.1 + */ + public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs) + throws IOException { + // nothing + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java index cdd4e4ba75..e8821d0cbb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java @@ -122,6 +122,7 @@ public abstract class RefRename { * * @return the result of the new ref update * @throws java.io.IOException + * if an IO error occurred */ public Result rename() throws IOException { try { @@ -138,6 +139,7 @@ public abstract class RefRename { * * @return the result of the rename operation. * @throws java.io.IOException + * if an IO error occurred */ protected abstract Result doRename() throws IOException; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java index d1be63b966..d113243bea 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java @@ -115,7 +115,6 @@ public abstract class RefUpdate { /** * The ref was renamed from another name - * <p> */ RENAMED, @@ -245,6 +244,7 @@ public abstract class RefUpdate { * a {@link org.eclipse.jgit.lib.RefUpdate.Result} object. * @return {@code result} * @throws java.io.IOException + * if an IO error occurred */ protected abstract Result doUpdate(Result desiredResult) throws IOException; @@ -255,6 +255,7 @@ public abstract class RefUpdate { * a {@link org.eclipse.jgit.lib.RefUpdate.Result} object. * @return {@code result} * @throws java.io.IOException + * if an IO error occurred */ protected abstract Result doDelete(Result desiredResult) throws IOException; @@ -265,6 +266,7 @@ public abstract class RefUpdate { * a {@link java.lang.String} object. * @return {@link org.eclipse.jgit.lib.RefUpdate.Result#NEW} on success. * @throws java.io.IOException + * if an IO error occurred */ protected abstract Result doLink(String target) throws IOException; @@ -612,6 +614,7 @@ public abstract class RefUpdate { * * @return the result status of the delete. * @throws java.io.IOException + * if an IO error occurred */ public Result delete() throws IOException { try (RevWalk rw = new RevWalk(getRepository())) { @@ -628,6 +631,7 @@ public abstract class RefUpdate { * the merge test. The walk will be reset to perform the test. * @return the result status of the delete. * @throws java.io.IOException + * if an IO error occurred */ public Result delete(RevWalk walk) throws IOException { final String myName = detachingSymbolicRef @@ -668,6 +672,7 @@ public abstract class RefUpdate { * @return {@link org.eclipse.jgit.lib.RefUpdate.Result#NEW} or * {@link org.eclipse.jgit.lib.RefUpdate.Result#FORCED} on success. * @throws java.io.IOException + * if an IO error occurred */ public Result link(String target) throws IOException { if (!target.startsWith(Constants.R_REFS)) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java index d2c3f9de68..41917f8b57 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java @@ -177,6 +177,7 @@ public abstract class RefWriter { * @param content * byte content of file to be written. * @throws java.io.IOException + * if an IO error occurred */ 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 index 1e6a661572..5233bfc109 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java @@ -25,6 +25,7 @@ public interface ReflogReader { * * @return the latest reflog entry, or null if no log * @throws java.io.IOException + * if an IO error occurred */ ReflogEntry getLastEntry() throws IOException; @@ -33,6 +34,7 @@ public interface ReflogReader { * * @return all reflog entries in reverse order * @throws java.io.IOException + * if an IO error occurred */ List<ReflogEntry> getReverseEntries() throws IOException; @@ -40,9 +42,11 @@ public interface ReflogReader { * Get specific entry in the reflog relative to the last entry which is * considered entry zero. * - * @param number a int. + * @param number + * a int. * @return reflog entry or null if not found * @throws java.io.IOException + * if an IO error occurred */ ReflogEntry getReverseEntry(int number) throws IOException; @@ -53,6 +57,7 @@ public interface ReflogReader { * max number of entries to read * @return all reflog entries in reverse order * @throws java.io.IOException + * if an IO error occurred */ List<ReflogEntry> getReverseEntries(int max) throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index db2571c673..c9dc6da4ba 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -15,8 +15,8 @@ package org.eclipse.jgit.lib; -import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX; import java.io.BufferedOutputStream; import java.io.File; @@ -26,17 +26,21 @@ import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.LinkOption; import java.text.MessageFormat; +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.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import org.eclipse.jgit.annotations.NonNull; @@ -113,9 +117,12 @@ public abstract class Repository implements AutoCloseable { final AtomicLong closedAt = new AtomicLong(); - /** Metadata directory holding the repository's critical files. */ + /** $GIT_DIR: metadata directory holding the repository's critical files. */ private final File gitDir; + /** $GIT_COMMON_DIR: metadata directory holding the common repository's critical files. */ + private final File gitCommonDir; + /** File abstraction used to resolve paths. */ private final FS fs; @@ -129,6 +136,8 @@ public abstract class Repository implements AutoCloseable { private final String initialBranch; + private final AtomicReference<Boolean> caseInsensitiveWorktree = new AtomicReference<>(); + /** * Initialize a new repository instance. * @@ -137,6 +146,7 @@ public abstract class Repository implements AutoCloseable { */ protected Repository(BaseRepositoryBuilder options) { gitDir = options.getGitDir(); + gitCommonDir = options.getGitCommonDir(); fs = options.getFS(); workTree = options.getWorkTree(); indexFile = options.getIndexFile(); @@ -175,6 +185,7 @@ public abstract class Repository implements AutoCloseable { * the same as {@code create(false)}. * * @throws java.io.IOException + * if an IO error occurred * @see #create(boolean) */ public void create() throws IOException { @@ -219,6 +230,16 @@ public abstract class Repository implements AutoCloseable { public abstract String getIdentifier(); /** + * Get common dir. + * + * @return $GIT_COMMON_DIR: local common metadata directory; + * @since 7.0 + */ + public File getCommonDirectory() { + return gitCommonDir; + } + + /** * Get the object database which stores this repository's data. * * @return the object database which stores this repository's data. @@ -292,25 +313,6 @@ public abstract class Repository implements AutoCloseable { } /** - * Whether the specified object is stored in this repo or any of the known - * shared repositories. - * - * @param objectId - * a {@link org.eclipse.jgit.lib.AnyObjectId} object. - * @return true if the specified object is stored in this repo or any of the - * known shared repositories. - * @deprecated use {@code getObjectDatabase().has(objectId)} - */ - @Deprecated - public boolean hasObject(AnyObjectId objectId) { - try { - return getObjectDatabase().has(objectId); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - /** * Open an object from this repository. * <p> * This is a one-shot call interface which may be faster than allocating a @@ -484,11 +486,14 @@ public abstract class Repository implements AutoCloseable { * Thus this method can be used to process an expression to a method that * expects a branch or revision id. * - * @param revstr a {@link java.lang.String} object. + * @param revstr + * a {@link java.lang.String} object. * @return object id or ref name from resolved expression or {@code null} if * given expression cannot be resolved * @throws org.eclipse.jgit.errors.AmbiguousObjectException + * if a shortened ObjectId was ambiguous * @throws java.io.IOException + * if an IO error occurred */ @Nullable public String simplify(String revstr) @@ -968,7 +973,6 @@ public abstract class Repository implements AutoCloseable { getRefDatabase().close(); } - /** {@inheritDoc} */ @Override @NonNull public String toString() { @@ -999,6 +1003,7 @@ public abstract class Repository implements AutoCloseable { * {@code null} if the repository is corrupt and has no HEAD * reference. * @throws java.io.IOException + * if an IO error occurred */ @Nullable public String getFullBranch() throws IOException { @@ -1027,6 +1032,7 @@ public abstract class Repository implements AutoCloseable { * in hex format if the current branch is detached, or {@code null} * if the repository is corrupt and has no HEAD reference. * @throws java.io.IOException + * if an IO error occurred */ @Nullable public String getBranch() throws IOException { @@ -1056,6 +1062,7 @@ public abstract class Repository implements AutoCloseable { * * @return unmodifiable collection of other known objects. * @throws IOException + * if an IO error occurred */ @NonNull public Set<ObjectId> getAdditionalHaves() throws IOException { @@ -1066,11 +1073,12 @@ public abstract class Repository implements AutoCloseable { * Get a ref by name. * * @param name - * the name of the ref to lookup. Must not be a short-hand - * form; e.g., "master" is not automatically expanded to + * the name of the ref to lookup. Must not be a short-hand form; + * e.g., "master" is not automatically expanded to * "refs/heads/master". * @return the Ref with the given name, or {@code null} if it does not exist * @throws java.io.IOException + * if an IO error occurred * @since 4.2 */ @Nullable @@ -1087,6 +1095,7 @@ public abstract class Repository implements AutoCloseable { * "refs/heads/master" if "refs/heads/master" already exists. * @return the Ref with the given name, or {@code null} if it does not exist * @throws java.io.IOException + * if an IO error occurred * @since 4.2 */ @Nullable @@ -1142,11 +1151,9 @@ public abstract class Repository implements AutoCloseable { * new Ref object representing the same data as Ref, but isPeeled() * will be true and getPeeledObjectId will contain the peeled object * (or null). - * @deprecated use {@code getRefDatabase().peel(ref)} instead. */ - @Deprecated @NonNull - public Ref peel(Ref ref) { + private Ref peel(Ref ref) { try { return getRefDatabase().peel(ref); } catch (IOException e) { @@ -1162,6 +1169,7 @@ public abstract class Repository implements AutoCloseable { * * @return a map with all objects referenced by a peeled ref. * @throws IOException + * if an IO error occurred */ @NonNull public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() @@ -1575,10 +1583,45 @@ public abstract class Repository implements AutoCloseable { } /** + * Tells whether the work tree is on a case-insensitive file system. + * + * @return {@code true} if the work tree is case-insensitive; {@code false} + * otherwise + * @throws NoWorkTreeException + * if the repository is bare + * @since 7.2 + */ + public boolean isWorkTreeCaseInsensitive() throws NoWorkTreeException { + Boolean flag = caseInsensitiveWorktree.get(); + if (flag == null) { + File directory = getWorkTree(); + // See if we can find ".git" also as ".GIT". + File dotGit = new File(directory, Constants.DOT_GIT); + if (Files.exists(dotGit.toPath(), LinkOption.NOFOLLOW_LINKS)) { + dotGit = new File(directory, + Constants.DOT_GIT.toUpperCase(Locale.ROOT)); + flag = Boolean.valueOf(Files.exists(dotGit.toPath(), + LinkOption.NOFOLLOW_LINKS)); + } else { + // Fall back to a mostly sane default. On Mac, HFS+ and APFS + // partitions are case-insensitive by default but can be + // configured to be case-sensitive. + SystemReader system = SystemReader.getInstance(); + flag = Boolean.valueOf(system.isWindows() || system.isMacOS()); + } + if (!caseInsensitiveWorktree.compareAndSet(null, flag)) { + flag = caseInsensitiveWorktree.get(); + } + } + return flag.booleanValue(); + } + + /** * Force a scan for changed refs. Fires an IndexChangedEvent(false) if * changes are detected. * * @throws java.io.IOException + * if an IO error occurred */ public abstract void scanForRepoChanges() throws IOException; @@ -1689,10 +1732,13 @@ public abstract class Repository implements AutoCloseable { * @throws java.io.IOException * the ref could not be accessed. * @since 3.0 + * @deprecated use {@code #getRefDatabase().getReflogReader(String)} instead */ + @Deprecated(since = "7.2") @Nullable - public abstract ReflogReader getReflogReader(String refName) - throws IOException; + public ReflogReader getReflogReader(String refName) throws IOException { + return getRefDatabase().getReflogReader(refName); + } /** * Get the reflog reader. Subclasses should override this method and provide @@ -1700,14 +1746,17 @@ public abstract class Repository implements AutoCloseable { * * @param ref * a Ref - * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied ref, - * or {@code null} if the ref does not exist. + * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied ref. * @throws IOException + * if an IO error occurred * @since 5.13.2 + * @deprecated use {@code #getRefDatabase().getReflogReader(Ref)} instead */ - public @Nullable ReflogReader getReflogReader(@NonNull Ref ref) + @Deprecated(since = "7.2") + @NonNull + public ReflogReader getReflogReader(@NonNull Ref ref) throws IOException { - return getReflogReader(ref.getName()); + return getRefDatabase().getReflogReader(ref); } /** @@ -1718,6 +1767,7 @@ public abstract class Repository implements AutoCloseable { * @return a String containing the content of the MERGE_MSG file or * {@code null} if this file doesn't exist * @throws java.io.IOException + * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. @@ -1737,6 +1787,7 @@ public abstract class Repository implements AutoCloseable { * the message which should be written or <code>null</code> to * delete the file * @throws java.io.IOException + * if an IO error occurred */ public void writeMergeCommitMsg(String msg) throws IOException { File mergeMsgFile = new File(gitDir, Constants.MERGE_MSG); @@ -1751,6 +1802,7 @@ public abstract class Repository implements AutoCloseable { * @return a String containing the content of the COMMIT_EDITMSG file or * {@code null} if this file doesn't exist * @throws java.io.IOException + * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. @@ -1770,6 +1822,7 @@ public abstract class Repository implements AutoCloseable { * the message which should be written or {@code null} to delete * the file * @throws java.io.IOException + * if an IO error occurred * @since 4.0 */ public void writeCommitEditMsg(String msg) throws IOException { @@ -1786,6 +1839,7 @@ public abstract class Repository implements AutoCloseable { * {@code null} if this file doesn't exist. Also if the file exists * but is empty {@code null} will be returned * @throws java.io.IOException + * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. @@ -1799,7 +1853,7 @@ public abstract class Repository implements AutoCloseable { if (raw == null) return null; - LinkedList<ObjectId> heads = new LinkedList<>(); + List<ObjectId> heads = new ArrayList<>(); for (int p = 0; p < raw.length;) { heads.add(ObjectId.fromString(raw, p)); p = RawParseUtils @@ -1818,6 +1872,7 @@ public abstract class Repository implements AutoCloseable { * a list of commits which IDs should be written to * $GIT_DIR/MERGE_HEAD or <code>null</code> to delete the file * @throws java.io.IOException + * if an IO error occurred */ public void writeMergeHeads(List<? extends ObjectId> heads) throws IOException { writeHeadsFile(heads, Constants.MERGE_HEAD); @@ -1830,6 +1885,7 @@ public abstract class Repository implements AutoCloseable { * doesn't exist. Also if the file exists but is empty {@code null} * will be returned * @throws java.io.IOException + * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. @@ -1854,6 +1910,7 @@ public abstract class Repository implements AutoCloseable { * doesn't exist. Also if the file exists but is empty {@code null} * will be returned * @throws java.io.IOException + * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. @@ -1877,6 +1934,7 @@ public abstract class Repository implements AutoCloseable { * an object id of the cherry commit or <code>null</code> to * delete the file * @throws java.io.IOException + * if an IO error occurred */ public void writeCherryPickHead(ObjectId head) throws IOException { List<ObjectId> heads = (head != null) ? Collections.singletonList(head) @@ -1892,6 +1950,7 @@ public abstract class Repository implements AutoCloseable { * an object id of the revert commit or <code>null</code> to * delete the file * @throws java.io.IOException + * if an IO error occurred */ public void writeRevertHead(ObjectId head) throws IOException { List<ObjectId> heads = (head != null) ? Collections.singletonList(head) @@ -1906,6 +1965,7 @@ public abstract class Repository implements AutoCloseable { * an object id of the original HEAD commit or <code>null</code> * to delete the file * @throws java.io.IOException + * if an IO error occurred */ public void writeOrigHead(ObjectId head) throws IOException { List<ObjectId> heads = head != null ? Collections.singletonList(head) @@ -1920,6 +1980,7 @@ public abstract class Repository implements AutoCloseable { * doesn't exist. Also if the file exists but is empty {@code null} * will be returned * @throws java.io.IOException + * if an IO error occurred * @throws org.eclipse.jgit.errors.NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. @@ -1941,6 +2002,7 @@ public abstract class Repository implements AutoCloseable { * @return a String containing the content of the SQUASH_MSG file or * {@code null} if this file doesn't exist * @throws java.io.IOException + * if an IO error occurred * @throws NoWorkTreeException * if this is bare, which implies it has no working directory. * See {@link #isBare()}. @@ -1960,6 +2022,7 @@ public abstract class Repository implements AutoCloseable { * the message which should be written or <code>null</code> to * delete the file * @throws java.io.IOException + * if an IO error occurred */ public void writeSquashCommitMsg(String msg) throws IOException { File squashMsgFile = new File(gitDir, Constants.SQUASH_MSG); @@ -1997,9 +2060,11 @@ public abstract class Repository implements AutoCloseable { * Read a file from the git directory. * * @param filename + * the file to read * @return the raw contents or {@code null} if the file doesn't exist or is * empty * @throws IOException + * if an IO error occurred */ private byte[] readGitDirectoryFile(String filename) throws IOException { File file = new File(getDirectory(), filename); @@ -2021,8 +2086,11 @@ public abstract class Repository implements AutoCloseable { * a list of object ids to write or null if the file should be * deleted. * @param filename + * name of the file to write heads to * @throws FileNotFoundException + * if the heads file couldn't be found * @throws IOException + * if an IO error occurred */ private void writeHeadsFile(List<? extends ObjectId> heads, String filename) throws FileNotFoundException, IOException { @@ -2052,6 +2120,7 @@ public abstract class Repository implements AutoCloseable { * <code>true</code> if also comments should be reported * @return the list of steps * @throws java.io.IOException + * if an IO error occurred * @since 3.2 */ @NonNull @@ -2072,6 +2141,7 @@ public abstract class Repository implements AutoCloseable { * @param append * whether to append to an existing file or to write a new file * @throws java.io.IOException + * if an IO error occurred * @since 3.2 */ public void writeRebaseTodoFile(String path, List<RebaseTodoLine> steps, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java index 41f291b57b..18366541da 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java @@ -382,6 +382,8 @@ public class RepositoryCache { private final FS fs; /** + * Create a pointer to a location on disk. + * * @param directory * exact location of the repository. * @param fs @@ -401,7 +403,11 @@ public class RepositoryCache { } } - /** @return location supplied to the constructor. */ + /** + * Get directory containing the repository database + * + * @return directory containing the repository database. + */ public final File getFile() { return path; } @@ -444,10 +450,21 @@ public class RepositoryCache { * Git directory. */ public static boolean isGitRepository(File dir, FS fs) { - return fs.resolve(dir, Constants.OBJECTS).exists() - && fs.resolve(dir, "refs").exists() //$NON-NLS-1$ - && (fs.resolve(dir, Constants.REFTABLE).exists() - || isValidHead(new File(dir, Constants.HEAD))); + // check if common-dir available or fallback to git-dir + File commonDir; + try { + commonDir = fs.getCommonDir(dir); + } catch (IOException e) { + commonDir = null; + } + if (commonDir == null) { + commonDir = dir; + } + return fs.resolve(commonDir, Constants.OBJECTS).exists() + && fs.resolve(commonDir, "refs").exists() //$NON-NLS-1$ + && (fs.resolve(commonDir, Constants.REFTABLE).exists() + || isValidHead( + new File(commonDir, Constants.HEAD))); } private static boolean isValidHead(File head) { @@ -490,15 +507,31 @@ public class RepositoryCache { * null if there is no suitable match. */ public static File resolve(File directory, FS fs) { - if (isGitRepository(directory, fs)) + // the folder itself + if (isGitRepository(directory, fs)) { return directory; - if (isGitRepository(new File(directory, Constants.DOT_GIT), fs)) - return new File(directory, Constants.DOT_GIT); - - final String name = directory.getName(); - final File parent = directory.getParentFile(); - if (isGitRepository(new File(parent, name + Constants.DOT_GIT_EXT), fs)) - return new File(parent, name + Constants.DOT_GIT_EXT); + } + // the .git subfolder or file (reference) + File dotDir = new File(directory, Constants.DOT_GIT); + if (dotDir.isFile()) { + try { + File refDir = BaseRepositoryBuilder.getSymRef(directory, + dotDir, fs); + if (refDir != null && isGitRepository(refDir, fs)) { + return refDir; + } + } catch (IOException ignored) { + // Continue searching if gitdir ref isn't found + } + } else if (isGitRepository(dotDir, fs)) { + return dotDir; + } + // the folder extended with .git (bare) + File bareDir = new File(directory.getParentFile(), + directory.getName() + Constants.DOT_GIT_EXT); + if (isGitRepository(bareDir, fs)) { + return bareDir; + } return null; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java index 98d719183d..eaa15fbc99 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java @@ -355,27 +355,37 @@ public enum RepositoryState { public abstract boolean canCheckout(); /** + * Whether we can commit + * * @return true if we can commit */ public abstract boolean canCommit(); /** + * Whether reset to another HEAD is considered SAFE + * * @return true if reset to another HEAD is considered SAFE */ public abstract boolean canResetHead(); /** + * Whether amending is considered SAFE + * * @return true if amending is considered SAFE */ public abstract boolean canAmend(); /** + * Whether a rebase is in progress + * * @return true if the repository is currently in a rebase * @since 3.0 */ public abstract boolean isRebasing(); /** + * Get a human readable description of the state + * * @return a human readable description of the state. */ public abstract String getDescription(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifier.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifier.java new file mode 100644 index 0000000000..2ce2708cb9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifier.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.util.Date; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.api.errors.JGitInternalException; + +/** + * A {@code SignatureVerifier} can verify signatures on git commits and tags. + * + * @since 7.0 + */ +public interface SignatureVerifier { + + /** + * Verifies a given signature for given data. + * + * @param repository + * the {@link Repository} the data comes from. + * @param config + * the {@link GpgConfig} + * @param data + * the signature is for + * @param signatureData + * the ASCII-armored signature + * @return a {@link SignatureVerification} describing the outcome + * @throws IOException + * if the signature cannot be parsed + * @throws JGitInternalException + * if signature verification fails + */ + SignatureVerification verify(@NonNull Repository repository, + @NonNull GpgConfig config, byte[] data, byte[] signatureData) + throws IOException; + + /** + * Retrieves the name of this verifier. This should be a short string + * identifying the engine that verified the signature, like "gpg" if GPG is + * used, or "bc" for a BouncyCastle implementation. + * + * @return the name + */ + @NonNull + String getName(); + + /** + * A {@link SignatureVerifier} may cache public keys to speed up + * verifying signatures on multiple objects. This clears this cache, if any. + */ + void clear(); + + /** + * A {@code SignatureVerification} returns data about a (positively or + * negatively) verified signature. + * + * @param verifierName + * the name of the verifier that created this verification result + * @param creationDate + * date and time the signature was created + * @param signer + * the signer as stored in the signature, or {@code null} if + * unknown + * @param keyFingerprint + * fingerprint of the public key, or {@code null} if unknown + * @param keyUser + * user associated with the key, or {@code null} if unknown + * @param verified + * whether the signature verification was successful + * @param expired + * whether the public key used for this signature verification + * was expired when the signature was created + * @param trustLevel + * the trust level of the public key used to verify the signature + * @param message + * human-readable message giving additional information about the + * outcome of the verification, possibly {@code null} + */ + record SignatureVerification( + String verifierName, + Date creationDate, + String signer, + String keyFingerprint, + String keyUser, + boolean verified, + boolean expired, + @NonNull TrustLevel trustLevel, + String message) { + } + + /** + * The owner's trust in a public key. + */ + enum TrustLevel { + UNKNOWN, NEVER, MARGINAL, FULL, ULTIMATE + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifierFactory.java new file mode 100644 index 0000000000..7844aba3bd --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifierFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import org.eclipse.jgit.annotations.NonNull; + +/** + * A factory for {@link SignatureVerifier}s. + * + * @since 7.0 + */ +public interface SignatureVerifierFactory { + + /** + * Tells what kind of {@link SignatureVerifier} this factory creates. + * + * @return the {@link GpgConfig.GpgFormat} of the signer + */ + @NonNull + GpgConfig.GpgFormat getType(); + + /** + * Creates a new instance of a {@link SignatureVerifier} that can produce + * signatures of type {@link #getType()}. + * + * @return a new {@link SignatureVerifier} + */ + @NonNull + SignatureVerifier create(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifiers.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifiers.java new file mode 100644 index 0000000000..01c8422b66 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifiers.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Map; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.util.RawParseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages the available signers. + * + * @since 7.0 + */ +public final class SignatureVerifiers { + + private static final Logger LOG = LoggerFactory.getLogger(SignatureVerifiers.class); + + private static final byte[] PGP_PREFIX = Constants.GPG_SIGNATURE_PREFIX + .getBytes(StandardCharsets.US_ASCII); + + private static final byte[] X509_PREFIX = Constants.CMS_SIGNATURE_PREFIX + .getBytes(StandardCharsets.US_ASCII); + + private static final byte[] SSH_PREFIX = Constants.SSH_SIGNATURE_PREFIX + .getBytes(StandardCharsets.US_ASCII); + + private static final Map<GpgConfig.GpgFormat, SignatureVerifierFactory> FACTORIES = loadSignatureVerifiers(); + + private static final Map<GpgConfig.GpgFormat, SignatureVerifier> VERIFIERS = new ConcurrentHashMap<>(); + + private static Map<GpgConfig.GpgFormat, SignatureVerifierFactory> loadSignatureVerifiers() { + Map<GpgConfig.GpgFormat, SignatureVerifierFactory> result = new EnumMap<>( + GpgConfig.GpgFormat.class); + try { + for (SignatureVerifierFactory factory : ServiceLoader + .load(SignatureVerifierFactory.class)) { + GpgConfig.GpgFormat format = factory.getType(); + SignatureVerifierFactory existing = result.get(format); + if (existing != null) { + LOG.warn("{}", //$NON-NLS-1$ + MessageFormat.format( + JGitText.get().signatureServiceConflict, + "SignatureVerifierFactory", format, //$NON-NLS-1$ + existing.getClass().getCanonicalName(), + factory.getClass().getCanonicalName())); + } else { + result.put(format, factory); + } + } + } catch (ServiceConfigurationError e) { + LOG.error(e.getMessage(), e); + } + return result; + } + + private SignatureVerifiers() { + // No instantiation + } + + /** + * Retrieves a {@link Signer} that can produce signatures of the given type + * {@code format}. + * + * @param format + * {@link GpgConfig.GpgFormat} the signer must support + * @return a {@link Signer}, or {@code null} if none is available + */ + public static SignatureVerifier get(@NonNull GpgConfig.GpgFormat format) { + return VERIFIERS.computeIfAbsent(format, f -> { + SignatureVerifierFactory factory = FACTORIES.get(format); + if (factory == null) { + return null; + } + return factory.create(); + }); + } + + /** + * Sets a specific signature verifier to use for a specific signature type. + * + * @param format + * signature type to set the {@code verifier} for + * @param verifier + * the {@link SignatureVerifier} to use for signatures of type + * {@code format}; if {@code null}, a default implementation, if + * available, may be used. + */ + public static void set(@NonNull GpgConfig.GpgFormat format, + SignatureVerifier verifier) { + SignatureVerifier previous; + if (verifier == null) { + previous = VERIFIERS.remove(format); + } else { + previous = VERIFIERS.put(format, verifier); + } + if (previous != null) { + previous.clear(); + } + } + + /** + * Verifies the signature on a signed commit or tag. + * + * @param repository + * the {@link Repository} the object is from + * @param config + * the {@link GpgConfig} to use + * @param object + * to verify + * @return a {@link SignatureVerifier.SignatureVerification} describing the + * outcome of the verification, or {@code null} if the object does + * not have a signature of a known type + * @throws IOException + * if an error occurs getting a public key + * @throws org.eclipse.jgit.api.errors.JGitInternalException + * if signature verification fails + */ + @Nullable + public static SignatureVerifier.SignatureVerification verify( + @NonNull Repository repository, @NonNull GpgConfig config, + @NonNull RevObject object) throws IOException { + if (object instanceof RevCommit) { + RevCommit commit = (RevCommit) object; + byte[] signatureData = commit.getRawGpgSignature(); + if (signatureData == null) { + return null; + } + byte[] raw = commit.getRawBuffer(); + // Now remove the GPG signature + byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' }; + int start = RawParseUtils.headerStart(header, raw, 0); + if (start < 0) { + return null; + } + int end = RawParseUtils.nextLfSkippingSplitLines(raw, start); + // start is at the beginning of the header's content + start -= header.length + 1; + // end is on the terminating LF; we need to skip that, too + if (end < raw.length) { + end++; + } + byte[] data = new byte[raw.length - (end - start)]; + System.arraycopy(raw, 0, data, 0, start); + System.arraycopy(raw, end, data, start, raw.length - end); + return verify(repository, config, data, signatureData); + } else if (object instanceof RevTag) { + RevTag tag = (RevTag) object; + byte[] signatureData = tag.getRawGpgSignature(); + if (signatureData == null) { + return null; + } + byte[] raw = tag.getRawBuffer(); + // The signature is just tacked onto the end of the message, which + // is last in the buffer. + byte[] data = Arrays.copyOfRange(raw, 0, + raw.length - signatureData.length); + return verify(repository, config, data, signatureData); + } + return null; + } + + /** + * Verifies a given signature for some give data. + * + * @param repository + * the {@link Repository} the object is from + * @param config + * the {@link GpgConfig} to use + * @param data + * to verify the signature of + * @param signature + * the given signature of the {@code data} + * @return a {@link SignatureVerifier.SignatureVerification} describing the + * outcome of the verification, or {@code null} if the signature + * type is unknown + * @throws IOException + * if an error occurs getting a public key + * @throws org.eclipse.jgit.api.errors.JGitInternalException + * if signature verification fails + */ + @Nullable + public static SignatureVerifier.SignatureVerification verify( + @NonNull Repository repository, @NonNull GpgConfig config, + byte[] data, byte[] signature) throws IOException { + GpgConfig.GpgFormat format = getFormat(signature); + if (format == null) { + return null; + } + SignatureVerifier verifier = get(format); + if (verifier == null) { + return null; + } + return verifier.verify(repository, config, data, signature); + } + + /** + * Determines the type of a given signature. + * + * @param signature + * to get the type of + * @return the signature type, or {@code null} if unknown + */ + @Nullable + public static GpgConfig.GpgFormat getFormat(byte[] signature) { + if (RawParseUtils.match(signature, 0, PGP_PREFIX) > 0) { + return GpgConfig.GpgFormat.OPENPGP; + } + if (RawParseUtils.match(signature, 0, X509_PREFIX) > 0) { + return GpgConfig.GpgFormat.X509; + } + if (RawParseUtils.match(signature, 0, SSH_PREFIX) > 0) { + return GpgConfig.GpgFormat.SSH; + } + return null; + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signer.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signer.java new file mode 100644 index 0000000000..3bb7464d08 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signer.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; +import org.eclipse.jgit.transport.CredentialsProvider; + +/** + * Creates signatures for Git objects. + * + * @since 7.0 + */ +public interface Signer { + + /** + * Signs the specified object. + * + * <p> + * Implementors should obtain the payload for signing from the specified + * object via {@link ObjectBuilder#build()} and create a proper + * {@link GpgSignature}. The generated signature is set on the specified + * {@code object} (see {@link ObjectBuilder#setGpgSignature(GpgSignature)}). + * </p> + * <p> + * Any existing signature on the object must be discarded prior obtaining + * the payload via {@link ObjectBuilder#build()}. + * </p> + * + * @param repository + * {@link Repository} the object belongs to + * @param config + * GPG settings from the git config + * @param object + * the object to sign (must not be {@code null} and must be + * complete to allow proper calculation of payload) + * @param committer + * the signing identity (to help with key lookup in case signing + * key is not specified) + * @param signingKey + * if non-{@code null} overrides the signing key from the config + * @param credentialsProvider + * provider to use when querying for signing key credentials (eg. + * passphrase) + * @throws CanceledException + * when signing was canceled (eg., user aborted when entering + * passphrase) + * @throws IOException + * if an I/O error occurs + * @throws UnsupportedSigningFormatException + * if a config is given and the wanted key format is not + * supported + */ + default void signObject(@NonNull Repository repository, + @NonNull GpgConfig config, @NonNull ObjectBuilder object, + @NonNull PersonIdent committer, String signingKey, + CredentialsProvider credentialsProvider) + throws CanceledException, IOException, + UnsupportedSigningFormatException { + try { + object.setGpgSignature(sign(repository, config, object.build(), + committer, signingKey, credentialsProvider)); + } catch (UnsupportedEncodingException e) { + throw new JGitInternalException(e.getMessage(), e); + } + } + + /** + * Signs arbitrary data. + * + * @param repository + * {@link Repository} the signature is created in + * @param config + * GPG settings from the git config + * @param data + * the data to sign + * @param committer + * the signing identity (to help with key lookup in case signing + * key is not specified) + * @param signingKey + * if non-{@code null} overrides the signing key from the config + * @param credentialsProvider + * provider to use when querying for signing key credentials (eg. + * passphrase) + * @return the signature for {@code data} + * @throws CanceledException + * when signing was canceled (eg., user aborted when entering + * passphrase) + * @throws IOException + * if an I/O error occurs + * @throws UnsupportedSigningFormatException + * if a config is given and the wanted key format is not + * supported + */ + GpgSignature sign(@NonNull Repository repository, @NonNull GpgConfig config, + byte[] data, @NonNull PersonIdent committer, String signingKey, + CredentialsProvider credentialsProvider) throws CanceledException, + IOException, UnsupportedSigningFormatException; + + /** + * Indicates if a signing key is available for the specified committer + * and/or signing key. + * + * @param repository + * the current {@link Repository} + * @param config + * GPG settings from the git config + * @param committer + * the signing identity (to help with key lookup in case signing + * key is not specified) + * @param signingKey + * if non-{@code null} overrides the signing key from the config + * @param credentialsProvider + * provider to use when querying for signing key credentials (eg. + * passphrase) + * @return {@code true} if a signing key is available, {@code false} + * otherwise + * @throws CanceledException + * when signing was canceled (eg., user aborted when entering + * passphrase) + */ + boolean canLocateSigningKey(@NonNull Repository repository, + @NonNull GpgConfig config, @NonNull PersonIdent committer, + String signingKey, CredentialsProvider credentialsProvider) + throws CanceledException; + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignerFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignerFactory.java new file mode 100644 index 0000000000..125d25e3b7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignerFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import org.eclipse.jgit.annotations.NonNull; + +/** + * A factory for {@link Signer}s. + * + * @since 7.0 + */ +public interface SignerFactory { + + /** + * Tells what kind of {@link Signer} this factory creates. + * + * @return the {@link GpgConfig.GpgFormat} of the signer + */ + @NonNull + GpgConfig.GpgFormat getType(); + + /** + * Creates a new instance of a {@link Signer} that can produce signatures of + * type {@link #getType()}. + * + * @return a new {@link Signer} + */ + @NonNull + Signer create(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signers.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signers.java new file mode 100644 index 0000000000..7771b07841 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signers.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import java.text.MessageFormat; +import java.util.EnumMap; +import java.util.Map; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.internal.JGitText; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages the available signers. + * + * @since 7.0 + */ +public final class Signers { + + private static final Logger LOG = LoggerFactory.getLogger(Signers.class); + + private static final Map<GpgConfig.GpgFormat, SignerFactory> SIGNER_FACTORIES = loadSigners(); + + private static final Map<GpgConfig.GpgFormat, Signer> SIGNERS = new ConcurrentHashMap<>(); + + private static Map<GpgConfig.GpgFormat, SignerFactory> loadSigners() { + Map<GpgConfig.GpgFormat, SignerFactory> result = new EnumMap<>( + GpgConfig.GpgFormat.class); + try { + for (SignerFactory factory : ServiceLoader + .load(SignerFactory.class)) { + GpgConfig.GpgFormat format = factory.getType(); + SignerFactory existing = result.get(format); + if (existing != null) { + LOG.warn("{}", //$NON-NLS-1$ + MessageFormat.format( + JGitText.get().signatureServiceConflict, + "SignerFactory", format, //$NON-NLS-1$ + existing.getClass().getCanonicalName(), + factory.getClass().getCanonicalName())); + } else { + result.put(format, factory); + } + } + } catch (ServiceConfigurationError e) { + LOG.error(e.getMessage(), e); + } + return result; + } + + private Signers() { + // No instantiation + } + + /** + * Retrieves a {@link Signer} that can produce signatures of the given type + * {@code format}. + * + * @param format + * {@link GpgConfig.GpgFormat} the signer must support + * @return a {@link Signer}, or {@code null} if none is available + */ + public static Signer get(@NonNull GpgConfig.GpgFormat format) { + return SIGNERS.computeIfAbsent(format, f -> { + SignerFactory factory = SIGNER_FACTORIES.get(format); + if (factory == null) { + return null; + } + return factory.create(); + }); + } + + /** + * Sets a specific signer to use for a specific signature type. + * + * @param format + * signature type to set the {@code signer} for + * @param signer + * the {@link Signer} to use for signatures of type + * {@code format}; if {@code null}, a default implementation, if + * available, may be used. + */ + public static void set(@NonNull GpgConfig.GpgFormat format, Signer signer) { + if (signer == null) { + SIGNERS.remove(format); + } else { + SIGNERS.put(format, signer); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/StoredConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/StoredConfig.java index 9ddfd59a18..a10eb0c583 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/StoredConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/StoredConfig.java @@ -57,7 +57,6 @@ public abstract class StoredConfig extends Config { */ public abstract void save() throws IOException; - /** {@inheritDoc} */ @Override public void clear() { super.clear(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java index 3ba33422c6..6fb8f326cc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java @@ -59,20 +59,17 @@ public class SymbolicRef implements Ref { this.updateIndex = updateIndex; } - /** {@inheritDoc} */ @Override @NonNull public String getName() { return name; } - /** {@inheritDoc} */ @Override public boolean isSymbolic() { return true; } - /** {@inheritDoc} */ @Override @NonNull public Ref getLeaf() { @@ -82,35 +79,30 @@ public class SymbolicRef implements Ref { return dst; } - /** {@inheritDoc} */ @Override @NonNull public Ref getTarget() { return target; } - /** {@inheritDoc} */ @Override @Nullable public ObjectId getObjectId() { return getLeaf().getObjectId(); } - /** {@inheritDoc} */ @Override @NonNull public Storage getStorage() { return Storage.LOOSE; } - /** {@inheritDoc} */ @Override @Nullable public ObjectId getPeeledObjectId() { return getLeaf().getPeeledObjectId(); } - /** {@inheritDoc} */ @Override public boolean isPeeled() { return getLeaf().isPeeled(); @@ -128,7 +120,6 @@ public class SymbolicRef implements Ref { return updateIndex; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java index facb4a54be..ea73d95102 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java @@ -206,24 +206,6 @@ public class TagBuilder extends ObjectBuilder { return os.toByteArray(); } - /** - * Format this builder's state as an annotated tag object. - * - * @return this object in the canonical annotated tag format, suitable for - * storage in a repository, or {@code null} if the tag cannot be - * encoded - * @deprecated since 5.11; use {@link #build()} instead - */ - @Deprecated - public byte[] toByteArray() { - try { - return build(); - } catch (UnsupportedEncodingException e) { - return null; - } - } - - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java index 85aa0b6639..98ea2f97a3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java @@ -45,7 +45,6 @@ public class TextProgressMonitor extends BatchingProgressMonitor { this.write = true; } - /** {@inheritDoc} */ @Override protected void onUpdate(String taskName, int workCurr, Duration duration) { StringBuilder s = new StringBuilder(); @@ -53,7 +52,6 @@ public class TextProgressMonitor extends BatchingProgressMonitor { send(s); } - /** {@inheritDoc} */ @Override protected void onEndTask(String taskName, int workCurr, Duration duration) { StringBuilder s = new StringBuilder(); @@ -73,7 +71,6 @@ public class TextProgressMonitor extends BatchingProgressMonitor { appendDuration(s, duration); } - /** {@inheritDoc} */ @Override protected void onUpdate(String taskName, int cmp, int totalWork, int pcnt, Duration duration) { @@ -82,7 +79,6 @@ public class TextProgressMonitor extends BatchingProgressMonitor { send(s); } - /** {@inheritDoc} */ @Override protected void onEndTask(String taskName, int cmp, int totalWork, int pcnt, Duration duration) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ThreadSafeProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ThreadSafeProgressMonitor.java index e553955560..05eacdcd63 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ThreadSafeProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ThreadSafeProgressMonitor.java @@ -55,7 +55,6 @@ public class ThreadSafeProgressMonitor implements ProgressMonitor { this.process = new Semaphore(0); } - /** {@inheritDoc} */ @Override public void start(int totalTasks) { if (!isMainThread()) @@ -63,7 +62,6 @@ public class ThreadSafeProgressMonitor implements ProgressMonitor { pm.start(totalTasks); } - /** {@inheritDoc} */ @Override public void beginTask(String title, int totalWork) { if (!isMainThread()) @@ -132,14 +130,12 @@ public class ThreadSafeProgressMonitor implements ProgressMonitor { pm.update(cnt); } - /** {@inheritDoc} */ @Override public void update(int completed) { if (0 == pendingUpdates.getAndAdd(completed)) process.release(); } - /** {@inheritDoc} */ @Override public boolean isCancelled() { lock.lock(); @@ -150,7 +146,6 @@ public class ThreadSafeProgressMonitor implements ProgressMonitor { } } - /** {@inheritDoc} */ @Override public void endTask() { if (!isMainThread()) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeFormatter.java index e1f8684830..c3c170a2c9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeFormatter.java @@ -332,7 +332,6 @@ public class TreeFormatter { } } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java index c4eb8f10d5..3d4e0d1f3c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.util.FS; @@ -50,13 +51,40 @@ public interface TypedConfigGetter { * default value to return if no value was present. * @return true if any value or defaultValue is true, false for missing or * explicit false + * @deprecated use + * {@link #getBoolean(Config, String, String, String, Boolean)} + * instead */ + @Deprecated boolean getBoolean(Config config, String section, String subsection, String name, boolean defaultValue); /** + * Get a boolean value from a git {@link Config}. + * + * @param config + * to get the value from + * @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 + * @since 7.2 + */ + @Nullable + Boolean getBoolean(Config config, String section, String subsection, + String name, @Nullable Boolean defaultValue); + + /** * Parse an enumeration from a git {@link Config}. * + * @param <T> + * type of the enum * @param config * to get the value from * @param all @@ -72,8 +100,9 @@ public interface TypedConfigGetter { * default value to return if no value was present. * @return the selected enumeration value, or {@code defaultValue}. */ + @Nullable <T extends Enum<?>> T getEnum(Config config, T[] all, String section, - String subsection, String name, T defaultValue); + String subsection, String name, @Nullable T defaultValue); /** * Obtain an integer value from a git {@link Config}. @@ -89,11 +118,34 @@ public interface TypedConfigGetter { * @param defaultValue * default value to return if no value was present. * @return an integer value from the configuration, or defaultValue. + * @deprecated use {@link #getInt(Config, String, String, String, Integer)} + * instead */ + @Deprecated int getInt(Config config, String section, String subsection, String name, int defaultValue); /** + * Obtain an integer value from a git {@link Config}. + * + * @param config + * to get the value from + * @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. + * @since 7.2 + */ + @Nullable + Integer getInt(Config config, String section, String subsection, + String name, @Nullable Integer defaultValue); + + /** * Obtain an integer value from a git {@link Config} which must be in given * range. * @@ -115,11 +167,43 @@ public interface TypedConfigGetter { * @return an integer value from the configuration, or defaultValue. * {@code #UNSET_INT} if unset. * @since 6.1 + * @deprecated use + * {@link #getIntInRange(Config, String, String, String, int, int, Integer)} + * instead */ + @Deprecated int getIntInRange(Config config, String section, String subsection, String name, int minValue, int maxValue, int defaultValue); /** + * Obtain an integer value from a git {@link Config} which must be in given + * range. + * + * @param config + * to get the value from + * @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 minValue + * minimal value + * @param maxValue + * maximum value + * @param defaultValue + * default value to return if no value was present. Use + * {@code #UNSET_INT} to set the default to unset. + * @return an integer value from the configuration, or defaultValue. + * {@code #UNSET_INT} if unset. + * @since 7.2 + */ + @Nullable + Integer getIntInRange(Config config, String section, String subsection, + String name, int minValue, int maxValue, + @Nullable Integer defaultValue); + + /** * Obtain a long value from a git {@link Config}. * * @param config @@ -133,11 +217,34 @@ public interface TypedConfigGetter { * @param defaultValue * default value to return if no value was present. * @return a long value from the configuration, or defaultValue. + * @deprecated use {@link #getLong(Config, String, String, String, Long)} + * instead */ + @Deprecated long getLong(Config config, String section, String subsection, String name, long defaultValue); /** + * Obtain a long value from a git {@link Config}. + * + * @param config + * to get the value from + * @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 a long value from the configuration, or defaultValue. + * @since 7.2 + */ + @Nullable + Long getLong(Config config, String section, String subsection, String name, + @Nullable Long defaultValue); + + /** * Parse a numerical time unit, such as "1 minute", from a git * {@link Config}. * @@ -157,11 +264,41 @@ public interface TypedConfigGetter { * indication of the units. * @return the value, or {@code defaultValue} if not set, expressed in * {@code units}. + * @deprecated use + * {@link #getTimeUnit(Config, String, String, String, Long, TimeUnit)} + * instead */ + @Deprecated long getTimeUnit(Config config, String section, String subsection, String name, long defaultValue, TimeUnit wantUnit); /** + * Parse a numerical time unit, such as "1 minute", from a git + * {@link Config}. + * + * @param config + * to get the value from + * @param section + * section the key is in. + * @param subsection + * subsection the key is in, or null if not in a subsection. + * @param name + * the key name. + * @param defaultValue + * default value to return if no value was present. + * @param wantUnit + * the units of {@code defaultValue} and the return value, as + * well as the units to assume if the value does not contain an + * indication of the units. + * @return the value, or {@code defaultValue} if not set, expressed in + * {@code units}. + * @since 7.2 + */ + @Nullable + Long getTimeUnit(Config config, String section, String subsection, + String name, @Nullable Long defaultValue, TimeUnit wantUnit); + + /** * Parse a string value from a git {@link Config} and treat it as a file * path, replacing a ~/ prefix by the user's home directory. * <p> @@ -187,9 +324,10 @@ public interface TypedConfigGetter { * @return the {@link Path}, or {@code defaultValue} if not set * @since 5.10 */ + @Nullable default Path getPath(Config config, String section, String subsection, String name, @NonNull FS fs, File resolveAgainst, - Path defaultValue) { + @Nullable Path defaultValue) { String value = config.getString(section, subsection, name); if (value == null) { return defaultValue; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java index 6d568643d5..a835a1dfc5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java @@ -23,5 +23,12 @@ public enum ContentMergeStrategy { OURS, /** Resolve the conflict hunk using the theirs version. */ - THEIRS -}
\ No newline at end of file + THEIRS, + + /** + * Resolve the conflict hunk using a union of both ours and theirs versions. + * + * @since 6.10.1 + */ + UNION +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java index e44970abff..8857ef8571 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java @@ -74,19 +74,21 @@ class EolAwareOutputStream extends OutputStream { write('\n'); } - /** @return true if a new line has just begun. */ + /** + * Whether a new line has just begun + * + * @return true if a new line has just begun. + */ boolean isBeginln() { return bol; } - /** {@inheritDoc} */ @Override public void write(int val) throws IOException { out.write(val); bol = (val == '\n'); } - /** {@inheritDoc} */ @Override public void write(byte[] buf, int pos, int cnt) throws IOException { if (cnt > 0) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java index 80607351ae..d0d4d367b9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java @@ -90,10 +90,16 @@ public final class MergeAlgorithm { /** * Does the three way merge between a common base and two sequences. * - * @param cmp comparison method for this execution. - * @param base the common base sequence - * @param ours the first sequence to be merged - * @param theirs the second sequence to be merged + * @param <S> + * type of the sequences + * @param cmp + * comparison method for this execution. + * @param base + * the common base sequence + * @param ours + * the first sequence to be merged + * @param theirs + * the second sequence to be merged * @return the resulting content */ public <S extends Sequence> MergeResult<S> merge( @@ -114,6 +120,7 @@ public final class MergeAlgorithm { result.add(1, 0, 0, ConflictState.NO_CONFLICT); break; case THEIRS: + case UNION: result.add(2, 0, theirs.size(), ConflictState.NO_CONFLICT); break; @@ -121,6 +128,8 @@ public final class MergeAlgorithm { // Let their complete content conflict with empty text result.add(1, 0, 0, ConflictState.FIRST_CONFLICTING_RANGE); + result.add(0, 0, base.size(), + ConflictState.BASE_CONFLICTING_RANGE); result.add(2, 0, theirs.size(), ConflictState.NEXT_CONFLICTING_RANGE); break; @@ -140,6 +149,7 @@ public final class MergeAlgorithm { // we modified, they deleted switch (strategy) { case OURS: + case UNION: result.add(1, 0, ours.size(), ConflictState.NO_CONFLICT); break; case THEIRS: @@ -149,6 +159,8 @@ public final class MergeAlgorithm { // Let our complete content conflict with empty text result.add(1, 0, ours.size(), ConflictState.FIRST_CONFLICTING_RANGE); + result.add(0, 0, base.size(), + ConflictState.BASE_CONFLICTING_RANGE); result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE); break; } @@ -208,13 +220,18 @@ public final class MergeAlgorithm { // set some initial values for the ranges in A and B which we // want to handle + int oursBeginA = oursEdit.getBeginA(); + int theirsBeginA = theirsEdit.getBeginA(); int oursBeginB = oursEdit.getBeginB(); int theirsBeginB = theirsEdit.getBeginB(); // harmonize the start of the ranges in A and B if (oursEdit.getBeginA() < theirsEdit.getBeginA()) { + theirsBeginA -= theirsEdit.getBeginA() + - oursEdit.getBeginA(); theirsBeginB -= theirsEdit.getBeginA() - oursEdit.getBeginA(); } else { + oursBeginA -= oursEdit.getBeginA() - theirsEdit.getBeginA(); oursBeginB -= oursEdit.getBeginA() - theirsEdit.getBeginA(); } @@ -260,11 +277,15 @@ public final class MergeAlgorithm { } // harmonize the end of the ranges in A and B + int oursEndA = oursEdit.getEndA(); + int theirsEndA = theirsEdit.getEndA(); int oursEndB = oursEdit.getEndB(); int theirsEndB = theirsEdit.getEndB(); if (oursEdit.getEndA() < theirsEdit.getEndA()) { + oursEndA += theirsEdit.getEndA() - oursEdit.getEndA(); oursEndB += theirsEdit.getEndA() - oursEdit.getEndA(); } else { + theirsEndA += oursEdit.getEndA() - theirsEdit.getEndA(); theirsEndB += oursEdit.getEndA() - theirsEdit.getEndA(); } @@ -314,10 +335,27 @@ public final class MergeAlgorithm { theirsEndB - commonSuffix, ConflictState.NO_CONFLICT); break; + case UNION: + result.add(1, oursBeginB + commonPrefix, + oursEndB - commonSuffix, + ConflictState.NO_CONFLICT); + + result.add(2, theirsBeginB + commonPrefix, + theirsEndB - commonSuffix, + ConflictState.NO_CONFLICT); + break; default: result.add(1, oursBeginB + commonPrefix, oursEndB - commonSuffix, ConflictState.FIRST_CONFLICTING_RANGE); + + int baseBegin = Math.min(oursBeginA, theirsBeginA) + + commonPrefix; + int baseEnd = Math.min(base.size(), + Math.max(oursEndA, theirsEndA)) - commonSuffix; + result.add(0, baseBegin, baseEnd, + ConflictState.BASE_CONFLICTING_RANGE); + result.add(2, theirsBeginB + commonPrefix, theirsEndB - commonSuffix, ConflictState.NEXT_CONFLICTING_RANGE); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeChunk.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeChunk.java index ca998e30bc..7102aa6998 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeChunk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeChunk.java @@ -29,14 +29,22 @@ public class MergeChunk { NO_CONFLICT, /** - * This chunk does belong to a conflict and is the first one of the + * This chunk does belong to a conflict and is the ours section of the * conflicting chunks */ FIRST_CONFLICTING_RANGE, /** - * This chunk does belong to a conflict but is not the first one of the - * conflicting chunks. It's a subsequent one. + * This chunk does belong to a conflict and is the base section of the + * conflicting chunks + * + * @since 6.7 + */ + BASE_CONFLICTING_RANGE, + + /** + * This chunk does belong to a conflict and is the theirs section of + * the conflicting chunks. It's a subsequent one. */ NEXT_CONFLICTING_RANGE } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java index 18b0ad92c8..079db4a07f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java @@ -22,6 +22,7 @@ import org.eclipse.jgit.diff.RawText; * A class to convert merge results into a Git conformant textual presentation */ public class MergeFormatter { + /** * Formats the results of a merge of {@link org.eclipse.jgit.diff.RawText} * objects in a Git conformant way. This method also assumes that the @@ -39,27 +40,24 @@ public class MergeFormatter { * name. This name is following the "<<<<<<< * " or ">>>>>>> " conflict markers. The * names for the sequences are given in this list - * @param charsetName - * the name of the character set used when writing conflict - * metadata + * @param charset + * the character set used when writing conflict metadata * @throws java.io.IOException - * @deprecated Use - * {@link #formatMerge(OutputStream, MergeResult, List, Charset)} - * instead. + * if an IO error occurred + * @since 5.2 */ - @Deprecated public void formatMerge(OutputStream out, MergeResult<RawText> res, - List<String> seqName, String charsetName) throws IOException { - formatMerge(out, res, seqName, Charset.forName(charsetName)); + List<String> seqName, Charset charset) throws IOException { + new MergeFormatterPass(out, res, seqName, charset).formatMerge(); } /** * Formats the results of a merge of {@link org.eclipse.jgit.diff.RawText} - * objects in a Git conformant way. This method also assumes that the - * {@link org.eclipse.jgit.diff.RawText} objects being merged are line - * oriented files which use LF as delimiter. This method will also use LF to - * separate chunks and conflict metadata, therefore it fits only to texts - * that are LF-separated lines. + * objects in a Git conformant way using diff3 style. This method also + * assumes that the {@link org.eclipse.jgit.diff.RawText} objects being + * merged are line oriented files which use LF as delimiter. This method + * will also use LF to separate chunks and conflict metadata, therefore it + * fits only to texts that are LF-separated lines. * * @param out * the output stream where to write the textual presentation @@ -68,16 +66,18 @@ public class MergeFormatter { * @param seqName * When a conflict is reported each conflicting range will get a * name. This name is following the "<<<<<<< - * " or ">>>>>>> " conflict markers. The - * names for the sequences are given in this list + * ", "|||||||" or ">>>>>>> " conflict + * markers. The names for the sequences are given in this list * @param charset * the character set used when writing conflict metadata * @throws java.io.IOException - * @since 5.2 + * if an IO error occurred + * @since 6.7 */ - public void formatMerge(OutputStream out, MergeResult<RawText> res, - List<String> seqName, Charset charset) throws IOException { - new MergeFormatterPass(out, res, seqName, charset).formatMerge(); + public void formatMergeDiff3(OutputStream out, + MergeResult<RawText> res, List<String> seqName, Charset charset) + throws IOException { + new MergeFormatterPass(out, res, seqName, charset, true).formatMerge(); } /** @@ -98,27 +98,29 @@ public class MergeFormatter { * the name ranges from ours should get * @param theirsName * the name ranges from theirs should get - * @param charsetName - * the name of the character set used when writing conflict - * metadata + * @param charset + * the character set used when writing conflict metadata * @throws java.io.IOException - * @deprecated use - * {@link #formatMerge(OutputStream, MergeResult, String, String, String, Charset)} - * instead. + * if an IO error occurred + * @since 5.2 */ - @Deprecated + @SuppressWarnings("unchecked") public void formatMerge(OutputStream out, MergeResult res, String baseName, - String oursName, String theirsName, String charsetName) throws IOException { - formatMerge(out, res, baseName, oursName, theirsName, - Charset.forName(charsetName)); + String oursName, String theirsName, Charset charset) + throws IOException { + List<String> names = new ArrayList<>(3); + names.add(baseName); + names.add(oursName); + names.add(theirsName); + formatMerge(out, res, names, charset); } /** - * Formats the results of a merge of exactly two - * {@link org.eclipse.jgit.diff.RawText} objects in a Git conformant way. - * This convenience method accepts the names for the three sequences (base - * and the two merged sequences) as explicit parameters and doesn't require - * the caller to specify a List + * Formats the results of a merge of three + * {@link org.eclipse.jgit.diff.RawText} objects in a Git conformant way, + * using diff-3 style. This convenience method accepts the names for the + * three sequences (base and the two merged sequences) as explicit + * parameters and doesn't require the caller to specify a List * * @param out * the {@link java.io.OutputStream} where to write the textual @@ -134,16 +136,17 @@ public class MergeFormatter { * @param charset * the character set used when writing conflict metadata * @throws java.io.IOException - * @since 5.2 + * if an IO error occurred + * @since 6.7 */ @SuppressWarnings("unchecked") - public void formatMerge(OutputStream out, MergeResult res, String baseName, - String oursName, String theirsName, Charset charset) - throws IOException { + public void formatMergeDiff3(OutputStream out, + MergeResult res, String baseName, String oursName, + String theirsName, Charset charset) throws IOException { List<String> names = new ArrayList<>(3); names.add(baseName); names.add(oursName); names.add(theirsName); - formatMerge(out, res, names, charset); + formatMergeDiff3(out, res, names, charset); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java index f09b343007..5e80b5fb23 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java @@ -31,6 +31,8 @@ class MergeFormatterPass { private final boolean threeWayMerge; + private final boolean writeBase; // diff3-style requested + private String lastConflictingName; // is set to non-null whenever we are in // a conflict @@ -50,22 +52,47 @@ class MergeFormatterPass { */ MergeFormatterPass(OutputStream out, MergeResult<RawText> res, List<String> seqName, Charset charset) { + this(out, res, seqName, charset, false); + } + + /** + * @param out + * the {@link java.io.OutputStream} where to write the textual + * presentation + * @param res + * the merge result which should be presented + * @param seqName + * When a conflict is reported each conflicting range will get a + * name. This name is following the "<<<<<<< + * ", "|||||||" or ">>>>>>> " conflict + * markers. The names for the sequences are given in this list + * @param charset + * the character set used when writing conflict metadata + * @param writeBase + * base's contribution should be written in conflicts + */ + MergeFormatterPass(OutputStream out, MergeResult<RawText> res, + List<String> seqName, Charset charset, boolean writeBase) { this.out = new EolAwareOutputStream(out); this.res = res; this.seqName = seqName; this.charset = charset; this.threeWayMerge = (res.getSequences().size() == 3); + this.writeBase = writeBase; } void formatMerge() throws IOException { boolean missingNewlineAtEnd = false; for (MergeChunk chunk : res) { - RawText seq = res.getSequences().get(chunk.getSequenceIndex()); - writeConflictMetadata(chunk); - // the lines with conflict-metadata are written. Now write the chunk - for (int i = chunk.getBegin(); i < chunk.getEnd(); i++) - writeLine(seq, i); - missingNewlineAtEnd = seq.isMissingNewlineAtEnd(); + if (!isBase(chunk) || writeBase) { + RawText seq = res.getSequences().get(chunk.getSequenceIndex()); + writeConflictMetadata(chunk); + // the lines with conflict-metadata are written. Now write the + // chunk + for (int i = chunk.getBegin(); i < chunk.getEnd(); i++) + writeLine(seq, i); + missingNewlineAtEnd = seq.isMissingNewlineAtEnd(); + } } // one possible leftover: if the merge result ended with a conflict we // have to close the last conflict here @@ -77,16 +104,19 @@ class MergeFormatterPass { private void writeConflictMetadata(MergeChunk chunk) throws IOException { if (lastConflictingName != null - && chunk.getConflictState() != ConflictState.NEXT_CONFLICTING_RANGE) { - // found the end of an conflict + && !isTheirs(chunk) && !isBase(chunk)) { + // found the end of a conflict writeConflictEnd(); } - if (chunk.getConflictState() == ConflictState.FIRST_CONFLICTING_RANGE) { - // found the start of an conflict + if (isOurs(chunk)) { + // found the start of a conflict writeConflictStart(chunk); - } else if (chunk.getConflictState() == ConflictState.NEXT_CONFLICTING_RANGE) { - // found another conflicting chunk + } else if (isTheirs(chunk)) { + // found the theirs conflicting chunk writeConflictChange(chunk); + } else if (isBase(chunk)) { + // found the base conflicting chunk + writeConflictBase(chunk); } } @@ -113,6 +143,11 @@ class MergeFormatterPass { + lastConflictingName); } + private void writeConflictBase(MergeChunk chunk) throws IOException { + lastConflictingName = seqName.get(chunk.getSequenceIndex()); + writeln("||||||| " + lastConflictingName); //$NON-NLS-1$ + } + private void writeln(String s) throws IOException { out.beginln(); out.write((s + "\n").getBytes(charset)); //$NON-NLS-1$ @@ -125,4 +160,17 @@ class MergeFormatterPass { if (out.isBeginln()) out.write('\n'); } + + private boolean isBase(MergeChunk chunk) { + return chunk.getConflictState() == ConflictState.BASE_CONFLICTING_RANGE; + } + + private boolean isOurs(MergeChunk chunk) { + return chunk + .getConflictState() == ConflictState.FIRST_CONFLICTING_RANGE; + } + + private boolean isTheirs(MergeChunk chunk) { + return chunk.getConflictState() == ConflictState.NEXT_CONFLICTING_RANGE; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java index e0c083f55c..039d7d844c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java @@ -92,24 +92,6 @@ public class MergeMessageFormatter { } /** - * Add section with conflicting paths to merge message. Lines are prefixed - * with a hash. - * - * @param message - * the original merge message - * @param conflictingPaths - * the paths with conflicts - * @return merge message with conflicting paths added - * @deprecated since 6.1; use - * {@link #formatWithConflicts(String, Iterable, char)} instead - */ - @Deprecated - public String formatWithConflicts(String message, - List<String> conflictingPaths) { - return formatWithConflicts(message, conflictingPaths, '#'); - } - - /** * Add section with conflicting paths to merge message. * * @param message diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java index 20fc3c339c..acf955303d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java @@ -97,7 +97,6 @@ public class MergeResult<S extends Sequence> implements Iterable<MergeChunk> { static final ConflictState[] states = ConflictState.values(); - /** {@inheritDoc} */ @Override public Iterator<MergeChunk> iterator() { return new Iterator<>() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java index df6068925b..f58ef4faba 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java @@ -18,10 +18,11 @@ package org.eclipse.jgit.merge; import java.io.IOException; import java.text.MessageFormat; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.TimeZone; +import java.util.stream.Collectors; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -123,6 +124,7 @@ public class RecursiveMerger extends ResolveMerger { * synthetic merge base this commit is visible only the merger's * RevWalk and will not be in the repository. * @throws java.io.IOException + * if an IO error occurred * @throws IncorrectObjectTypeException * one of the input objects is not a commit. * @throws NoMergeBaseException @@ -184,12 +186,15 @@ public class RecursiveMerger extends ResolveMerger { if (mergeTrees(bcTree, currentBase.getTree(), nextBase.getTree(), true)) currentBase = createCommitForTree(resultTree, parents); - else + else { + String failedPaths = failingPathsMessage(); throw new NoMergeBaseException( NoMergeBaseException.MergeBaseFailureReason.CONFLICTS_DURING_MERGE_BASE_CALCULATION, MessageFormat.format( JGitText.get().mergeRecursiveConflictsWhenMergingCommonAncestors, - currentBase.getName(), nextBase.getName())); + currentBase.getName(), nextBase.getName(), + failedPaths)); + } } } finally { inCore = oldIncore; @@ -213,6 +218,7 @@ public class RecursiveMerger extends ResolveMerger { * the list of parent commits * @return a new commit visible only within this merger's RevWalk. * @throws IOException + * if an IO error occurred */ private RevCommit createCommitForTree(ObjectId tree, List<RevCommit> parents) throws IOException { @@ -227,11 +233,23 @@ public class RecursiveMerger extends ResolveMerger { private static PersonIdent mockAuthor(List<RevCommit> parents) { String name = RecursiveMerger.class.getSimpleName(); int time = 0; - for (RevCommit p : parents) + for (RevCommit p : parents) { time = Math.max(time, p.getCommitTime()); - return new PersonIdent( - name, name + "@JGit", //$NON-NLS-1$ - new Date((time + 1) * 1000L), - TimeZone.getTimeZone("GMT+0000")); //$NON-NLS-1$ + } + return new PersonIdent(name, name + "@JGit", //$NON-NLS-1$ + Instant.ofEpochSecond(time+1), ZoneOffset.UTC); + } + + private String failingPathsMessage() { + int max = 25; + String failedPaths = failingPaths.entrySet().stream().limit(max) + .map(entry -> entry.getKey() + ":" + entry.getValue()) //$NON-NLS-1$ + .collect(Collectors.joining("\n")); //$NON-NLS-1$ + + if (failingPaths.size() > max) { + failedPaths = String.format("%s\n... (%s failing paths omitted)", //$NON-NLS-1$ + failedPaths, Integer.valueOf(failingPaths.size() - max)); + } + return failedPaths; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index e56513d4e9..dc96f65b87 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -3,8 +3,8 @@ * Copyright (C) 2010-2012, Matthias Sohn <matthias.sohn@sap.com> * Copyright (C) 2012, Research In Motion Limited * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) - * Copyright (C) 2018, 2022 Thomas Wolf <twolf@apache.org> - * Copyright (C) 2022, Google Inc. and others + * Copyright (C) 2018, 2023 Thomas Wolf <twolf@apache.org> + * Copyright (C) 2023, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -32,7 +32,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -42,11 +41,13 @@ import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.attributes.Attribute; import org.eclipse.jgit.attributes.Attributes; +import org.eclipse.jgit.attributes.AttributesNodeProvider; import org.eclipse.jgit.diff.DiffAlgorithm; import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.diff.Sequence; +import org.eclipse.jgit.dircache.Checkout; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuildIterator; import org.eclipse.jgit.dircache.DirCacheBuilder; @@ -79,7 +80,6 @@ import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.treewalk.WorkingTreeIterator; import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.filter.TreeFilter; -import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.LfsFactory; import org.eclipse.jgit.util.LfsFactory.LfsInputStream; import org.eclipse.jgit.util.TemporaryBuffer; @@ -106,13 +106,15 @@ public class ResolveMerger extends ThreeWayMerger { */ public static class Result { - private final List<String> modifiedFiles = new LinkedList<>(); + private final List<String> modifiedFiles = new ArrayList<>(); - private final List<String> failedToDelete = new LinkedList<>(); + private final List<String> failedToDelete = new ArrayList<>(); private ObjectId treeId = null; /** + * Get modified tree id if any + * * @return Modified tree ID if any, or null otherwise. */ public ObjectId getTreeId() { @@ -120,6 +122,8 @@ public class ResolveMerger extends ThreeWayMerger { } /** + * Get path of files that couldn't be deleted + * * @return Files that couldn't be deleted. */ public List<String> getFailedToDelete() { @@ -127,6 +131,8 @@ public class ResolveMerger extends ThreeWayMerger { } /** + * Get path of modified files + * * @return Files modified during this operation. */ public List<String> getModifiedFiles() { @@ -205,6 +211,12 @@ public class ResolveMerger extends ThreeWayMerger { private boolean indexChangesWritten; /** + * {@link Checkout} to use for actually checking out files if + * {@link #inCore} is {@code false}. + */ + private Checkout checkout; + + /** * @param repo * the {@link Repository}. * @param dirCache @@ -223,6 +235,7 @@ public class ResolveMerger extends ThreeWayMerger { this.inCoreFileSizeLimit = getInCoreFileSizeLimit(config); this.checkoutMetadataByPath = new HashMap<>(); this.cleanupMetadataByPath = new HashMap<>(); + this.checkout = new Checkout(nonNullRepo(), workingTreeOptions); } /** @@ -350,9 +363,8 @@ public class ResolveMerger extends ThreeWayMerger { } // All content operations are successfully done. If we can now write - // the - // new index we are on quite safe ground. Even if the checkout of - // files coming from "theirs" fails the user can work around such + // the new index we are on quite safe ground. Even if the checkout + // of files coming from "theirs" fails the user can work around such // failures by checking out the index again. if (!builder.commit()) { revertModifiedFiles(); @@ -466,7 +478,6 @@ public class ResolveMerger extends ThreeWayMerger { /** * Detects if CRLF conversion has been configured. * <p> - * </p> * See {@link EolStreamTypeUtil#detectStreamType} for more info. * * @param attributes @@ -518,14 +529,14 @@ public class ResolveMerger extends ThreeWayMerger { for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut .entrySet()) { DirCacheEntry dirCacheEntry = entry.getValue(); + String gitPath = entry.getKey(); if (dirCacheEntry.getFileMode() == FileMode.GITLINK) { - new File(nonNullRepo().getWorkTree(), entry.getKey()) - .mkdirs(); + checkout.checkoutGitlink(dirCacheEntry, gitPath); } else { - DirCacheCheckout.checkoutEntry(repo, dirCacheEntry, reader, - false, checkoutMetadataByPath.get(entry.getKey()), - workingTreeOptions); - result.modifiedFiles.add(entry.getKey()); + checkout.checkout(dirCacheEntry, + checkoutMetadataByPath.get(gitPath), reader, + gitPath); + result.modifiedFiles.add(gitPath); } } } @@ -550,9 +561,8 @@ public class ResolveMerger extends ThreeWayMerger { for (String path : result.modifiedFiles) { DirCacheEntry entry = dirCache.getEntry(path); if (entry != null) { - DirCacheCheckout.checkoutEntry(repo, entry, reader, false, - cleanupMetadataByPath.get(path), - workingTreeOptions); + checkout.checkout(entry, cleanupMetadataByPath.get(path), + reader, path); } } } @@ -586,6 +596,8 @@ public class ResolveMerger extends ThreeWayMerger { if (inCore) { return; } + checkout.safeCreateParentDirectory(path, file.getParentFile(), + false); CheckoutMetadata metadata = new CheckoutMetadata(streamType, smudgeCommand); @@ -826,6 +838,13 @@ public class ResolveMerger extends ThreeWayMerger { @NonNull private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT; + /** + * The {@link AttributesNodeProvider} to use while merging trees. + * + * @since 6.10.1 + */ + protected AttributesNodeProvider attributesNodeProvider; + private static MergeAlgorithm getMergeAlgorithm(Config config) { SupportedAlgorithm diffAlg = config.getEnum( CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM, @@ -904,7 +923,6 @@ public class ResolveMerger extends ThreeWayMerger { : strategy; } - /** {@inheritDoc} */ @Override protected boolean mergeImpl() throws IOException { return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], @@ -915,18 +933,23 @@ public class ResolveMerger extends ThreeWayMerger { * adds a new path with the specified stage to the index builder * * @param path + * the new path * @param p + * canonical tree parser * @param stage - * @param lastMod + * the stage + * @param lastModified + * lastModified attribute of the file * @param len + * file length * @return the entry which was added to the index */ private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage, - Instant lastMod, long len) { + Instant lastModified, long len) { if (p != null && !p.getEntryFileMode().equals(FileMode.TREE)) { return workTreeUpdater.addExistingToIndex(p.getEntryObjectId(), path, p.getEntryFileMode(), stage, - lastMod, (int) len); + lastModified, (int) len); } return null; } @@ -1056,6 +1079,7 @@ public class ResolveMerger extends ThreeWayMerger { * didn't match ours or the working-dir file was dirty and a * conflict occurred * @throws java.io.IOException + * if an IO error occurred * @since 6.1 */ protected boolean processEntry(CanonicalTreeParser base, @@ -1257,10 +1281,22 @@ public class ResolveMerger extends ThreeWayMerger { default: break; } + if (ignoreConflicts) { + // If the path is selected to be treated as binary via attributes, we do not perform + // content merge. When ignoreConflicts = true, we simply keep OURS to allow virtual commit + // to be built. + keep(ourDce); + return true; + } + // add the conflicting path to merge result + String currentPath = tw.getPathString(); + MergeResult<RawText> result = new MergeResult<>( + Collections.emptyList()); + result.setContainsConflicts(true); + mergeResults.put(currentPath, result); addConflict(base, ours, theirs); - // attribute merge issues are conflicts but not failures - unmergedPaths.add(tw.getPathString()); + unmergedPaths.add(currentPath); return true; } @@ -1272,38 +1308,52 @@ public class ResolveMerger extends ThreeWayMerger { MergeResult<RawText> result = null; boolean hasSymlink = FileMode.SYMLINK.equals(modeO) || FileMode.SYMLINK.equals(modeT); + + String currentPath = tw.getPathString(); + // if the path is not a symlink in ours and theirs if (!hasSymlink) { try { result = contentMerge(base, ours, theirs, attributes, getContentMergeStrategy()); - } catch (BinaryBlobException e) { - // result == null - } - } - if (result == null) { - switch (getContentMergeStrategy()) { - case OURS: - keep(ourDce); - return true; - case THEIRS: - DirCacheEntry e = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_0, EPOCH, 0); - if (e != null) { - addToCheckout(tw.getPathString(), e, attributes); + if (result.containsConflicts() && !ignoreConflicts) { + result.setContainsConflicts(true); + unmergedPaths.add(currentPath); + } else if (ignoreConflicts) { + result.setContainsConflicts(false); } + updateIndex(base, ours, theirs, result, attributes[T_OURS]); + workTreeUpdater.markAsModified(currentPath); + // Entry is null - only add the metadata + addToCheckout(currentPath, null, attributes); return true; - default: - result = new MergeResult<>(Collections.emptyList()); - result.setContainsConflicts(true); - break; + } catch (BinaryBlobException e) { + // The file is binary in either OURS, THEIRS or BASE + if (ignoreConflicts) { + // When ignoreConflicts = true, we simply keep OURS to allow virtual commit to be built. + keep(ourDce); + return true; + } } } - if (ignoreConflicts) { - result.setContainsConflicts(false); + switch (getContentMergeStrategy()) { + case OURS: + keep(ourDce); + return true; + case THEIRS: + DirCacheEntry e = add(tw.getRawPath(), theirs, + DirCacheEntry.STAGE_0, EPOCH, 0); + if (e != null) { + addToCheckout(currentPath, e, attributes); + } + return true; + default: + result = new MergeResult<>(Collections.emptyList()); + result.setContainsConflicts(true); + break; } - String currentPath = tw.getPathString(); if (hasSymlink) { if (ignoreConflicts) { + result.setContainsConflicts(false); if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE)) { DirCacheEntry e = add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_0, EPOCH, 0); @@ -1312,9 +1362,9 @@ public class ResolveMerger extends ThreeWayMerger { keep(ourDce); } } else { - // Record the conflict DirCacheEntry e = addConflict(base, ours, theirs); mergeResults.put(currentPath, result); + unmergedPaths.add(currentPath); // If theirs is a file, check it out. In link/file // conflicts, C git prefers the file. if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE) @@ -1323,14 +1373,14 @@ public class ResolveMerger extends ThreeWayMerger { } } } else { - updateIndex(base, ours, theirs, result, attributes[T_OURS]); - } - if (result.containsConflicts() && !ignoreConflicts) { + // This is reachable if contentMerge() call above threw BinaryBlobException, so we don't + // need to check ignoreConflicts here, since it's already handled above. + result.setContainsConflicts(true); + addConflict(base, ours, theirs); unmergedPaths.add(currentPath); + mergeResults.put(currentPath, result); } - workTreeUpdater.markAsModified(currentPath); - // Entry is null - only adds the metadata. - addToCheckout(currentPath, null, attributes); + return true; } else if (modeO != modeT) { // OURS or THEIRS has been deleted if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw @@ -1432,15 +1482,21 @@ public class ResolveMerger extends ThreeWayMerger { * specified as <code>null</code> then an empty text will be used instead. * * @param base + * used to parse base tree * @param ours + * used to parse ours tree * @param theirs + * used to parse theirs tree * @param attributes + * attributes for the different stages * @param strategy + * merge strategy * * @return the result of the content merge * @throws BinaryBlobException * if any of the blobs looks like a binary blob * @throws IOException + * if an IO error occurred */ private MergeResult<RawText> contentMerge(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, @@ -1454,11 +1510,26 @@ public class ResolveMerger extends ThreeWayMerger { : getRawText(ours.getEntryObjectId(), attributes[T_OURS]); RawText theirsText = theirs == null ? RawText.EMPTY_TEXT : getRawText(theirs.getEntryObjectId(), attributes[T_THEIRS]); - mergeAlgorithm.setContentMergeStrategy(strategy); + mergeAlgorithm.setContentMergeStrategy( + getAttributesContentMergeStrategy(attributes[T_OURS], + strategy)); return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText, ourText, theirsText); } + private ContentMergeStrategy getAttributesContentMergeStrategy( + Attributes attributes, ContentMergeStrategy strategy) { + Attribute attr = attributes.get(Constants.ATTR_MERGE); + if (attr != null) { + String attrValue = attr.getValue(); + if (attrValue != null && attrValue + .equals(Constants.ATTR_BUILTIN_UNION_MERGE_DRIVER)) { + return ContentMergeStrategy.UNION; + } + } + return strategy; + } + private boolean isIndexDirty() { if (inCore) { return false; @@ -1516,11 +1587,17 @@ public class ResolveMerger extends ThreeWayMerger { * correct stages to the index. * * @param base + * used to parse base tree * @param ours + * used to parse ours tree * @param theirs + * used to parse theirs tree * @param result + * merge result * @param attributes + * the file's attributes * @throws IOException + * if an IO error occurred */ private void updateIndex(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, @@ -1571,20 +1648,17 @@ public class ResolveMerger extends ThreeWayMerger { * the files .gitattributes entries * @return the working tree file to which the merged content was written. * @throws IOException + * if an IO error occurred */ private File writeMergedFile(TemporaryBuffer rawMerged, Attributes attributes) throws IOException { File workTree = nonNullRepo().getWorkTree(); - FS fs = nonNullRepo().getFS(); - File of = new File(workTree, tw.getPathString()); - File parentFolder = of.getParentFile(); + String gitPath = tw.getPathString(); + File of = new File(workTree, gitPath); EolStreamType eol = workTreeUpdater.detectCheckoutStreamType(attributes); - if (!fs.exists(parentFolder)) { - parentFolder.mkdirs(); - } workTreeUpdater.updateFileWithContent(rawMerged::openInputStream, - eol, tw.getSmudgeCommand(attributes), of.getPath(), of); + eol, tw.getSmudgeCommand(attributes), gitPath, of); return of; } @@ -1659,7 +1733,6 @@ public class ResolveMerger extends ThreeWayMerger { return FileMode.GITLINK.equals(mode); } - /** {@inheritDoc} */ @Override public ObjectId getResultTreeId() { return (resultTree == null) ? null : resultTree.toObjectId(); @@ -1787,6 +1860,18 @@ public class ResolveMerger extends ThreeWayMerger { this.workingTreeIterator = workingTreeIterator; } + /** + * Sets the {@link AttributesNodeProvider} to be used by this merger. + * + * @param attributesNodeProvider + * the attributeNodeProvider to set + * @since 6.10.1 + */ + public void setAttributesNodeProvider( + AttributesNodeProvider attributesNodeProvider) { + this.attributesNodeProvider = attributesNodeProvider; + } + /** * The resolve conflict way of three way merging @@ -1819,6 +1904,7 @@ public class ResolveMerger extends ThreeWayMerger { * content-merge conflicts. * @return whether the trees merged cleanly * @throws java.io.IOException + * if an IO error occurred * @since 3.5 */ protected boolean mergeTrees(AbstractTreeIterator baseTree, @@ -1830,6 +1916,9 @@ public class ResolveMerger extends ThreeWayMerger { WorkTreeUpdater.createWorkTreeUpdater(db, dircache); dircache = workTreeUpdater.getLockedDirCache(); tw = new NameConflictTreeWalk(db, reader); + if (attributesNodeProvider != null) { + tw.setAttributesNodeProvider(attributesNodeProvider); + } tw.addTree(baseTree); tw.setHead(tw.addTree(headTree)); @@ -1878,6 +1967,7 @@ public class ResolveMerger extends ThreeWayMerger { * {@link org.eclipse.jgit.merge.ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} * @return Whether the trees merged cleanly. * @throws java.io.IOException + * if an IO error occurred * @since 3.5 */ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java index 7055ccd127..6064ebe326 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java @@ -43,25 +43,21 @@ public class StrategyOneSided extends MergeStrategy { treeIndex = index; } - /** {@inheritDoc} */ @Override public String getName() { return strategyName; } - /** {@inheritDoc} */ @Override public Merger newMerger(Repository db) { return new OneSide(db, treeIndex); } - /** {@inheritDoc} */ @Override public Merger newMerger(Repository db, boolean inCore) { return new OneSide(db, treeIndex); } - /** {@inheritDoc} */ @Override public Merger newMerger(ObjectInserter inserter, Config config) { return new OneSide(inserter, treeIndex); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java index c9512edf63..a74bfc0379 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java @@ -21,25 +21,21 @@ import org.eclipse.jgit.lib.Repository; */ public class StrategyRecursive extends StrategyResolve { - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db) { return new RecursiveMerger(db, false); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db, boolean inCore) { return new RecursiveMerger(db, inCore); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(ObjectInserter inserter, Config config) { return new RecursiveMerger(inserter, config); } - /** {@inheritDoc} */ @Override public String getName() { return "recursive"; //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java index 5991ef68a3..a686fd0964 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java @@ -19,25 +19,21 @@ import org.eclipse.jgit.lib.Repository; */ public class StrategyResolve extends ThreeWayMergeStrategy { - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db) { return new ResolveMerger(db, false); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db, boolean inCore) { return new ResolveMerger(db, inCore); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(ObjectInserter inserter, Config config) { return new ResolveMerger(inserter, config); } - /** {@inheritDoc} */ @Override public String getName() { return "resolve"; //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java index ff40fc1d68..c3180abd35 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java @@ -43,26 +43,22 @@ public class StrategySimpleTwoWayInCore extends ThreeWayMergeStrategy { // } - /** {@inheritDoc} */ @Override public String getName() { return "simple-two-way-in-core"; //$NON-NLS-1$ } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db) { return new InCoreMerger(db); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db, boolean inCore) { // This class is always inCore, so ignore the parameter return newMerger(db); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(ObjectInserter inserter, Config config) { return new InCoreMerger(inserter); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java index 411789fd95..8cefa6566e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java @@ -16,11 +16,9 @@ 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 { - /** {@inheritDoc} */ @Override public abstract ThreeWayMerger newMerger(Repository db); - /** {@inheritDoc} */ @Override public abstract ThreeWayMerger newMerger(Repository db, boolean inCore); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java index f283edee01..68a1b5e509 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java @@ -88,7 +88,6 @@ public abstract class ThreeWayMerger extends Merger { } } - /** {@inheritDoc} */ @Override public boolean merge(AnyObjectId... tips) throws IOException { if (tips.length != 2) @@ -96,7 +95,6 @@ public abstract class ThreeWayMerger extends Merger { return super.merge(tips); } - /** {@inheritDoc} */ @Override public ObjectId getBaseCommitId() { return baseCommitId; @@ -108,6 +106,7 @@ public abstract class ThreeWayMerger extends Merger { * @return an iterator over the caller-specified merge base, or the natural * merge base of the two input commits. * @throws java.io.IOException + * if an IO error occurred */ protected AbstractTreeIterator mergeBase() throws IOException { if (baseTree != null) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java b/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java index 881873de6f..16d17637e2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java @@ -83,6 +83,8 @@ public class NLS { * injected as described in the * {@link org.eclipse.jgit.nls.TranslationBundle}. * + * @param <T> + * type of the translation bundle * @param type * required bundle type * @return an instance of the required bundle type diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/DefaultNoteMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/DefaultNoteMerger.java index bde0b9e445..b65f28c918 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/DefaultNoteMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/DefaultNoteMerger.java @@ -34,7 +34,6 @@ import org.eclipse.jgit.util.io.UnionInputStream; */ public class DefaultNoteMerger implements NoteMerger { - /** {@inheritDoc} */ @Override public Note merge(Note base, Note ours, Note theirs, ObjectReader reader, ObjectInserter inserter) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java index c35696323e..2f719bf453 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/Note.java @@ -45,7 +45,6 @@ public class Note extends ObjectId { data = newData; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java index 93adf36a3a..ed45f81476 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java @@ -166,7 +166,6 @@ public class NoteMap implements Iterable<Note> { this.reader = reader; } - /** {@inheritDoc} */ @Override public Iterator<Note> iterator() { try { @@ -349,7 +348,11 @@ public class NoteMap implements Iterable<Note> { return root.writeTree(inserter); } - /** @return the root note bucket */ + /** + * Get the root note bucket + * + * @return the root note bucket + */ InMemoryNoteBucket getRoot() { return root; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java index a6ea523a78..30512c17b8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java @@ -94,6 +94,7 @@ public class NoteMapMerger { * theirs version of the note tree * @return merge result as a new NoteMap * @throws java.io.IOException + * if an IO error occurred */ public NoteMap merge(NoteMap base, NoteMap ours, NoteMap theirs) throws IOException { @@ -113,11 +114,16 @@ public class NoteMapMerger { * between base, ours and theirs. * * @param treeDepth + * depth of the tree * @param base + * base version * @param ours + * ours version * @param theirs + * theirs version * @return merge result as an InMemoryBucket * @throws IOException + * if an IO error occurred */ private InMemoryNoteBucket merge(int treeDepth, InMemoryNoteBucket base, InMemoryNoteBucket ours, InMemoryNoteBucket theirs) @@ -193,7 +199,7 @@ public class NoteMapMerger { if (child == null) return; if (child instanceof InMemoryNoteBucket) - b.setBucket(cell, ((InMemoryNoteBucket) child).writeTree(inserter)); + b.setBucket(cell, child.writeTree(inserter)); else b.setBucket(cell, child.getTreeId()); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java index 4ba7cca51e..e29af614a6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java @@ -40,7 +40,6 @@ public class CombinedFileHeader extends FileHeader { super(b, offset); } - /** {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public List<? extends CombinedHunkHeader> getHunks() { @@ -48,9 +47,6 @@ public class CombinedFileHeader extends FileHeader { } /** - * {@inheritDoc} - * <p> - * * @return number of ancestor revisions mentioned in this diff. */ @Override @@ -60,7 +56,7 @@ public class CombinedFileHeader extends FileHeader { /** * {@inheritDoc} - * <p> + * * @return get the file mode of the first parent. */ @Override @@ -81,7 +77,6 @@ public class CombinedFileHeader extends FileHeader { /** * {@inheritDoc} - * <p> * * @return get the object id of the first parent. */ @@ -101,7 +96,6 @@ public class CombinedFileHeader extends FileHeader { return oldIds[nthParent]; } - /** {@inheritDoc} */ @Override public String getScriptText(Charset ocs, Charset ncs) { final Charset[] cs = new Charset[getParentCount() + 1]; @@ -110,11 +104,6 @@ public class CombinedFileHeader extends FileHeader { return getScriptText(cs); } - /** - * {@inheritDoc} - * <p> - * Convert the patch script for this file into a string. - */ @Override public String getScriptText(Charset[] charsetGuess) { return super.getScriptText(charsetGuess); @@ -156,7 +145,6 @@ public class CombinedFileHeader extends FileHeader { return ptr; } - /** {@inheritDoc} */ @Override protected void parseIndexLine(int ptr, int eol) { // "index $asha1,$bsha1..$csha1" @@ -178,7 +166,6 @@ public class CombinedFileHeader extends FileHeader { oldModes = new FileMode[oldIds.length]; } - /** {@inheritDoc} */ @Override protected void parseNewFileMode(int ptr, int eol) { for (int i = 0; i < oldModes.length; i++) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java index 263b1b9ddc..49cf499865 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java @@ -45,13 +45,11 @@ public class CombinedHunkHeader extends HunkHeader { } } - /** {@inheritDoc} */ @Override public CombinedFileHeader getFileHeader() { return (CombinedFileHeader) super.getFileHeader(); } - /** {@inheritDoc} */ @Override public OldImage getOldImage() { return getOldImage(0); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java index 1e6fb780b2..a47b73dc34 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java @@ -187,6 +187,13 @@ public class FileHeader extends DiffEntry { return getScriptText(new Charset[] { oldCharset, newCharset }); } + /** + * Convert the patch script for this file into a string. + * + * @param charsetGuess + * hint which charset is used + * @return the patch script, as a Unicode string. + */ String getScriptText(Charset[] charsetGuess) { if (getHunks().isEmpty()) { // If we have no hunks then we can safely assume the entire diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java index 5618a71782..8d21b6dabb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java @@ -91,7 +91,6 @@ public class FormatError { return RawParseUtils.decode(UTF_8, buf, offset, eol); } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java index 4b59fcfc63..9e98f9f272 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java @@ -42,27 +42,47 @@ public class HunkHeader { /** 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. */ + /** + * Get line number where hunk starts + * + * @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. */ + /** + * Get number of lines this hunk covers + * + * @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. */ + /** + * Get number of lines deleted by the post-image + * + * @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. */ + /** + * Get number of lines added by the post-image + * + * @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. */ + /** + * Get id of the pre-image file + * + * @return object id of the pre-image file. + */ public abstract AbbreviatedObjectId getId(); } @@ -409,7 +429,6 @@ public class HunkHeader { offsets[fileIdx] = end < 0 ? s.length() : end + 1; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java index da698d6bf6..23e09b9479 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022, Google Inc. and others + * Copyright (C) 2023, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardCopyOption; @@ -33,12 +34,13 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.zip.InflaterInputStream; + import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.errors.FilterFailedException; -import org.eclipse.jgit.api.errors.PatchFormatException; import org.eclipse.jgit.attributes.Attribute; import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.attributes.FilterCommand; @@ -52,6 +54,7 @@ import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheCheckout.StreamSupplier; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IndexWriteException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Config; @@ -59,6 +62,7 @@ import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.FileModeCache; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectLoader; @@ -81,10 +85,12 @@ import org.eclipse.jgit.util.LfsFactory; import org.eclipse.jgit.util.LfsFactory.LfsInputStream; import org.eclipse.jgit.util.RawParseUtils; import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer.LocalFile; import org.eclipse.jgit.util.io.BinaryDeltaInputStream; import org.eclipse.jgit.util.io.BinaryHunkInputStream; +import org.eclipse.jgit.util.io.CountingOutputStream; import org.eclipse.jgit.util.io.EolStreamTypeUtil; import org.eclipse.jgit.util.sha1.SHA1; @@ -97,11 +103,12 @@ import org.eclipse.jgit.util.sha1.SHA1; * @since 6.4 */ public class PatchApplier { - private static final byte[] NO_EOL = "\\ No newline at end of file" //$NON-NLS-1$ .getBytes(StandardCharsets.US_ASCII); - /** The tree before applying the patch. Only non-null for inCore operation. */ + /** + * The tree before applying the patch. Only non-null for inCore operation. + */ @Nullable private final RevTree beforeTree; @@ -111,10 +118,14 @@ public class PatchApplier { private final ObjectReader reader; + private final Charset charset; + private WorkingTreeOptions workingTreeOptions; private int inCoreSizeLimit; + private boolean allowConflicts; + /** * @param repo * repository to apply the patch in @@ -124,7 +135,8 @@ public class PatchApplier { inserter = repo.newObjectInserter(); reader = inserter.newReader(); beforeTree = null; - + allowConflicts = false; + charset = StandardCharsets.UTF_8; Config config = repo.getConfig(); workingTreeOptions = config.get(WorkingTreeOptions.KEY); inCoreSizeLimit = config.getInt(ConfigConstants.CONFIG_MERGE_SECTION, @@ -139,11 +151,14 @@ public class PatchApplier { * @param oi * to be used for modifying objects */ - public PatchApplier(Repository repo, RevTree beforeTree, ObjectInserter oi) { + public PatchApplier(Repository repo, RevTree beforeTree, + ObjectInserter oi) { this.repo = repo; this.beforeTree = beforeTree; inserter = oi; reader = oi.newReader(); + allowConflicts = false; + charset = StandardCharsets.UTF_8; } /** @@ -153,35 +168,76 @@ public class PatchApplier { * @since 6.3 */ public static class Result { - /** * A wrapper for a patch applying error that affects a given file. * * @since 6.6 */ + // TODO(ms): rename this class in next major release + @SuppressWarnings("JavaLangClash") public static class Error { + final String msg; - private String msg; - private String oldFileName; - private @Nullable HunkHeader hh; + final String oldFileName; - private Error(String msg, String oldFileName, - @Nullable HunkHeader hh) { + @Nullable + final HunkHeader hh; + + final boolean isGitConflict; + + Error(String msg, String oldFileName, @Nullable HunkHeader hh, + boolean isGitConflict) { this.msg = msg; this.oldFileName = oldFileName; this.hh = hh; + this.isGitConflict = isGitConflict; + } + + /** + * Signals if as part of encountering this error, conflict markers + * were added to the file. + * + * @return {@code true} if conflict markers were added for this + * error. + * + * @since 6.10 + */ + public boolean isGitConflict() { + return isGitConflict; } @Override public String toString() { if (hh != null) { - return MessageFormat.format(JGitText.get().patchApplyErrorWithHunk, - oldFileName, hh, msg); + return MessageFormat.format( + JGitText.get().patchApplyErrorWithHunk, oldFileName, + hh, msg); } - return MessageFormat.format(JGitText.get().patchApplyErrorWithoutHunk, - oldFileName, msg); + return MessageFormat.format( + JGitText.get().patchApplyErrorWithoutHunk, oldFileName, + msg); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !(o instanceof Error)) { + return false; + } + Error error = (Error) o; + return Objects.equals(msg, error.msg) + && Objects.equals(oldFileName, error.oldFileName) + && Objects.equals(hh, error.hh) + && isGitConflict == error.isGitConflict; + } + + @Override + public int hashCode() { + return Objects.hash(msg, oldFileName, hh, + Boolean.valueOf(isGitConflict)); + } } private ObjectId treeId; @@ -191,6 +247,8 @@ public class PatchApplier { private List<Error> errors = new ArrayList<>(); /** + * Get modified paths + * * @return List of modified paths. */ public List<String> getPaths() { @@ -198,6 +256,8 @@ public class PatchApplier { } /** + * Get tree ID + * * @return The applied tree ID. */ public ObjectId getTreeId() { @@ -205,6 +265,8 @@ public class PatchApplier { } /** + * Get errors + * * @return Errors occurred while applying the patch. * * @since 6.6 @@ -213,35 +275,15 @@ public class PatchApplier { return errors; } - private void addError(String msg,String oldFileName, @Nullable HunkHeader hh) { - errors.add(new Error(msg, oldFileName, hh)); + private void addError(String msg, String oldFileName, + @Nullable HunkHeader hh) { + errors.add(new Error(msg, oldFileName, hh, false)); } - } - /** - * Applies the given patch - * - * @param patchInput - * the patch to apply. - * @return the result of the patch - * @throws PatchFormatException - * if the patch cannot be parsed - * @throws IOException - * if the patch read fails - * @deprecated use {@link #applyPatch(Patch)} instead - */ - @Deprecated - public Result applyPatch(InputStream patchInput) - throws PatchFormatException, IOException { - Patch p = new Patch(); - try (InputStream inStream = patchInput) { - p.parse(inStream); - - if (!p.getErrors().isEmpty()) { - throw new PatchFormatException(p.getErrors()); - } + private void addErrorWithGitConflict(String msg, String oldFileName, + @Nullable HunkHeader hh) { + errors.add(new Error(msg, oldFileName, hh, true)); } - return applyPatch(p); } /** @@ -251,6 +293,7 @@ public class PatchApplier { * the patch to apply. * @return the result of the patch * @throws IOException + * if an IO error occurred * @since 6.6 */ public Result applyPatch(Patch p) throws IOException { @@ -258,6 +301,7 @@ public class PatchApplier { DirCache dirCache = inCore() ? DirCache.read(reader, beforeTree) : repo.lockDirCache(); + FileModeCache directoryCache = new FileModeCache(repo); DirCacheBuilder dirCacheBuilder = dirCache.builder(); Set<String> modifiedPaths = new HashSet<>(); for (FileHeader fh : p.getFiles()) { @@ -270,7 +314,8 @@ public class PatchApplier { switch (type) { case ADD: { if (dest != null) { - FileUtils.mkdirs(dest.getParentFile(), true); + directoryCache.safeCreateParentDirectory(fh.getNewPath(), + dest.getParentFile(), false); FileUtils.createNewFile(dest); } apply(fh.getNewPath(), dirCache, dirCacheBuilder, dest, fh, result); @@ -295,7 +340,8 @@ public class PatchApplier { * apply() will write a fresh stream anyway, which will * overwrite if there were hunks in the patch. */ - FileUtils.mkdirs(dest.getParentFile(), true); + directoryCache.safeCreateParentDirectory(fh.getNewPath(), + dest.getParentFile(), false); FileUtils.rename(src, dest, StandardCopyOption.ATOMIC_MOVE); } @@ -306,7 +352,8 @@ public class PatchApplier { } case COPY: { if (!inCore()) { - FileUtils.mkdirs(dest.getParentFile(), true); + directoryCache.safeCreateParentDirectory(fh.getNewPath(), + dest.getParentFile(), false); Files.copy(src.toPath(), dest.toPath()); } apply(fh.getOldPath(), dirCache, dirCacheBuilder, dest, fh, result); @@ -340,8 +387,19 @@ public class PatchApplier { return result; } + /** + * Sets up the {@link PatchApplier} to apply patches even if they conflict. + * + * @return the {@link PatchApplier} to apply any patches + * @since 6.10 + */ + public PatchApplier allowConflicts() { + allowConflicts = true; + return this; + } + private File getFile(String path) { - return (inCore()) ? null : new File(repo.getWorkTree(), path); + return inCore() ? null : new File(repo.getWorkTree(), path); } /* returns null if the path is not found. */ @@ -401,9 +459,28 @@ public class PatchApplier { fh.getPatchType()), fh.getNewPath(), null); isValid = false; } + if (srcShouldExist && !validGitPath(fh.getOldPath())) { + result.addError(JGitText.get().applyPatchSourceInvalid, + fh.getOldPath(), null); + isValid = false; + } + if (destShouldNotExist && !validGitPath(fh.getNewPath())) { + result.addError(JGitText.get().applyPatchDestInvalid, + fh.getNewPath(), null); + isValid = false; + } return isValid; } + private boolean validGitPath(String path) { + try { + SystemReader.getInstance().checkPath(path); + return true; + } catch (CorruptObjectException e) { + return false; + } + } + private static final int FILE_TREE_INDEX = 1; /** @@ -423,6 +500,7 @@ public class PatchApplier { * @param result * The patch application result. * @throws IOException + * if an IO error occurred */ private void apply(String pathWithOriginalContent, DirCache dirCache, DirCacheBuilder dirCacheBuilder, @Nullable File f, FileHeader fh, Result result) @@ -503,7 +581,9 @@ public class PatchApplier { convertCrLf); resultStreamLoader = applyText(raw, fh, result); } - if (resultStreamLoader == null || !result.getErrors().isEmpty()) { + if (resultStreamLoader == null + || (!result.getErrors().isEmpty() && result.getErrors().stream() + .anyMatch(e -> !e.msg.equals("cannot apply hunk")))) { //$NON-NLS-1$ return; } @@ -777,7 +857,9 @@ public class PatchApplier { * The patch application result * @return a loader for the new content, or null if invalid. * @throws IOException + * if an IO error occurred * @throws UnsupportedOperationException + * if an operation isn't supported */ private @Nullable ContentStreamLoader applyBinary(String path, File f, FileHeader fh, StreamSupplier inputSupplier, ObjectId id, Result result) @@ -832,6 +914,7 @@ public class PatchApplier { } } + @SuppressWarnings("ByteBufferBackingArray") private @Nullable ContentStreamLoader applyText(RawText rt, FileHeader fh, Result result) throws IOException { List<ByteBuffer> oldLines = new ArrayList<>(rt.size()); @@ -922,9 +1005,51 @@ public class PatchApplier { } } if (!applies) { - result.addError(JGitText.get().applyTextPatchCannotApplyHunk, - fh.getOldPath(), hh); - return null; + if (!allowConflicts) { + result.addError( + JGitText.get().applyTextPatchCannotApplyHunk, + fh.getOldPath(), hh); + return null; + } + // Insert conflict markers. This is best-guess because the + // file might have changed completely. But at least we give + // the user a graceful state that they can resolve manually. + // An alternative to this is using the 3-way merger. This + // only works if the pre-image SHA is contained in the repo. + // If that was the case, cherry-picking the original commit + // should be preferred to apply a patch. + result.addErrorWithGitConflict("cannot apply hunk", fh.getOldPath(), hh); //$NON-NLS-1$ + newLines.add(Math.min(applyAt++, newLines.size()), + asBytes("<<<<<<< HEAD")); //$NON-NLS-1$ + applyAt += hh.getOldImage().lineCount; + newLines.add(Math.min(applyAt++, newLines.size()), + asBytes("=======")); //$NON-NLS-1$ + + int sz = hunkLines.size(); + for (int j = 1; j < sz; j++) { + ByteBuffer hunkLine = hunkLines.get(j); + if (!hunkLine.hasRemaining()) { + // Completely empty line; accept as empty context + // line + applyAt++; + lastWasRemoval = false; + continue; + } + switch (hunkLine.array()[hunkLine.position()]) { + case ' ': + case '+': + newLines.add(Math.min(applyAt++, newLines.size()), + slice(hunkLine, 1)); + break; + case '-': + case '\\': + default: + break; + } + } + newLines.add(Math.min(applyAt++, newLines.size()), + asBytes(">>>>>>> PATCH")); //$NON-NLS-1$ + continue; } // Hunk applies at applyAt. Apply it, and update afterLastHunk and // lineNumberShift @@ -971,11 +1096,18 @@ public class PatchApplier { } else if (!rt.isMissingNewlineAtEnd()) { newLines.add(null); } + return toContentStreamLoader(newLines); + } + private static ContentStreamLoader toContentStreamLoader( + List<ByteBuffer> newLines) throws IOException { // We could check if old == new, but the short-circuiting complicates // logic for inCore patching, so just write the new thing regardless. TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null); - try (OutputStream out = buffer) { + // TemporaryBuffer::length reports incorrect length until the buffer + // is closed. To use it as input for ContentStreamLoader below, we + // need a wrapper with a reliable in-progress length. + try (CountingOutputStream out = new CountingOutputStream(buffer)) { for (Iterator<ByteBuffer> l = newLines.iterator(); l.hasNext();) { ByteBuffer line = l.next(); if (line == null) { @@ -988,10 +1120,15 @@ public class PatchApplier { } } return new ContentStreamLoader(buffer::openInputStream, - buffer.length()); + out.getCount()); } } + private ByteBuffer asBytes(String str) { + return ByteBuffer.wrap(str.getBytes(charset)); + } + + @SuppressWarnings("ByteBufferBackingArray") private boolean canApplyAt(List<ByteBuffer> hunkLines, List<ByteBuffer> newLines, int line) { int sz = hunkLines.size(); @@ -1023,11 +1160,13 @@ public class PatchApplier { return true; } + @SuppressWarnings("ByteBufferBackingArray") private ByteBuffer slice(ByteBuffer b, int off) { int newOffset = b.position() + off; return ByteBuffer.wrap(b.array(), newOffset, b.limit() - newOffset); } + @SuppressWarnings("ByteBufferBackingArray") private boolean isNoNewlineAtEnd(ByteBuffer hunkLine) { return Arrays.equals(NO_EOL, 0, NO_EOL.length, hunkLine.array(), hunkLine.position(), hunkLine.limit()); @@ -1078,4 +1217,4 @@ public class PatchApplier { in.close(); } } -} +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java index c11fca13d8..fa109e8208 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java @@ -179,7 +179,6 @@ public class PlotCommit<L extends PlotLane> extends RevCommit { return (L) lane; } - /** {@inheritDoc} */ @Override public void reset() { forkingOffLanes = NO_LANES; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java index 458f240982..3177de17e8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java @@ -49,7 +49,6 @@ public class PlotCommitList<L extends PlotLane> extends private final HashMap<PlotLane, Integer> laneLength = new HashMap<>( 32); - /** {@inheritDoc} */ @Override public void clear() { super.clear(); @@ -59,7 +58,6 @@ public class PlotCommitList<L extends PlotLane> extends laneLength.clear(); } - /** {@inheritDoc} */ @Override public void source(RevWalk w) { if (!(w instanceof PlotWalk)) @@ -91,7 +89,6 @@ public class PlotCommitList<L extends PlotLane> extends result.add((L) p); } - /** {@inheritDoc} */ @SuppressWarnings("ReferenceEquality") @Override protected void enter(int index, PlotCommit<L> currCommit) { @@ -184,6 +181,7 @@ public class PlotCommitList<L extends PlotLane> extends * @param index * the index of <code>currCommit</code> in the list * @param currCommit + * the current commit * @param childOnLane * the direct child on the same lane as <code>currCommit</code>, * may be null if <code>currCommit</code> is the first commit on @@ -287,8 +285,11 @@ public class PlotCommitList<L extends PlotLane> extends * All blockades on the lane must be resolved before calling this method. * * @param commitIndex + * commit index * @param child + * child to connect * @param laneToContinue + * lane to continue */ @SuppressWarnings("ReferenceEquality") private void drawLaneToChild(final int commitIndex, PlotCommit child, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java index 0582338652..c8c454a228 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java @@ -49,7 +49,6 @@ public class PlotWalk extends RevWalk { private Repository repository; - /** {@inheritDoc} */ @Override public void dispose() { super.dispose(); @@ -83,6 +82,7 @@ public class PlotWalk extends RevWalk { * @param refs * additional refs * @throws java.io.IOException + * if an IO error occurred */ public void addAdditionalRefs(Iterable<Ref> refs) throws IOException { for (Ref ref : refs) { @@ -97,7 +97,6 @@ public class PlotWalk extends RevWalk { } } - /** {@inheritDoc} */ @Override public void sort(RevSort s, boolean use) { if (s == RevSort.TOPO && !use) @@ -105,13 +104,11 @@ public class PlotWalk extends RevWalk { super.sort(s, use); } - /** {@inheritDoc} */ @Override protected RevCommit createCommit(AnyObjectId id) { return new PlotCommit(id); } - /** {@inheritDoc} */ @Override public RevCommit next() throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -172,8 +169,9 @@ public class PlotWalk extends RevWalk { } long timeof(RevObject o) { - if (o instanceof RevCommit) - return ((RevCommit) o).getCommitTime(); + if (o instanceof RevCommit) { + return ((RevCommit) o).getCommitTime() * 1000L; + } if (o instanceof RevTag) { RevTag tag = (RevTag) o; try { @@ -182,7 +180,7 @@ public class PlotWalk extends RevWalk { return 0; } PersonIdent who = tag.getTaggerIdent(); - return who != null ? who.getWhen().getTime() : 0; + return who != null ? who.getWhenAsInstant().toEpochMilli() : 0; } 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 index dda108bc69..73ae62a23f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java @@ -79,8 +79,6 @@ abstract class AbstractRevQueue extends Generator { } /** - * {@inheritDoc} - * <p> * Remove the first commit from the queue. */ @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapWalker.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapWalker.java index 8cd5eb2238..7e65ffb8f6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapWalker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapWalker.java @@ -50,12 +50,15 @@ public final class BitmapWalker { /** * Create a BitmapWalker. * - * @param walker walker to use when traversing the object graph. - * @param bitmapIndex index to obtain bitmaps from. - * @param pm progress monitor to report progress on. + * @param walker + * walker to use when traversing the object graph. + * @param bitmapIndex + * index to obtain bitmaps from. + * @param pm + * progress monitor to report progress on. */ - public BitmapWalker( - ObjectWalk walker, BitmapIndex bitmapIndex, ProgressMonitor pm) { + public BitmapWalker(ObjectWalk walker, BitmapIndex bitmapIndex, + ProgressMonitor pm) { this.walker = walker; this.bitmapIndex = bitmapIndex; this.pm = (pm == null) ? NullProgressMonitor.INSTANCE : pm; @@ -178,8 +181,9 @@ public final class BitmapWalker { for (ObjectId obj : start) { Bitmap bitmap = bitmapIndex.getBitmap(obj); - if (bitmap != null) + if (bitmap != null) { bitmapResult.or(bitmap); + } } boolean marked = false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java index cdd8073d6e..e855d8f700 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java @@ -45,8 +45,6 @@ abstract class BlockRevQueue extends AbstractRevQueue { } /** - * {@inheritDoc} - * <p> * Reconfigure this queue to share the same free list as another. * <p> * Multiple revision queues can be connected to the same free list, making @@ -56,6 +54,11 @@ abstract class BlockRevQueue extends AbstractRevQueue { * <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 + * another FIFO queue that wants to share our queue's free list. + * + * @see Generator#shareFreeList */ @Override public void shareFreeList(BlockRevQueue q) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevPriorityQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevPriorityQueue.java new file mode 100644 index 0000000000..233dd64a3c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevPriorityQueue.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023, GerritForge Ltd + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.revwalk; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.JGitText; + +import java.io.IOException; +import java.util.Comparator; +import java.util.PriorityQueue; + +/** + * A queue of commits sorted by commit time order using a Java PriorityQueue. + * For the commits with the same commit time insertion order will be preserved. + */ +class DateRevPriorityQueue extends DateRevQueue { + private PriorityQueue<RevCommitEntry> queue; + + private final AtomicInteger sequence = new AtomicInteger(1); + + /** + * Create an empty queue of commits sorted by commit time order. + */ + public DateRevPriorityQueue() { + this(false); + } + + /** + * Create an empty queue of commits sorted by commit time order. + * + * @param firstParent + * treat first element as a parent + */ + DateRevPriorityQueue(boolean firstParent) { + super(firstParent); + initPriorityQueue(); + } + + private void initPriorityQueue() { + sequence.set(1); + queue = new PriorityQueue<>(Comparator.comparingInt( + (RevCommitEntry ent) -> ent.getEntry().getCommitTime()) + .reversed() + .thenComparingInt(RevCommitEntry::getInsertSequenceNumber)); + } + + DateRevPriorityQueue(Generator s) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + this(s.firstParent); + for (;;) { + final RevCommit c = s.next(); + if (c == null) { + break; + } + add(c); + } + } + + @Override + public void add(RevCommit c) { + // PriorityQueue does not accept null values. To keep the same behaviour + // do the same check and throw the same exception before creating entry + if (c == null) { + throw new NullPointerException(JGitText.get().nullRevCommit); + } + queue.add(new RevCommitEntry(sequence.getAndIncrement(), c)); + } + + @Override + public RevCommit next() { + RevCommitEntry entry = queue.poll(); + return entry == null ? null : entry.getEntry(); + } + + /** + * Peek at the next commit, without removing it. + * + * @return the next available commit; null if there are no commits left. + */ + @Override + public @Nullable RevCommit peek() { + RevCommitEntry entry = queue.peek(); + return entry == null ? null : entry.getEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + sequence.set(1); + queue.clear(); + } + + @Override + boolean everbodyHasFlag(int f) { + return queue.stream().map(RevCommitEntry::getEntry) + .noneMatch(c -> (c.flags & f) == 0); + } + + @Override + boolean anybodyHasFlag(int f) { + return queue.stream().map(RevCommitEntry::getEntry) + .anyMatch(c -> (c.flags & f) != 0); + } + + @Override + int outputType() { + return outputType | SORT_COMMIT_TIME_DESC; + } + + @Override + public String toString() { + final StringBuilder s = new StringBuilder(); + for (RevCommitEntry e : queue) { + describe(s, e.getEntry()); + } + return s.toString(); + } + + private static class RevCommitEntry { + private final int insertSequenceNumber; + + private final RevCommit entry; + + public RevCommitEntry(int insertSequenceNumber, RevCommit entry) { + this.insertSequenceNumber = insertSequenceNumber; + this.entry = entry; + } + + public int getInsertSequenceNumber() { + return insertSequenceNumber; + } + + public RevCommit getEntry() { + return entry; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java index 0cabf07057..905dcb62ad 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java @@ -8,11 +8,11 @@ * * SPDX-License-Identifier: BSD-3-Clause */ - package org.eclipse.jgit.revwalk; import java.io.IOException; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -36,11 +36,17 @@ public class DateRevQueue extends AbstractRevQueue { private int last = -1; - /** Create an empty date queue. */ + /** Create an empty DateRevQueue. */ public DateRevQueue() { super(false); } + /** + * Create an empty DateRevQueue. + * + * @param firstParent + * treat first element as a parent + */ DateRevQueue(boolean firstParent) { super(firstParent); } @@ -56,7 +62,6 @@ public class DateRevQueue extends AbstractRevQueue { } } - /** {@inheritDoc} */ @Override public void add(RevCommit c) { sinceLastIndex++; @@ -102,7 +107,6 @@ public class DateRevQueue extends AbstractRevQueue { } } - /** {@inheritDoc} */ @Override public RevCommit next() { final Entry q = head; @@ -135,11 +139,10 @@ public class DateRevQueue extends AbstractRevQueue { * * @return the next available commit; null if there are no commits left. */ - public RevCommit peek() { + public @Nullable RevCommit peek() { return head != null ? head.commit : null; } - /** {@inheritDoc} */ @Override public void clear() { head = null; @@ -173,7 +176,6 @@ public class DateRevQueue extends AbstractRevQueue { return outputType | SORT_COMMIT_TIME_DESC; } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder s = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java index ec0824cb0b..664f8fa3a7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java @@ -55,10 +55,15 @@ class DepthGenerator extends Generator { /** * @param w - * @param s Parent generator + * walk used for depth filtering + * @param s + * Parent generator * @throws MissingObjectException + * if an object is missing * @throws IncorrectObjectTypeException + * if an object has an unexpected type * @throws IOException + * if an IO error occurred */ DepthGenerator(DepthWalk w, Generator s) throws MissingObjectException, IncorrectObjectTypeException, IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthWalk.java index 5277563ac4..a7ffd34233 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthWalk.java @@ -35,6 +35,8 @@ public interface DepthWalk { int getDepth(); /** + * Get deepen-since value + * * @return the deepen-since value; if not 0, this walk only returns commits * whose commit time is at or after this limit * @since 5.2 @@ -44,6 +46,8 @@ public interface DepthWalk { } /** + * Get deepen-not values + * * @return the objects specified by the client using --shallow-exclude * @since 5.2 */ @@ -51,7 +55,11 @@ public interface DepthWalk { return Collections.emptyList(); } - /** @return flag marking commits that should become unshallow. */ + /** + * Get unshallow flag + * + * @return flag marking commits that should become unshallow. + */ /** * Get flag marking commits that should become unshallow. * @@ -67,7 +75,10 @@ public interface DepthWalk { RevFlag getReinterestingFlag(); /** - * @return flag marking commits that are to be excluded because of --shallow-exclude + * Get deepen-not flag + * + * @return flag marking commits that are to be excluded because of + * --shallow-exclude * @since 5.2 */ RevFlag getDeepenNotFlag(); @@ -85,12 +96,18 @@ public interface DepthWalk { */ boolean makesChildBoundary; - /** @return depth of this commit, as found by the shortest path. */ + /** + * Get depth + * + * @return depth of this commit, as found by the shortest path. + */ public int getDepth() { return depth; } /** + * Whether at least one commit was excluded due to shallow fetch + * * @return true if at least one of this commit's parents was excluded * due to a shallow fetch setting, false otherwise * @since 5.2 @@ -126,8 +143,12 @@ public interface DepthWalk { private final RevFlag DEEPEN_NOT; /** - * @param repo Repository to walk - * @param depth Maximum depth to return + * Create RevWalk + * + * @param repo + * Repository to walk + * @param depth + * Maximum depth to return */ public RevWalk(Repository repo, int depth) { super(repo); @@ -140,8 +161,12 @@ public interface DepthWalk { } /** - * @param or ObjectReader to use - * @param depth Maximum depth to return + * Create RevWalk + * + * @param or + * ObjectReader to use + * @param depth + * Maximum depth to return */ public RevWalk(ObjectReader or, int depth) { super(or); @@ -159,8 +184,11 @@ public interface DepthWalk { * @param c * Commit to mark * @throws IOException + * if an IO error occurred * @throws IncorrectObjectTypeException + * if object has an unexpected type * @throws MissingObjectException + * if object is missing */ public void markRoot(RevCommit c) throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -228,6 +256,8 @@ public interface DepthWalk { } /** + * Convert to ObjectWalk with same objects + * * @since 4.5 */ @Override @@ -256,8 +286,12 @@ public interface DepthWalk { private final RevFlag DEEPEN_NOT; /** - * @param repo Repository to walk - * @param depth Maximum depth to return + * Create ObjectWalk + * + * @param repo + * Repository to walk + * @param depth + * Maximum depth to return */ public ObjectWalk(Repository repo, int depth) { super(repo); @@ -270,8 +304,12 @@ public interface DepthWalk { } /** - * @param or Object Reader - * @param depth Maximum depth to return + * Create ObjectWalk + * + * @param or + * Object Reader + * @param depth + * Maximum depth to return */ public ObjectWalk(ObjectReader or, int depth) { super(or); @@ -289,8 +327,11 @@ public interface DepthWalk { * @param o * Commit to mark * @throws IOException + * if an IO error occurred * @throws IncorrectObjectTypeException + * if object has an unexpected type * @throws MissingObjectException + * if object is missing */ public void markRoot(RevObject o) throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -313,8 +354,11 @@ public interface DepthWalk { * @param c * Commit to mark * @throws MissingObjectException + * if object is missing * @throws IncorrectObjectTypeException + * if object has an unexpected type * @throws IOException + * if an IO error occurred */ public void markUnshallow(RevObject c) throws MissingObjectException, IncorrectObjectTypeException, IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java index 0f8eddd6d4..996745c158 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java @@ -37,7 +37,6 @@ public class FIFORevQueue extends BlockRevQueue { super(s); } - /** {@inheritDoc} */ @Override public void add(RevCommit c) { Block b = tail; @@ -82,7 +81,6 @@ public class FIFORevQueue extends BlockRevQueue { head = b; } - /** {@inheritDoc} */ @Override public RevCommit next() { final Block b = head; @@ -99,7 +97,6 @@ public class FIFORevQueue extends BlockRevQueue { return c; } - /** {@inheritDoc} */ @Override public void clear() { head = null; @@ -135,7 +132,6 @@ public class FIFORevQueue extends BlockRevQueue { } } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder s = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FollowFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FollowFilter.java index 3b54123f0b..12e6c4ea98 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FollowFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FollowFilter.java @@ -11,12 +11,14 @@ package org.eclipse.jgit.revwalk; import java.io.IOException; +import java.util.Optional; +import java.util.Set; import org.eclipse.jgit.diff.DiffConfig; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.treewalk.filter.ChangedPathTreeFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; /** @@ -54,41 +56,49 @@ public class FollowFilter extends TreeFilter { * @since 3.0 */ public static FollowFilter create(String path, DiffConfig cfg) { - return new FollowFilter(PathFilter.create(path), cfg); + return new FollowFilter(ChangedPathTreeFilter.create(path), cfg); } - private final PathFilter path; + private final ChangedPathTreeFilter path; final DiffConfig cfg; private RenameCallback renameCallback; - FollowFilter(PathFilter path, DiffConfig cfg) { + FollowFilter(ChangedPathTreeFilter path, DiffConfig cfg) { this.path = path; this.cfg = cfg; } - /** @return the path this filter matches. */ /** * Get the path this filter matches. * * @return the path this filter matches. */ public String getPath() { - return path.getPath(); + return path.getPaths().get(0); } - /** {@inheritDoc} */ @Override public boolean include(TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException { - return path.include(walker) && ANY_DIFF.include(walker); + return path.include(walker); + } + + @Override + public boolean shouldTreeWalk(RevCommit c, RevWalk rw, + MutableBoolean cpfUsed) { + return path.shouldTreeWalk(c, rw, cpfUsed); } - /** {@inheritDoc} */ @Override public boolean shouldBeRecursive() { - return path.shouldBeRecursive() || ANY_DIFF.shouldBeRecursive(); + return path.shouldBeRecursive(); + } + + @Override + public Optional<Set<byte[]>> getPathsBestEffort() { + return path.getPathsBestEffort(); } /** {@inheritDoc} */ @@ -97,13 +107,10 @@ public class FollowFilter extends TreeFilter { return new FollowFilter(path.clone(), cfg); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { - return "(FOLLOW(" + path.toString() + ")" // - + " AND " // - + ANY_DIFF.toString() + ")"; + return "(FOLLOW(" + path.toString() + "))"; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterKey.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterKey.java index 74e4c1edb7..af50794014 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterKey.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterKey.java @@ -51,7 +51,6 @@ public final class FooterKey { return name; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java index 3d128a6bd3..fa7fb316c9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java @@ -11,6 +11,9 @@ package org.eclipse.jgit.revwalk; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.eclipse.jgit.util.RawParseUtils; @@ -47,6 +50,137 @@ public final class FooterLine { } /** + * Extract the footer lines from the given message. + * + * @param str + * the message to extract footers from. + * @return ordered list of footer lines; empty list if no footers found. + * @see RevCommit#getFooterLines() + * @since 6.7 + */ + public static List<FooterLine> fromMessage( + String str) { + return fromMessage(str.getBytes()); + } + + /** + * Extract the footer lines from the given message. + * + * @param raw + * the raw message to extract footers from. + * @return ordered list of footer lines; empty list if no footers found. + * @see RevCommit#getFooterLines() + * @since 6.7 + */ + public static List<FooterLine> fromMessage( + byte[] raw) { + // Find the end of the last paragraph. + int parEnd = raw.length; + for (; parEnd > 0 && (raw[parEnd - 1] == '\n' + || raw[parEnd - 1] == ' '); --parEnd) { + // empty + } + + // The first non-header line is never a footer. + int msgB = RawParseUtils.nextLfSkippingSplitLines(raw, + RawParseUtils.hasAnyKnownHeaders(raw) + ? RawParseUtils.commitMessage(raw, 0) + : 0); + ArrayList<FooterLine> r = new ArrayList<>(4); + Charset enc = RawParseUtils.guessEncoding(raw); + + // Search for the beginning of last paragraph + int parStart = parEnd; + for (; parStart > msgB; --parStart) { + if (parStart < 2) { + // Too close to beginning: this is not a raw message + parStart = 0; + break; + } + if (raw[parStart - 1] == '\n' && raw[parStart - 2] == '\n') { + break; + } + } + + for (int ptr = parStart; ptr < parEnd;) { + int keyStart = ptr; + int keyEnd = RawParseUtils.endOfFooterLineKey(raw, ptr); + if (keyEnd < 0) { + // Not a well-formed footer line, skip it. + ptr = RawParseUtils.nextLF(raw, ptr); + continue; + } + + // Skip over the ': *' at the end of the key before the value. + int valStart; + int valEnd; + for (valStart = keyEnd + 1; valStart < raw.length + && raw[valStart] == ' '; ++valStart) { + // empty + } + + for(ptr = valStart;;) { + ptr = RawParseUtils.nextLF(raw, ptr); + // Next line starts with whitespace for a multiline footer. + if (ptr == raw.length || raw[ptr] != ' ') { + valEnd = raw[ptr - 1] == '\n' ? ptr - 1 : ptr; + break; + } + } + if (keyStart == msgB) { + // Fist line cannot be a footer + continue; + } + r.add(new FooterLine(raw, enc, keyStart, keyEnd, valStart, valEnd)); + } + + return r; + } + + /** + * Get the values of all footer lines with the given key. + * + * @param footers + * list of footers to find the values in. + * @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 #fromMessage + * @since 6.7 + */ + public static List<String> getValues(List<FooterLine> footers, String keyName) { + return getValues(footers, new FooterKey(keyName)); + } + + /** + * Get the values of all footer lines with the given key. + * + * @param footers + * list of footers to find the values in. + * @param key + * 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 #fromMessage + * @since 6.7 + */ + public static List<String> getValues(List<FooterLine> footers, FooterKey key) { + if (footers.isEmpty()) + return Collections.emptyList(); + ArrayList<String> r = new ArrayList<>(footers.size()); + for (FooterLine f : footers) { + if (f.matches(key)) + r.add(f.getValue()); + } + return r; + } + + /** * Whether keys match * * @param key @@ -90,7 +224,7 @@ public final class FooterLine { * character encoding. */ public String getValue() { - return RawParseUtils.decode(enc, buffer, valStart, valEnd); + return RawParseUtils.decode(enc, buffer, valStart, valEnd).replaceAll("\n +", " "); //$NON-NLS-1$ //$NON-NLS-2$ } /** @@ -117,7 +251,28 @@ public final class FooterLine { return RawParseUtils.decode(enc, buffer, lt, gt - 1); } - /** {@inheritDoc} */ + /** + * @return start offset of the footer relative to the original raw message + * byte buffer + * + * @see #fromMessage(byte[]) + * @since 6.9 + */ + public int getStartOffset() { + return keyStart; + } + + /** + * @return end offset of the footer relative to the original raw message + * byte buffer + * + * @see #fromMessage(byte[]) + * @since 6.9 + */ + public int getEndOffset() { + return valEnd; + } + @Override public String toString() { return getKey() + ": " + getValue(); //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java index 3493dcfb47..d97812ff45 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java @@ -71,8 +71,11 @@ abstract class Generator { * * @return next available commit; null if no more are to be returned. * @throws MissingObjectException + * if an object is missing * @throws IncorrectObjectTypeException + * if an object has an unexpected type * @throws IOException + * if an IO error occurred */ 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 index 4773ca8427..ae67ca7599 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/LIFORevQueue.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/LIFORevQueue.java @@ -34,7 +34,6 @@ public class LIFORevQueue extends BlockRevQueue { super(s); } - /** {@inheritDoc} */ @Override public void add(RevCommit c) { Block b = head; @@ -47,7 +46,6 @@ public class LIFORevQueue extends BlockRevQueue { b.unpop(c); } - /** {@inheritDoc} */ @Override public RevCommit next() { final Block b = head; @@ -62,7 +60,6 @@ public class LIFORevQueue extends BlockRevQueue { return c; } - /** {@inheritDoc} */ @Override public void clear() { head = null; @@ -89,7 +86,6 @@ public class LIFORevQueue extends BlockRevQueue { return false; } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder s = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java index a213dd47c6..be29dc3138 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java @@ -12,7 +12,7 @@ package org.eclipse.jgit.revwalk; import java.io.IOException; import java.text.MessageFormat; -import java.util.LinkedList; +import java.util.ArrayDeque; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -47,7 +47,8 @@ class MergeBaseGenerator extends Generator { private int recarryTest; private int recarryMask; private int mergeBaseAncestor = -1; - private LinkedList<RevCommit> ret = new LinkedList<>(); + + private ArrayDeque<RevCommit> ret = new ArrayDeque<>(); private CarryStack stack; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java index 4e48a5c328..7c763bc9b8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java @@ -43,7 +43,7 @@ import org.eclipse.jgit.util.RawParseUtils; * 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. + * commits that reference them and the order they appear within a tree. * Ordering can be affected by changing the * {@link org.eclipse.jgit.revwalk.RevSort} used to order the commits that are * returned first. @@ -164,29 +164,6 @@ public class ObjectWalk extends RevWalk { } /** - * Create an object reachability checker that will use bitmaps if possible. - * - * This reachability checker accepts any object as target. For checks - * exclusively between commits, see - * {@link RevWalk#createReachabilityChecker()}. - * - * @return an object reachability checker, using bitmaps if possible. - * - * @throws IOException - * when the index fails to load. - * - * @since 5.8 - * @deprecated use - * {@code ObjectReader#createObjectReachabilityChecker(ObjectWalk)} - * instead. - */ - @Deprecated - public final ObjectReachabilityChecker createObjectReachabilityChecker() - throws IOException { - return reader.createObjectReachabilityChecker(this); - } - - /** * Mark an object or commit to start graph traversal from. * <p> * Callers are encouraged to use @@ -300,14 +277,12 @@ public class ObjectWalk extends RevWalk { addObject(o); } - /** {@inheritDoc} */ @Override public void sort(RevSort s) { super.sort(s); boundary = hasRevSort(RevSort.BOUNDARY); } - /** {@inheritDoc} */ @Override public void sort(RevSort s, boolean use) { super.sort(s, use); @@ -357,7 +332,6 @@ public class ObjectWalk extends RevWalk { visitationPolicy = requireNonNull(policy); } - /** {@inheritDoc} */ @Override public RevCommit next() throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -645,6 +619,8 @@ public class ObjectWalk extends RevWalk { } /** + * Get the current traversal depth from the root tree object + * * @return the current traversal depth from the root tree object * @since 5.4 */ @@ -762,7 +738,6 @@ public class ObjectWalk extends RevWalk { pathBuf = newBuf; } - /** {@inheritDoc} */ @Override public void dispose() { super.dispose(); @@ -771,7 +746,6 @@ public class ObjectWalk extends RevWalk { freeVisit = null; } - /** {@inheritDoc} */ @Override protected void reset(int retainFlags) { super.reset(retainFlags); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java index d499084b42..5afb669a15 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java @@ -26,42 +26,8 @@ import org.eclipse.jgit.errors.MissingObjectException; * @since 5.4 */ public interface ReachabilityChecker { - - /** - * Check if all targets are reachable from the {@code starter} commits. - * <p> - * Caller should parse the objectIds (preferably with - * {@code walk.parseCommit()} and handle missing/incorrect type objects - * before calling this method. - * - * @param targets - * commits to reach. - * @param starters - * known starting points. - * @return An unreachable target if at least one of the targets is - * unreachable. An empty optional if all targets are reachable from - * the starters. - * - * @throws MissingObjectException - * if any of the incoming objects doesn't exist in the - * repository. - * @throws IncorrectObjectTypeException - * if any of the incoming objects is not a commit or a tag. - * @throws IOException - * if any of the underlying indexes or readers can not be - * opened. - * - * @deprecated see {{@link #areAllReachable(Collection, Stream)} - */ - @Deprecated - default Optional<RevCommit> areAllReachable(Collection<RevCommit> targets, - Collection<RevCommit> starters) throws MissingObjectException, - IncorrectObjectTypeException, IOException { - return areAllReachable(targets, starters.stream()); - } - /** - * Check if all targets are reachable from the {@code starter} commits. + * Check if all targets are reachable from the {@code starters} commits. * <p> * Caller should parse the objectIds (preferably with * {@code walk.parseCommit()} and handle missing/incorrect type objects diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RenameCallback.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RenameCallback.java index ba3399ce9f..9856f2c252 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RenameCallback.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RenameCallback.java @@ -23,8 +23,30 @@ public abstract class RenameCallback { * Called whenever a diff was found that is actually a rename or copy of a * file. * + * <p>Subclass of this class have to override this to receive diffEntry for + * the rename. + * * @param entry * the entry representing the rename/copy */ public abstract void renamed(DiffEntry entry); + + /** + * Called whenever a diff was found that is actually a rename or copy of a + * file along with the commit that caused it. + * + * <p>Subclass of this class have an option to override this if it wants to + * know what commit generated the diffEntry. Otherwise defaults to the + * {@link RenameCallback#renamed(DiffEntry)} function. + * + * @param entry + * the entry representing the rename/copy + * @param commit + * commit at which callback occurred + * + * @since 6.7 + */ + public void renamed(DiffEntry entry, RevCommit commit) { + renamed(entry); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevBlob.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevBlob.java index f5abbb870a..1f81b6a922 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevBlob.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevBlob.java @@ -33,7 +33,6 @@ public class RevBlob extends RevObject { super(id); } - /** {@inheritDoc} */ @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 index b64c9ce906..871545fca2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java @@ -1,6 +1,6 @@ /* - * Copyright (C) 2008-2009, Google Inc. - * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2009 Google Inc. + * Copyright (C) 2008, 2024 Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -11,20 +11,18 @@ package org.eclipse.jgit.revwalk; -import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.jgit.util.RawParseUtils.guessEncoding; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.storage.commitgraph.ChangedPathFilter; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.MutableObjectId; @@ -222,7 +220,6 @@ public class RevCommit extends RevObject { flags |= PARSED; } - /** {@inheritDoc} */ @Override public final int getType() { return Constants.OBJ_COMMIT; @@ -404,14 +401,14 @@ public class RevCommit extends RevObject { * @since 5.1 */ public final byte[] getRawGpgSignature() { - final byte[] raw = buffer; - final byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' }; - final int start = RawParseUtils.headerStart(header, raw, 0); + byte[] raw = buffer; + byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' }; + int start = RawParseUtils.headerStart(header, raw, 0); if (start < 0) { return null; } - final int end = RawParseUtils.headerEnd(raw, start); - return Arrays.copyOfRange(raw, start, end); + int end = RawParseUtils.nextLfSkippingSplitLines(raw, start); + return RawParseUtils.headerValue(raw, start, end); } /** @@ -484,7 +481,8 @@ public class RevCommit extends RevObject { if (msgB < 0) { return ""; //$NON-NLS-1$ } - return RawParseUtils.decode(guessEncoding(), raw, msgB, raw.length); + return RawParseUtils.decode(guessEncoding(buffer), raw, msgB, + raw.length); } /** @@ -510,7 +508,8 @@ public class RevCommit extends RevObject { } int msgE = RawParseUtils.endOfParagraph(raw, msgB); - String str = RawParseUtils.decode(guessEncoding(), raw, msgB, msgE); + String str = RawParseUtils.decode(guessEncoding(buffer), raw, msgB, + msgE); if (hasLF(raw, msgB, msgE)) { str = StringUtils.replaceLineBreaksWithSpace(str); } @@ -525,6 +524,30 @@ public class RevCommit extends RevObject { } /** + * Parse the commit message and return its first line, i.e., everything up + * to but not including the first newline, if any. + * + * @return the first line of the decoded commit message as a string; never + * {@code null}. + * @since 7.2 + */ + public final String getFirstMessageLine() { + int msgB = RawParseUtils.commitMessage(buffer, 0); + if (msgB < 0) { + return ""; //$NON-NLS-1$ + } + int msgE = msgB; + byte[] raw = buffer; + while (msgE < raw.length && raw[msgE] != '\n') { + msgE++; + } + if (msgE > msgB && msgE > 0 && raw[msgE - 1] == '\r') { + msgE--; + } + return RawParseUtils.decode(guessEncoding(buffer), buffer, msgB, msgE); + } + + /** * Determine the encoding of the commit message buffer. * <p> * Locates the "encoding" header (if present) and returns its value. Due to @@ -562,14 +585,6 @@ public class RevCommit extends RevObject { return RawParseUtils.parseEncoding(buffer); } - private Charset guessEncoding() { - try { - return getEncoding(); - } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { - return UTF_8; - } - } - /** * Parse the footer lines (e.g. "Signed-off-by") for machine processing. * <p> @@ -578,8 +593,9 @@ public class RevCommit extends RevObject { * 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: + * the value is free-form. The Value may be split over multiple lines with + * each subsequent line starting with at least one whitespace. 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) @@ -592,50 +608,14 @@ public class RevCommit extends RevObject { * @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<>(4); - final Charset enc = guessEncoding(); - 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; + return FooterLine.fromMessage(buffer); } /** * Get the values of all footer lines with the given key. * * @param keyName - * footer key to find values of, case insensitive. + * 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 @@ -643,30 +623,22 @@ public class RevCommit extends RevObject { * @see #getFooterLines() */ public final List<String> getFooterLines(String keyName) { - return getFooterLines(new FooterKey(keyName)); + return FooterLine.getValues(getFooterLines(), keyName); } /** * Get the values of all footer lines with the given key. * - * @param keyName - * footer key to find values of, case insensitive. + * @param key + * 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(FooterKey keyName) { - final List<FooterLine> src = getFooterLines(); - if (src.isEmpty()) - return Collections.emptyList(); - final ArrayList<String> r = new ArrayList<>(src.size()); - for (FooterLine f : src) { - if (f.matches(keyName)) - r.add(f.getValue()); - } - return r; + public final List<String> getFooterLines(FooterKey key) { + return FooterLine.getValues(getFooterLines(), key); } /** @@ -688,6 +660,21 @@ public class RevCommit extends RevObject { } /** + * Get the changed path filter of the commit. + * <p> + * This is null when there is no commit graph file, the commit is not in the + * commit graph file, or the commit graph file was generated without changed + * path filters. + * + * @param rw A revwalk to load the commit graph (if available) + * @return the changed path filter + * @since 6.7 + */ + public ChangedPathFilter getChangedPathFilter(RevWalk rw) { + return null; + } + + /** * Reset this commit to allow another RevWalk with the same instances. * <p> * Subclasses <b>must</b> call <code>super.reset()</code> to ensure the @@ -713,7 +700,6 @@ public class RevCommit extends RevObject { buffer = null; } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder s = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitCG.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitCG.java index 4d3664da11..c7a03992b7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitCG.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitCG.java @@ -14,6 +14,7 @@ import java.io.IOException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.storage.commitgraph.ChangedPathFilter; import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; @@ -44,7 +45,6 @@ class RevCommitCG extends RevCommit { this.graphPosition = graphPosition; } - /** {@inheritDoc} */ @Override void parseCanonical(RevWalk walk, byte[] raw) throws IOException { if (walk.isRetainBody()) { @@ -53,7 +53,6 @@ class RevCommitCG extends RevCommit { parseInGraph(walk); } - /** {@inheritDoc} */ @Override void parseHeaders(RevWalk walk) throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -98,9 +97,14 @@ class RevCommitCG extends RevCommit { flags |= PARSED; } - /** {@inheritDoc} */ @Override int getGeneration() { return generation; } + + /** {@inheritDoc} */ + @Override + public ChangedPathFilter getChangedPathFilter(RevWalk rw) { + return rw.commitGraph().getChangedPathFilter(graphPosition); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitList.java index 59213a8e60..58f5d425cc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitList.java @@ -25,7 +25,6 @@ import org.eclipse.jgit.revwalk.filter.RevFilter; public class RevCommitList<E extends RevCommit> extends RevObjectList<E> { private RevWalk walker; - /** {@inheritDoc} */ @Override public void clear() { super.clear(); @@ -313,7 +312,6 @@ public class RevCommitList<E extends RevCommit> extends RevObjectList<E> { * walker specified by {@link #source(RevWalk)} is pumped until the * specified commit is loaded. Callers can test the final size of the list * by {@link #size()} to determine if the high water mark specified was met. - * <p> * * @param commitToLoad * commit the caller wants this list to contain when the fill diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlag.java index a0160ddbef..3221bf6e8f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlag.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlag.java @@ -67,7 +67,6 @@ public class RevFlag { return walker; } - /** {@inheritDoc} */ @Override public String toString() { return name; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObject.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObject.java index 4d2684a0f0..55ea30d24b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObject.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObject.java @@ -132,7 +132,6 @@ public abstract class RevObject extends ObjectIdOwnerMap.Entry { flags &= ~set.mask; } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder s = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObjectList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObjectList.java index 0393f55957..ad8aeedeb9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObjectList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObjectList.java @@ -47,7 +47,6 @@ public class RevObjectList<E extends RevObject> extends AbstractList<E> { // Initialized above. } - /** {@inheritDoc} */ @Override public void add(int index, E element) { if (index != size) @@ -58,7 +57,6 @@ public class RevObjectList<E extends RevObject> extends AbstractList<E> { size++; } - /** {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public E set(int index, E element) { @@ -80,7 +78,6 @@ public class RevObjectList<E extends RevObject> extends AbstractList<E> { return (E) old; } - /** {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public E get(int index) { @@ -95,13 +92,11 @@ public class RevObjectList<E extends RevObject> extends AbstractList<E> { return s != null ? (E) s.contents[index] : null; } - /** {@inheritDoc} */ @Override public int size() { return size; } - /** {@inheritDoc} */ @Override public void clear() { contents = new Block(0); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java index b9d145008b..0737a78085 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java @@ -38,8 +38,17 @@ import org.eclipse.jgit.util.StringUtils; */ public class RevTag extends RevObject { - private static final byte[] hSignature = Constants - .encodeASCII("-----BEGIN PGP SIGNATURE-----"); //$NON-NLS-1$ + private static final byte[] SIGNATURE_START = Constants + .encodeASCII("-----BEGIN"); //$NON-NLS-1$ + + private static final byte[] GPG_SIGNATURE_START = Constants + .encodeASCII(Constants.GPG_SIGNATURE_PREFIX); + + private static final byte[] CMS_SIGNATURE_START = Constants + .encodeASCII(Constants.CMS_SIGNATURE_PREFIX); + + private static final byte[] SSH_SIGNATURE_START = Constants + .encodeASCII(Constants.SSH_SIGNATURE_PREFIX); /** * Parse an annotated tag from its canonical format. @@ -149,7 +158,6 @@ public class RevTag extends RevObject { flags |= PARSED; } - /** {@inheritDoc} */ @Override public final int getType() { return Constants.OBJ_TAG; @@ -209,20 +217,27 @@ public class RevTag extends RevObject { return msgB; } // Find the last signature start and return the rest - int start = nextStart(hSignature, raw, msgB); + int start = nextStart(SIGNATURE_START, raw, msgB); if (start < 0) { return start; } int next = RawParseUtils.nextLF(raw, start); while (next < raw.length) { - int newStart = nextStart(hSignature, raw, next); + int newStart = nextStart(SIGNATURE_START, raw, next); if (newStart < 0) { break; } start = newStart; next = RawParseUtils.nextLF(raw, start); } - return start; + // SIGNATURE_START is just a prefix. Check that it is one of the known + // full signature start tags. + if (RawParseUtils.match(raw, start, GPG_SIGNATURE_START) > 0 + || RawParseUtils.match(raw, start, CMS_SIGNATURE_START) > 0 + || RawParseUtils.match(raw, start, SSH_SIGNATURE_START) > 0) { + return start; + } + return -1; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTree.java index 8119af468a..d81af0c029 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTree.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTree.java @@ -33,7 +33,6 @@ public class RevTree extends RevObject { super(id); } - /** {@inheritDoc} */ @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 index 3737c6b67b..41f98bad84 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java @@ -19,8 +19,14 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util. +Optional; +import java.util.Set; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; @@ -30,9 +36,9 @@ import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.RevWalkException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.AsyncObjectLoaderQueue; -import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.NullProgressMonitor; @@ -220,7 +226,6 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { /** * Create a new revision walker for a given repository. - * <p> * * @param or * the reader the walker will obtain data from. The reader is not @@ -236,7 +241,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { idBuffer = new MutableObjectId(); objects = new ObjectIdOwnerMap<>(); roots = new ArrayList<>(); - queue = new DateRevQueue(false); + queue = newDateRevQueue(false); pending = new StartGenerator(this); sorting = EnumSet.of(RevSort.NONE); filter = RevFilter.ALL; @@ -245,6 +250,30 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { commitGraph = null; } + static AbstractRevQueue newDateRevQueue(boolean firstParent) { + if(usePriorityQueue()) { + return new DateRevPriorityQueue(firstParent); + } + + return new DateRevQueue(firstParent); + } + + static DateRevQueue newDateRevQueue(Generator g) throws IOException { + if(usePriorityQueue()) { + return new DateRevPriorityQueue(g); + } + + return new DateRevQueue(g); + } + + @SuppressWarnings("boxing") + private static boolean usePriorityQueue() { + return Optional + .ofNullable(System.getProperty("REVWALK_USE_PRIORITY_QUEUE")) //$NON-NLS-1$ + .map(Boolean::parseBoolean) + .orElse(false); + } + /** * Get the reader this walker is using to load objects. * @@ -255,23 +284,6 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { } /** - * Get a reachability checker for commits over this revwalk. - * - * @return the most efficient reachability checker for this repository. - * @throws IOException - * if it cannot open any of the underlying indices. - * - * @since 5.4 - * @deprecated use {@code ObjectReader#createReachabilityChecker(RevWalk)} - * instead. - */ - @Deprecated - public final ReachabilityChecker createReachabilityChecker() - throws IOException { - return reader.createReachabilityChecker(this); - } - - /** * {@inheritDoc} * <p> * Release any resources used by this walker's reader. @@ -454,7 +466,6 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * <p> * A commit is merged into a ref if we can find a path of commits that leads * from that specific ref and ends at <code>commit</code>. - * <p> * * @param commit * commit the caller thinks is reachable from <code>refs</code>. @@ -476,7 +487,6 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * <p> * A commit is merged into a ref if we can find a path of commits that leads * from that specific ref and ends at <code>commit</code>. - * <p> * * @param commit * commit the caller thinks is reachable from <code>refs</code>. @@ -518,6 +528,27 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { } /** + * Determine if a <code>commit</code> is merged into any of the given + * <code>revs</code>. + * + * @param commit + * commit the caller thinks is reachable from <code>revs</code>. + * @param revs + * commits to start iteration from, and which is most likely a + * descendant (child) of <code>commit</code>. + * @return true if commit is merged into any of the revs; false otherwise. + * @throws java.io.IOException + * a pack file or loose object could not be read. + * @since 6.10.1 + */ + public boolean isMergedIntoAnyCommit(RevCommit commit, Collection<RevCommit> revs) + throws IOException { + return getCommitsMergedInto(commit, revs, + GetMergedIntoStrategy.RETURN_ON_FIRST_FOUND, + NullProgressMonitor.INSTANCE).size() > 0; + } + + /** * Determine if a <code>commit</code> is merged into all of the given * <code>refs</code>. * @@ -540,7 +571,26 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { private List<Ref> getMergedInto(RevCommit needle, Collection<Ref> haystacks, Enum returnStrategy, ProgressMonitor monitor) throws IOException { + Map<RevCommit, List<Ref>> refsByCommit = new HashMap<>(); + for (Ref r : haystacks) { + RevObject o = peel(parseAny(r.getObjectId())); + if (!(o instanceof RevCommit)) { + continue; + } + refsByCommit.computeIfAbsent((RevCommit) o, c -> new ArrayList<>()).add(r); + } + monitor.update(1); List<Ref> result = new ArrayList<>(); + for (RevCommit c : getCommitsMergedInto(needle, refsByCommit.keySet(), + returnStrategy, monitor)) { + result.addAll(refsByCommit.get(c)); + } + return result; + } + + private Set<RevCommit> getCommitsMergedInto(RevCommit needle, Collection<RevCommit> haystacks, + Enum returnStrategy, ProgressMonitor monitor) throws IOException { + Set<RevCommit> result = new HashSet<>(); List<RevCommit> uninteresting = new ArrayList<>(); List<RevCommit> marked = new ArrayList<>(); RevFilter oldRF = filter; @@ -556,16 +606,11 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { needle.parseHeaders(this); } int cutoff = needle.getGeneration(); - for (Ref r : haystacks) { + for (RevCommit c : haystacks) { if (monitor.isCancelled()) { return result; } monitor.update(1); - RevObject o = peel(parseAny(r.getObjectId())); - if (!(o instanceof RevCommit)) { - continue; - } - RevCommit c = (RevCommit) o; reset(UNINTERESTING | TEMP_MARK); markStart(c); boolean commitFound = false; @@ -577,7 +622,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { } if (References.isSameObject(next, needle) || (next.flags & TEMP_MARK) != 0) { - result.add(r); + result.add(c); if (returnStrategy == GetMergedIntoStrategy.RETURN_ON_FIRST_FOUND) { return result; } @@ -824,6 +869,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { } /** + * Whether only first-parent links should be followed when walking + * * @return whether only first-parent links should be followed when walking. * * @since 5.5 @@ -848,7 +895,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { assertNotStarted(); assertNoCommitsMarkedStart(); firstParent = enable; - queue = new DateRevQueue(firstParent); + queue = newDateRevQueue(firstParent); pending = new StartGenerator(this); } @@ -1197,6 +1244,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { /** * Asynchronous object parsing. * + * @param <T> + * Type of returned {@code ObjectId} * @param objectIds * objects to open from the object store. The supplied collection * must not be modified until the queue has finished. @@ -1566,7 +1615,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { } roots.clear(); - queue = new DateRevQueue(firstParent); + queue = newDateRevQueue(firstParent); pending = new StartGenerator(this); } @@ -1587,7 +1636,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { firstParent = false; objects.clear(); roots.clear(); - queue = new DateRevQueue(firstParent); + queue = newDateRevQueue(firstParent); pending = new StartGenerator(this); shallowCommitsInitialized = false; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java index e52e916318..4bfe2fda02 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java @@ -51,8 +51,11 @@ public final class RevWalkUtils { * should be done until there are no more commits * @return the number of commits * @throws org.eclipse.jgit.errors.MissingObjectException + * if object is missing * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException + * if object has unexpected type * @throws java.io.IOException + * if an IO error occurred */ public static int count(final RevWalk walk, final RevCommit start, final RevCommit end) throws MissingObjectException, @@ -80,8 +83,11 @@ public final class RevWalkUtils { * should be done until there are no more commits * @return the commits found * @throws org.eclipse.jgit.errors.MissingObjectException + * if object is missing * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException + * if object has unexpected type * @throws java.io.IOException + * if an IO error occurred */ public static List<RevCommit> find(final RevWalk walk, final RevCommit start, final RevCommit end) @@ -116,8 +122,11 @@ public final class RevWalkUtils { * the set of branches we want to see reachability from * @return the list of branches a given commit is reachable from * @throws org.eclipse.jgit.errors.MissingObjectException + * if object is missing * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException + * if object has unexpected type * @throws java.io.IOException + * if an IO error occurred */ public static List<Ref> findBranchesReachableFrom(RevCommit commit, RevWalk revWalk, Collection<Ref> refs) @@ -147,8 +156,11 @@ public final class RevWalkUtils { * the callback for progress and cancellation * @return the list of branches a given commit is reachable from * @throws org.eclipse.jgit.errors.MissingObjectException + * if object is missing * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException + * if object has unexpected type * @throws java.io.IOException + * if an IO error occurred * @since 5.4 */ public static List<Ref> findBranchesReachableFrom(RevCommit commit, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java index 2c88bb872e..4a93f4d745 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java @@ -101,9 +101,13 @@ class RewriteGenerator extends Generator { * of this commit by the previous {@link PendingGenerator}. * * @param c + * given commit * @throws MissingObjectException + * if an object is missing * @throws IncorrectObjectTypeException + * if an object has an unexpected type * @throws IOException + * if an IO error occurred */ private void applyFilterToParents(RevCommit c) throws MissingObjectException, IncorrectObjectTypeException, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java index a79901ca10..6854b6083d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java @@ -93,10 +93,11 @@ class StartGenerator extends Generator { final DateRevQueue pending; int pendingOutputType = 0; - if (q instanceof DateRevQueue) + if (q instanceof DateRevQueue) { pending = (DateRevQueue)q; - else - pending = new DateRevQueue(q); + } else { + pending = RevWalk.newDateRevQueue(q); + } if (tf != TreeFilter.ALL) { int rewriteFlag; if (w.getRewriteParents()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoNonIntermixSortGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoNonIntermixSortGenerator.java index 452545a04e..c9d8ff90fd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoNonIntermixSortGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoNonIntermixSortGenerator.java @@ -33,8 +33,11 @@ class TopoNonIntermixSortGenerator extends Generator { * @param s * generator to pull all commits out of, and into this buffer. * @throws MissingObjectException + * if an object is missing * @throws IncorrectObjectTypeException + * if an object has an unexpected type * @throws IOException + * if an IO error occurred */ TopoNonIntermixSortGenerator(Generator s) throws MissingObjectException, IncorrectObjectTypeException, IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java index 4739f78fc0..950b0a45b4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java @@ -33,8 +33,11 @@ class TopoSortGenerator extends Generator { * @param s * generator to pull all commits out of, and into this buffer. * @throws MissingObjectException + * if an object is missing * @throws IncorrectObjectTypeException + * if an object has an unexpected type * @throws IOException + * if an IO error occurred */ TopoSortGenerator(Generator s) throws MissingObjectException, IncorrectObjectTypeException, IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java index f921449ffa..e9a3e72c7f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java @@ -17,7 +17,6 @@ import org.eclipse.jgit.diff.DiffConfig; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.RenameDetector; -import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.StopWalkException; @@ -25,6 +24,7 @@ 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; +import org.eclipse.jgit.treewalk.filter.TreeFilter.MutableBoolean; /** * Filter applying a {@link org.eclipse.jgit.treewalk.filter.TreeFilter} against @@ -47,6 +47,14 @@ public class TreeRevFilter extends RevFilter { private final TreeWalk pathFilter; + private final MutableBoolean changedPathFilterUsed = new MutableBoolean(); + + private long changedPathFilterTruePositive = 0; + + private long changedPathFilterFalsePositive = 0; + + private long changedPathFilterNegative = 0; + /** * Create a {@link org.eclipse.jgit.revwalk.filter.RevFilter} from a * {@link org.eclipse.jgit.treewalk.filter.TreeFilter}. @@ -92,13 +100,11 @@ public class TreeRevFilter extends RevFilter { this.rewriteFlag = rewriteFlag; } - /** {@inheritDoc} */ @Override public RevFilter clone() { throw new UnsupportedOperationException(); } - /** {@inheritDoc} */ @Override public boolean include(RevWalk walker, RevCommit c) throws StopWalkException, MissingObjectException, @@ -119,17 +125,34 @@ public class TreeRevFilter extends RevFilter { } trees[nParents] = c.getTree(); tw.reset(trees); + changedPathFilterUsed.reset(); 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. + TreeFilter tf = pathFilter.getFilter(); + boolean mustCalculateChgs = tf.shouldTreeWalk(c, walker, + changedPathFilterUsed); + if (mustCalculateChgs) { + while (tw.next()) { + chgs++; + if (tw.getRawMode(0) == 0 && tw.getRawMode(1) != 0) { + adds++; + } else { + break; // no point in looking at this further. + } + } + if (changedPathFilterUsed.get()) { + if (chgs > 0) { + changedPathFilterTruePositive++; + } else { + changedPathFilterFalsePositive++; + } + } + } else { + if (changedPathFilterUsed.get()) { + changedPathFilterNegative++; } } @@ -149,7 +172,8 @@ public class TreeRevFilter extends RevFilter { // commit. We need to update our filter to its older // name, if we can discover it. Find out what that is. // - updateFollowFilter(trees, ((FollowFilter) tw.getFilter()).cfg); + updateFollowFilter(trees, ((FollowFilter) tw.getFilter()).cfg, + c); } return true; } else if (nParents == 0) { @@ -241,15 +265,47 @@ public class TreeRevFilter extends RevFilter { return false; } - /** {@inheritDoc} */ @Override public boolean requiresCommitBody() { return false; } - private void updateFollowFilter(ObjectId[] trees, DiffConfig cfg) - throws MissingObjectException, IncorrectObjectTypeException, - CorruptObjectException, IOException { + /** + * Return how many times a changed path filter correctly predicted that a + * path was changed in a commit, for statistics gathering purposes. + * + * @return count of true positives + * @since 6.7 + */ + public long getChangedPathFilterTruePositive() { + return changedPathFilterTruePositive; + } + + /** + * Return how many times a changed path filter wrongly predicted that a path + * was changed in a commit, for statistics gathering purposes. + * + * @return count of false positives + * @since 6.7 + */ + public long getChangedPathFilterFalsePositive() { + return changedPathFilterFalsePositive; + } + + /** + * Return how many times a changed path filter predicted that a path was not + * changed in a commit (allowing that commit to be skipped), for statistics + * gathering purposes. + * + * @return count of negatives + * @since 6.7 + */ + public long getChangedPathFilterNegative() { + return changedPathFilterNegative; + } + + private void updateFollowFilter(ObjectId[] trees, DiffConfig cfg, + RevCommit commit) throws IOException { TreeWalk tw = pathFilter; FollowFilter oldFilter = (FollowFilter) tw.getFilter(); tw.setFilter(TreeFilter.ANY_DIFF); @@ -266,7 +322,7 @@ public class TreeRevFilter extends RevFilter { newFilter = FollowFilter.create(ent.getOldPath(), cfg); RenameCallback callback = oldFilter.getRenameCallback(); if (callback != null) { - callback.renamed(ent); + callback.renamed(ent, commit); // forward the callback to the new follow filter ((FollowFilter) newFilter).setRenameCallback(callback); } 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 index 6ac490a345..c9186b5491 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitTimeRevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitTimeRevFilter.java @@ -12,6 +12,7 @@ package org.eclipse.jgit.revwalk.filter; import java.io.IOException; +import java.time.Instant; import java.util.Date; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -30,9 +31,24 @@ public abstract class CommitTimeRevFilter extends RevFilter { * @param ts * the point in time to cut on. * @return a new filter to select commits on or before <code>ts</code>. + * + * @deprecated Use {@link #before(Instant)} instead. */ + @Deprecated(since="7.2") public static final RevFilter before(Date ts) { - return before(ts.getTime()); + return before(ts.toInstant()); + } + + /** + * 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>. + * @since 7.2 + */ + public static RevFilter before(Instant ts) { + return new Before(ts); } /** @@ -43,7 +59,7 @@ public abstract class CommitTimeRevFilter extends RevFilter { * @return a new filter to select commits on or before <code>ts</code>. */ public static final RevFilter before(long ts) { - return new Before(ts); + return new Before(Instant.ofEpochMilli(ts)); } /** @@ -52,9 +68,24 @@ public abstract class CommitTimeRevFilter extends RevFilter { * @param ts * the point in time to cut on. * @return a new filter to select commits on or after <code>ts</code>. + * + * @deprecated Use {@link #after(Instant)} instead. */ + @Deprecated(since="7.2") public static final RevFilter after(Date ts) { - return after(ts.getTime()); + return after(ts.toInstant()); + } + + /** + * 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>. + * @since 7.2 + */ + public static RevFilter after(Instant ts) { + return new After(ts); } /** @@ -65,7 +96,7 @@ public abstract class CommitTimeRevFilter extends RevFilter { * @return a new filter to select commits on or after <code>ts</code>. */ public static final RevFilter after(long ts) { - return new After(ts); + return after(Instant.ofEpochMilli(ts)); } /** @@ -75,9 +106,28 @@ public abstract class CommitTimeRevFilter extends RevFilter { * @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. + * + * @deprecated Use {@link #between(Instant, Instant)} instead. */ + @Deprecated(since="7.2") public static final RevFilter between(Date since, Date until) { - return between(since.getTime(), until.getTime()); + return between(since.toInstant(), until.toInstant()); + } + + /** + * 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. + * @since 7.2 + */ + public static RevFilter between(Instant since, Instant until) { + return new Between(since, until); } /** @@ -87,9 +137,12 @@ public abstract class CommitTimeRevFilter extends RevFilter { * @param since the point in time to cut on, in milliseconds. * @param until the point in time to cut off, in millisconds. * @return a new filter to select commits between the given date/times. + * + * @deprecated Use {@link #between(Instant, Instant)} instead. */ + @Deprecated(since="7.2") public static final RevFilter between(long since, long until) { - return new Between(since, until); + return new Between(Instant.ofEpochMilli(since), Instant.ofEpochMilli(until)); } final int when; @@ -98,21 +151,23 @@ public abstract class CommitTimeRevFilter extends RevFilter { when = (int) (ts / 1000); } - /** {@inheritDoc} */ + CommitTimeRevFilter(Instant t) { + when = (int) t.getEpochSecond(); + } + @Override public RevFilter clone() { return this; } - /** {@inheritDoc} */ @Override public boolean requiresCommitBody() { return false; } private static class Before extends CommitTimeRevFilter { - Before(long ts) { - super(ts); + Before(Instant t) { + super(t); } @Override @@ -125,14 +180,12 @@ public abstract class CommitTimeRevFilter extends RevFilter { @SuppressWarnings("nls") @Override public String toString() { - return super.toString() + "(" + new Date(when * 1000L) + ")"; + return super.toString() + "(" + Instant.ofEpochSecond(when) + ")"; } } private static class After extends CommitTimeRevFilter { - After(long ts) { - super(ts); - } + After(Instant t) { super(t); } @Override public boolean include(RevWalk walker, RevCommit cmit) @@ -150,16 +203,16 @@ public abstract class CommitTimeRevFilter extends RevFilter { @SuppressWarnings("nls") @Override public String toString() { - return super.toString() + "(" + new Date(when * 1000L) + ")"; + return super.toString() + "(" + Instant.ofEpochSecond(when) + ")"; } } private static class Between extends CommitTimeRevFilter { private final int until; - Between(long since, long until) { + Between(Instant since, Instant until) { super(since); - this.until = (int) (until / 1000); + this.until = (int) until.getEpochSecond(); } @Override @@ -172,8 +225,8 @@ public abstract class CommitTimeRevFilter extends RevFilter { @SuppressWarnings("nls") @Override public String toString() { - return super.toString() + "(" + new Date(when * 1000L) + " - " - + new Date(until * 1000L) + ")"; + return super.toString() + "(" + Instant.ofEpochSecond(when) + " - " + + Instant.ofEpochSecond(until) + ")"; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/MaxCountRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/MaxCountRevFilter.java index 0e3a803b64..09bcb33f2e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/MaxCountRevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/MaxCountRevFilter.java @@ -46,7 +46,6 @@ public class MaxCountRevFilter extends RevFilter { this.maxCount = maxCount; } - /** {@inheritDoc} */ @Override public boolean include(RevWalk walker, RevCommit cmit) throws StopWalkException, MissingObjectException, @@ -57,7 +56,6 @@ public class MaxCountRevFilter extends RevFilter { return true; } - /** {@inheritDoc} */ @Override public RevFilter clone() { return new MaxCountRevFilter(maxCount); 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 index 1482c51291..8fcebc9fa1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/NotRevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/NotRevFilter.java @@ -38,13 +38,11 @@ public class NotRevFilter extends RevFilter { a = one; } - /** {@inheritDoc} */ @Override public RevFilter negate() { return a; } - /** {@inheritDoc} */ @Override public boolean include(RevWalk walker, RevCommit c) throws MissingObjectException, IncorrectObjectTypeException, @@ -52,19 +50,16 @@ public class NotRevFilter extends RevFilter { return !a.include(walker, c); } - /** {@inheritDoc} */ @Override public boolean requiresCommitBody() { return a.requiresCommitBody(); } - /** {@inheritDoc} */ @Override public RevFilter clone() { return new NotRevFilter(a.clone()); } - /** {@inheritDoc} */ @Override public String toString() { return "NOT " + a.toString(); //$NON-NLS-1$ 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 index 256e4794cd..8430646305 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFilter.java @@ -264,7 +264,6 @@ public abstract class RevFilter { @Override public abstract RevFilter clone(); - /** {@inheritDoc} */ @Override public String toString() { String n = getClass().getName(); 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 index 16d9f76143..f9ee632b50 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFlagFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFlagFilter.java @@ -91,13 +91,11 @@ public abstract class RevFlagFilter extends RevFilter { flags = m; } - /** {@inheritDoc} */ @Override public RevFilter clone() { return this; } - /** {@inheritDoc} */ @Override public String toString() { return super.toString() + flags; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SkipRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SkipRevFilter.java index 84df36a2d2..bf93b7ba3e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SkipRevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SkipRevFilter.java @@ -45,7 +45,6 @@ public class SkipRevFilter extends RevFilter { this.skip = skip; } - /** {@inheritDoc} */ @Override public boolean include(RevWalk walker, RevCommit cmit) throws StopWalkException, MissingObjectException, @@ -55,7 +54,6 @@ public class SkipRevFilter extends RevFilter { return true; } - /** {@inheritDoc} */ @Override public RevFilter clone() { return new SkipRevFilter(skip); 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 index 7f00dfd504..ff1fea2418 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SubStringRevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SubStringRevFilter.java @@ -67,7 +67,6 @@ public abstract class SubStringRevFilter extends RevFilter { pattern = new RawSubStringPattern(patternText); } - /** {@inheritDoc} */ @Override public boolean include(RevWalk walker, RevCommit cmit) throws MissingObjectException, IncorrectObjectTypeException, @@ -75,7 +74,6 @@ public abstract class SubStringRevFilter extends RevFilter { return pattern.match(text(cmit)) >= 0; } - /** {@inheritDoc} */ @Override public boolean requiresCommitBody() { return true; @@ -90,13 +88,11 @@ public abstract class SubStringRevFilter extends RevFilter { */ protected abstract RawCharSequence text(RevCommit cmit); - /** {@inheritDoc} */ @Override public RevFilter clone() { return this; // Typically we are actually thread-safe. } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java index cba5e1697c..3b7c02eca9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java @@ -5,7 +5,7 @@ * Copyright (C) 2009, JetBrains s.r.o. * Copyright (C) 2008-2009, 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 others + * Copyright (C) 2008, 2023 Thad Hughes <thadh@thad.corp.google.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -20,8 +20,11 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.text.MessageFormat; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantReadWriteLock; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.LockFailedException; @@ -46,12 +49,18 @@ public class FileBasedConfig extends StoredConfig { private final FS fs; + // In-process synchronization between load() and save(). + private final ReentrantReadWriteLock lock; + private boolean utf8Bom; private volatile FileSnapshot snapshot; private volatile ObjectId hash; + private AtomicBoolean exists = new AtomicBoolean(); + + /** * Create a configuration with no default fallback. * @@ -82,9 +91,9 @@ public class FileBasedConfig extends StoredConfig { this.fs = fs; this.snapshot = FileSnapshot.DIRTY; this.hash = ObjectId.zeroId(); + this.lock = new ReentrantReadWriteLock(false); } - /** {@inheritDoc} */ @Override protected boolean notifyUponTransientChanges() { // we will notify listeners upon save() @@ -100,6 +109,10 @@ public class FileBasedConfig extends StoredConfig { return configFile; } + boolean exists() { + return exists.get(); + } + /** * {@inheritDoc} * <p> @@ -110,49 +123,51 @@ public class FileBasedConfig extends StoredConfig { */ @Override public void load() throws IOException, ConfigInvalidException { + lock.readLock().lock(); try { - FileSnapshot[] lastSnapshot = { null }; - Boolean wasRead = FileUtils.readWithRetries(getFile(), f -> { - final FileSnapshot oldSnapshot = snapshot; - final FileSnapshot newSnapshot; - // don't use config in this snapshot to avoid endless recursion - newSnapshot = FileSnapshot.saveNoConfig(f); - lastSnapshot[0] = newSnapshot; - final byte[] in = IO.readFully(f); - final ObjectId newHash = hash(in); - if (hash.equals(newHash)) { - if (oldSnapshot.equals(newSnapshot)) { - oldSnapshot.setClean(newSnapshot); - } else { - snapshot = newSnapshot; - } - } else { - final String decoded; - if (isUtf8(in)) { - decoded = RawParseUtils.decode(UTF_8, - in, 3, in.length); - utf8Bom = true; - } else { - decoded = RawParseUtils.decode(in); - } - fromText(decoded); - snapshot = newSnapshot; - hash = newHash; - } - return Boolean.TRUE; - }); + Boolean wasRead = FileUtils.readWithRetries(getFile(), this::load); if (wasRead == null) { clear(); - snapshot = lastSnapshot[0]; + snapshot = FileSnapshot.MISSING_FILE; } + exists.set(wasRead != null); } catch (IOException e) { throw e; } catch (Exception e) { throw new ConfigInvalidException(MessageFormat .format(JGitText.get().cannotReadFile, getFile()), e); + } finally { + lock.readLock().unlock(); } } + private Boolean load(File f) throws Exception { + FileSnapshot oldSnapshot = snapshot; + // don't use config in this snapshot to avoid endless recursion + FileSnapshot newSnapshot = FileSnapshot.saveNoConfig(f); + byte[] in = IO.readFully(f); + ObjectId newHash = hash(in); + if (hash.equals(newHash)) { + if (oldSnapshot.equals(newSnapshot)) { + oldSnapshot.setClean(newSnapshot); + } else { + snapshot = newSnapshot; + } + } else { + String decoded; + if (isUtf8(in)) { + decoded = RawParseUtils.decode(UTF_8, in, 3, in.length); + utf8Bom = true; + } else { + decoded = RawParseUtils.decode(in); + } + fromText(decoded); + snapshot = newSnapshot; + hash = newHash; + } + return Boolean.TRUE; + } + /** * {@inheritDoc} * <p> @@ -166,38 +181,45 @@ public class FileBasedConfig extends StoredConfig { */ @Override public void save() throws IOException { - final byte[] out; - final String text = toText(); - if (utf8Bom) { - final ByteArrayOutputStream bos = new ByteArrayOutputStream(); - bos.write(0xEF); - bos.write(0xBB); - bos.write(0xBF); - bos.write(text.getBytes(UTF_8)); - out = bos.toByteArray(); - } else { - out = Constants.encode(text); - } - - final LockFile lf = new LockFile(getFile()); + lock.writeLock().lock(); try { - if (!lf.lock()) { - throw new LockFailedException(getFile()); + byte[] out; + String text = toText(); + if (utf8Bom) { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(0xEF); + bos.write(0xBB); + bos.write(0xBF); + bos.write(text.getBytes(UTF_8)); + out = bos.toByteArray(); + } else { + out = Constants.encode(text); } - lf.setNeedSnapshotNoConfig(true); - lf.write(out); - if (!lf.commit()) - throw new IOException(MessageFormat.format(JGitText.get().cannotCommitWriteTo, getFile())); + + LockFile lf = new LockFile(getFile()); + try { + if (!lf.lock()) { + throw new LockFailedException(getFile()); + } + lf.setNeedSnapshotNoConfig(true); + lf.write(out); + if (!lf.commit()) { + throw new IOException(MessageFormat.format( + JGitText.get().cannotCommitWriteTo, getFile())); + } + } finally { + lf.unlock(); + } + snapshot = lf.getCommitSnapshot(); + hash = hash(out); + exists.set(true); } finally { - lf.unlock(); + lock.writeLock().unlock(); } - snapshot = lf.getCommitSnapshot(); - hash = hash(out); // notify the listeners fireConfigChangedEvent(); } - /** {@inheritDoc} */ @Override public void clear() { hash = hash(new byte[0]); @@ -208,7 +230,6 @@ public class FileBasedConfig extends StoredConfig { return ObjectId.fromRaw(Constants.newMessageDigest().digest(rawText)); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { @@ -246,6 +267,8 @@ public class FileBasedConfig extends StoredConfig { try { return IO.readFully(file); + } catch (FileNotFoundException e) { + return null; } catch (IOException ioe) { throw new ConfigInvalidException(MessageFormat .format(JGitText.get().cannotReadFile, relPath), ioe); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/UserConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/UserConfigFile.java new file mode 100644 index 0000000000..c1eaac7b55 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/UserConfigFile.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023, Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.storage.file; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.util.FS; + +/** + * User (global) git config based on two possible locations, + * {@code ~/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config}. + * <p> + * For reading, both locations are considered, first the XDG file, then the file + * in the home directory. All updates occur in the last file read that exists, + * or in the home directory file if neither exists. In other words: if only the + * XDG file exists, it is updated, otherwise the home directory file is updated. + * </p> + * + * @since 6.7 + */ +public class UserConfigFile extends FileBasedConfig { + + private final FileBasedConfig parent; + + /** + * Creates a new {@link UserConfigFile}. + * + * @param parent + * parent {@link Config}; may be {@code null} + * @param config + * {@link File} for {@code ~/.gitconfig} + * @param xdgConfig + * {@link File} for {@code $XDG_CONFIG_HOME/.gitconfig} + * @param fileSystem + * {@link FS} to use for the two files; normally + * {@link FS#DETECTED} + */ + public UserConfigFile(Config parent, @NonNull File config, + @NonNull File xdgConfig, @NonNull FS fileSystem) { + super(new FileBasedConfig(parent, xdgConfig, fileSystem), config, + fileSystem); + this.parent = (FileBasedConfig) getBaseConfig(); + } + + @Override + public void setStringList(String section, String subsection, String name, + List<String> values) { + if (exists() || !parent.exists()) { + super.setStringList(section, subsection, name, values); + } else { + parent.setStringList(section, subsection, name, values); + } + } + + @Override + public void unset(String section, String subsection, String name) { + if (exists() || !parent.exists()) { + super.unset(section, subsection, name); + } else { + parent.unset(section, subsection, name); + } + } + + @Override + public void unsetSection(String section, String subsection) { + if (exists() || !parent.exists()) { + super.unsetSection(section, subsection); + } else { + parent.unsetSection(section, subsection); + } + } + + @Override + public boolean removeSection(String section, String subsection) { + if (exists() || !parent.exists()) { + return super.removeSection(section, subsection); + } + return parent.removeSection(section, subsection); + } + + @Override + public boolean isOutdated() { + return super.isOutdated() || parent.isOutdated(); + } + + @Override + public void load() throws IOException, ConfigInvalidException { + if (super.isOutdated()) { + super.load(); + } + if (parent.isOutdated()) { + parent.load(); + } + } + + @Override + public void save() throws IOException { + if (exists() || !parent.exists()) { + if (exists() || !toText().strip().isEmpty()) { + super.save(); + } + } else { + parent.save(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheConfig.java index a12f652598..571248eac6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheConfig.java @@ -16,8 +16,9 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACKED_GIT_LIMIT; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACKED_GIT_MMAP; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACKED_GIT_OPENFILES; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACKED_GIT_WINDOWSIZE; -import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_FILE_TRESHOLD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_FILE_THRESHOLD; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACKED_GIT_USE_STRONGREFS; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACKED_INDEX_GIT_USE_STRONGREFS; import org.eclipse.jgit.internal.storage.file.WindowCache; import org.eclipse.jgit.lib.Config; @@ -39,6 +40,8 @@ public class WindowCacheConfig { private boolean useStrongRefs; + private boolean useStrongIndexRefs; + private int packedGitWindowSize; private boolean packedGitMMAP; @@ -56,6 +59,7 @@ public class WindowCacheConfig { packedGitOpenFiles = 128; packedGitLimit = 10 * MB; useStrongRefs = false; + useStrongIndexRefs = true; packedGitWindowSize = 8 * KB; packedGitMMAP = false; deltaBaseCacheLimit = 10 * MB; @@ -133,6 +137,31 @@ public class WindowCacheConfig { } /** + * Get whether the Pack indices cache should use strong references or + * SoftReferences + * + * @return {@code true} if the cached Pack indices should use strong references, + * otherwise it will use {@link java.lang.ref.SoftReference}s + * @since 6.7 + */ + public boolean isPackedIndexGitUseStrongRefs() { + return useStrongIndexRefs; + } + + /** + * Set if the Pack indices cache should use strong refs or soft refs + * + * @param useStrongRefs + * if @{code true} the Pack strongly references cached indices + * otherwise it uses {@link java.lang.ref.SoftReference}s which + * can be evicted by the Java gc if heap is almost full + * @since 6.7 + */ + public void setPackedIndexGitUseStrongRefs(boolean useStrongRefs) { + this.useStrongIndexRefs = useStrongRefs; + } + + /** * Get size in bytes of a single window mapped or read in from the pack * file. * @@ -270,6 +299,8 @@ public class WindowCacheConfig { setPackedGitUseStrongRefs(rc.getBoolean(CONFIG_CORE_SECTION, CONFIG_KEY_PACKED_GIT_USE_STRONGREFS, isPackedGitUseStrongRefs())); + setPackedIndexGitUseStrongRefs(rc.getBoolean(CONFIG_CORE_SECTION, + CONFIG_KEY_PACKED_INDEX_GIT_USE_STRONGREFS, isPackedIndexGitUseStrongRefs())); setPackedGitOpenFiles(rc.getInt(CONFIG_CORE_SECTION, null, CONFIG_KEY_PACKED_GIT_OPENFILES, getPackedGitOpenFiles())); setPackedGitLimit(rc.getLong(CONFIG_CORE_SECTION, null, @@ -283,7 +314,7 @@ public class WindowCacheConfig { long maxMem = Runtime.getRuntime().maxMemory(); long sft = rc.getLong(CONFIG_CORE_SECTION, null, - CONFIG_KEY_STREAM_FILE_TRESHOLD, getStreamFileThreshold()); + CONFIG_KEY_STREAM_FILE_THRESHOLD, getStreamFileThreshold()); sft = Math.min(sft, maxMem / 4); // don't use more than 1/4 of the heap sft = Math.min(sft, Integer.MAX_VALUE); // cannot exceed array length setStreamFileThreshold((int) sft); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheStats.java index ae329b686a..668b92cf53 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheStats.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheStats.java @@ -22,25 +22,10 @@ import org.eclipse.jgit.internal.storage.file.WindowCache; */ @MXBean public interface WindowCacheStats { - /** - * @return the number of open files. - * @deprecated use {@link #getOpenFileCount()} instead - */ - @Deprecated - public static int getOpenFiles() { - return (int) WindowCache.getInstance().getStats().getOpenFileCount(); - } - - /** - * @return the number of open bytes. - * @deprecated use {@link #getOpenByteCount()} instead - */ - @Deprecated - public static long getOpenBytes() { - return WindowCache.getInstance().getStats().getOpenByteCount(); - } /** + * Get cache statistics + * * @return cache statistics for the WindowCache * @since 5.1.13 */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java index a0c978f6ea..863b79466a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java @@ -16,6 +16,7 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BIGFILE_THRESHOLD; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BITMAP_CONTIGUOUS_COMMIT_COUNT; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BITMAP_DISTANT_COMMIT_SPAN; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BITMAP_EXCESSIVE_BRANCH_COUNT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BITMAP_EXCLUDED_REFS_PREFIXES; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BITMAP_INACTIVE_BRANCH_AGE_INDAYS; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BITMAP_RECENT_COMMIT_COUNT; @@ -27,7 +28,11 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_DELTA_CACHE_SIZE; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_DELTA_COMPRESSION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_DEPTH; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_INDEXVERSION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_SIZE_PREVENT_RACYPACK; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACK_KEPT_OBJECTS; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PRESERVE_OLD_PACKS; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PRUNE_PRESERVED; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REUSE_DELTAS; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REUSE_OBJECTS; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_SEARCH_FOR_REUSE_TIMEOUT; @@ -36,17 +41,16 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_THREADS; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WAIT_PREVENT_RACYPACK; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WINDOW; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WINDOW_MEMORY; -import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WRITE_REVERSE_INDEX; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION; -import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PRESERVE_OLD_PACKS; -import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PRUNE_PRESERVED; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_REPACK_SECTION; import java.time.Duration; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.zip.Deflater; -import org.eclipse.jgit.internal.storage.file.PackIndexWriter; +import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Repository; @@ -162,6 +166,14 @@ public class PackConfig { public static final int DEFAULT_INDEX_VERSION = 2; /** + * Default value of the write reverse index option: {@value} + * + * @see #setWriteReverseIndex(boolean) + * @since 6.6 + */ + public static final boolean DEFAULT_WRITE_REVERSE_INDEX = false; + + /** * Default value of the build bitmaps option: {@value} * * @see #setBuildBitmaps(boolean) @@ -169,6 +181,16 @@ public class PackConfig { */ public static final boolean DEFAULT_BUILD_BITMAPS = true; + + /** + * Default value for including objects in packs locked by .keep file when + * repacking: {@value} + * + * @see #setPackKeptObjects(boolean) + * @since 5.13.3 + */ + public static final boolean DEFAULT_PACK_KEPT_OBJECTS = false; + /** * Default count of most recent commits to select for bitmaps. Only applies * when bitmaps are enabled: {@value} @@ -219,6 +241,17 @@ public class PackConfig { public static final int DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT = 100; /** + * Default maxium count of branches to create tip bitmaps for. If the number + * of branches exceeds this, then tip bitmaps will only be created for the + * most recently active branches. Branches exceeding this count will receive + * 0 bitmaps: {@value #DEFAULT_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT} + * + * @see #setBitmapExcessiveBranchTipCount(int) + * @since 6.9 + */ + public static final int DEFAULT_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT = Integer.MAX_VALUE; + + /** * Default age at which a branch is considered inactive. Age is taken as the * number of days ago that the most recent commit was made to a branch. Only * affects bitmap processing if bitmaps are enabled and the @@ -247,8 +280,9 @@ public class PackConfig { public static final int DEFAULT_MIN_BYTES_FOR_OBJ_SIZE_INDEX = -1; /** - * Default max time to spend during the search for reuse phase. This - * optimization is disabled by default: {@value} + * Default max time to spend during the search for reuse phase. + * + * This optimization is disabled by default: {@link Integer#MAX_VALUE} seconds. * * @see #setSearchForReuseTimeout(Duration) * @since 5.13 @@ -292,8 +326,12 @@ public class PackConfig { private int indexVersion = DEFAULT_INDEX_VERSION; + private boolean writeReverseIndex = DEFAULT_WRITE_REVERSE_INDEX; + private boolean buildBitmaps = DEFAULT_BUILD_BITMAPS; + private boolean packKeptObjects = DEFAULT_PACK_KEPT_OBJECTS; + private int bitmapContiguousCommitCount = DEFAULT_BITMAP_CONTIGUOUS_COMMIT_COUNT; private int bitmapRecentCommitCount = DEFAULT_BITMAP_RECENT_COMMIT_COUNT; @@ -304,6 +342,8 @@ public class PackConfig { private int bitmapExcessiveBranchCount = DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT; + private int bitmapExcessiveBranchTipCount = DEFAULT_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT; + private int bitmapInactiveBranchAgeInDays = DEFAULT_BITMAP_INACTIVE_BRANCH_AGE_IN_DAYS; private String[] bitmapExcludedRefsPrefixes = DEFAULT_BITMAP_EXCLUDED_REFS_PREFIXES; @@ -373,7 +413,9 @@ public class PackConfig { this.threads = cfg.threads; this.executor = cfg.executor; this.indexVersion = cfg.indexVersion; + this.writeReverseIndex = cfg.writeReverseIndex; this.buildBitmaps = cfg.buildBitmaps; + this.packKeptObjects = cfg.packKeptObjects; this.bitmapContiguousCommitCount = cfg.bitmapContiguousCommitCount; this.bitmapRecentCommitCount = cfg.bitmapRecentCommitCount; this.bitmapRecentCommitSpan = cfg.bitmapRecentCommitSpan; @@ -953,7 +995,7 @@ public class PackConfig { * * @return the index version, the special version 0 designates the oldest * (most compatible) format available for the objects. - * @see PackIndexWriter + * @see BasePackIndexWriter */ public int getIndexVersion() { return indexVersion; @@ -967,13 +1009,38 @@ public class PackConfig { * @param version * the version to write. The special version 0 designates the * oldest (most compatible) format available for the objects. - * @see PackIndexWriter + * @see BasePackIndexWriter */ public void setIndexVersion(int version) { indexVersion = version; } /** + * True if the writer should write reverse index files. + * + * Default setting: {@value #DEFAULT_WRITE_REVERSE_INDEX} + * + * @return whether the writer should write reverse index files + * @since 6.6 + */ + public boolean isWriteReverseIndex() { + return writeReverseIndex; + } + + /** + * Set whether the writer will write reverse index files. + * + * Default setting: {@value #DEFAULT_WRITE_REVERSE_INDEX} + * + * @param writeReverseIndex + * whether the writer should write reverse index files + * @since 6.6 + */ + public void setWriteReverseIndex(boolean writeReverseIndex) { + this.writeReverseIndex = writeReverseIndex; + } + + /** * True if writer is allowed to build bitmaps for indexes. * * Default setting: {@value #DEFAULT_BUILD_BITMAPS} @@ -1003,6 +1070,34 @@ public class PackConfig { } /** + * Set whether to include objects in `.keep` files when repacking. + * + * <p> + * Default setting: {@value #DEFAULT_PACK_KEPT_OBJECTS} + * + * @param packKeptObjects + * boolean indicating whether to include objects in `.keep` files + * when repacking. + * @since 5.13.3 + */ + public void setPackKeptObjects(boolean packKeptObjects) { + this.packKeptObjects = packKeptObjects; + } + + /** + * True if objects in `.keep` files should be included when repacking. + * + * Default setting: {@value #DEFAULT_PACK_KEPT_OBJECTS} + * + * @return True if objects in `.keep` files should be included when + * repacking. + * @since 5.13.3 + */ + public boolean isPackKeptObjects() { + return packKeptObjects; + } + + /** * Get the count of most recent commits for which to build bitmaps. * * Default setting: {@value #DEFAULT_BITMAP_CONTIGUOUS_COMMIT_COUNT} @@ -1109,7 +1204,8 @@ public class PackConfig { * a repository exceeds this number and bitmaps are enabled, "inactive" * branches will have fewer bitmaps than "active" branches. * - * Default setting: {@value #DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT} + * Default setting: {@value #DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT}. See + * also {@link #getBitmapExcessiveBranchTipCount}. * * @return the count of branches deemed "excessive" * @since 4.2 @@ -1123,7 +1219,8 @@ public class PackConfig { * a repository exceeds this number and bitmaps are enabled, "inactive" * branches will have fewer bitmaps than "active" branches. * - * Default setting: {@value #DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT} + * Default setting: {@value #DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT}. See + * also {@link #setBitmapExcessiveBranchTipCount(int)}. * * @param count * the count of branches deemed "excessive" @@ -1134,6 +1231,57 @@ public class PackConfig { } /** + * Get the count of branches deemed "excessive". If the count of branches in + * a repository exceeds this number and bitmaps are enabled, branches + * exceeding this count will have no bitmaps selected. Branches are indexed + * most recent first. + * + * <li>The first {@code DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT} most active + * branches have full bitmap coverage. + * <li>The {@code DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT} to {@code + * DEFAULT_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT} most active branches have + * only the tip commit covered. + * <li>The remaining branches have no bitmap coverage. + * + * If {@link #getBitmapExcessiveBranchCount()} is greater, then that value + * will override this value. + * + * Default setting: {@value #DEFAULT_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT} + * + * @return the count of branch tips deemed "excessive" + * @since 6.9 + */ + public int getBitmapExcessiveBranchTipCount() { + return bitmapExcessiveBranchTipCount; + } + + /** + * Get the count of branches deemed "excessive". If the count of branches in + * a repository exceeds this number and bitmaps are enabled, branches + * exceeding this count will have no bitmaps selected. Branches are indexed + * most recent first. + * + * <li>The first {@code DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT} most active + * branches have full bitmap coverage. + * <li>The {@code DEFAULT_BITMAP_EXCESSIVE_BRANCH_COUNT} to {@code + * DEFAULT_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT} most active branches have + * only the tip commit covered. + * <li>The remaining branches have no bitmap coverage. + * + * If {@link #getBitmapExcessiveBranchCount()} is greater, then that value + * will override this value. + * + * Default setting: {@value #DEFAULT_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT} + * + * @param count + * the count of branch tips deemed "excessive" + * @since 6.9 + */ + public void setBitmapExcessiveBranchTipCount(int count) { + bitmapExcessiveBranchTipCount = count; + } + + /** * Get the age in days that marks a branch as "inactive". * * Default setting: {@value #DEFAULT_BITMAP_INACTIVE_BRANCH_AGE_IN_DAYS} @@ -1148,7 +1296,7 @@ public class PackConfig { /** * Get the max time to spend during the search for reuse phase. * - * Default setting: {@value #DEFAULT_SEARCH_FOR_REUSE_TIMEOUT} + * Default setting: {@link #DEFAULT_SEARCH_FOR_REUSE_TIMEOUT} * * @return the maximum time to spend during the search for reuse phase. * @since 5.13 @@ -1160,7 +1308,7 @@ public class PackConfig { /** * Set the age in days that marks a branch as "inactive". * - * Default setting: {@value #DEFAULT_BITMAP_INACTIVE_BRANCH_AGE_IN_DAYS} + * Default setting: {@link #DEFAULT_BITMAP_INACTIVE_BRANCH_AGE_IN_DAYS} * * @param ageInDays * the age in days that marks a branch as "inactive" @@ -1197,7 +1345,7 @@ public class PackConfig { * @param timeout * max time allowed during the search for reuse phase * - * Default setting: {@value #DEFAULT_SEARCH_FOR_REUSE_TIMEOUT} + * Default setting: {@link #DEFAULT_SEARCH_FOR_REUSE_TIMEOUT} * @since 5.13 */ public void setSearchForReuseTimeout(Duration timeout) { @@ -1286,8 +1434,14 @@ public class PackConfig { setSinglePack(rc.getBoolean(CONFIG_PACK_SECTION, CONFIG_KEY_SINGLE_PACK, getSinglePack())); - setBuildBitmaps(rc.getBoolean(CONFIG_PACK_SECTION, - CONFIG_KEY_BUILD_BITMAPS, isBuildBitmaps())); + setWriteReverseIndex(rc.getBoolean(CONFIG_PACK_SECTION, + CONFIG_KEY_WRITE_REVERSE_INDEX, isWriteReverseIndex())); + boolean buildBitmapsFromConfig = rc.getBoolean(CONFIG_PACK_SECTION, + CONFIG_KEY_BUILD_BITMAPS, isBuildBitmaps()); + setBuildBitmaps(buildBitmapsFromConfig); + setPackKeptObjects(rc.getBoolean(CONFIG_REPACK_SECTION, + CONFIG_KEY_PACK_KEPT_OBJECTS, + buildBitmapsFromConfig || isPackKeptObjects())); setBitmapContiguousCommitCount(rc.getInt(CONFIG_PACK_SECTION, CONFIG_KEY_BITMAP_CONTIGUOUS_COMMIT_COUNT, getBitmapContiguousCommitCount())); @@ -1303,6 +1457,9 @@ public class PackConfig { setBitmapExcessiveBranchCount(rc.getInt(CONFIG_PACK_SECTION, CONFIG_KEY_BITMAP_EXCESSIVE_BRANCH_COUNT, getBitmapExcessiveBranchCount())); + setBitmapExcessiveBranchTipCount(rc.getInt(CONFIG_PACK_SECTION, + CONFIG_KEY_BITMAP_EXCESSIVE_BRANCH_TIP_COUNT, + getBitmapExcessiveBranchTipCount())); setBitmapInactiveBranchAgeInDays(rc.getInt(CONFIG_PACK_SECTION, CONFIG_KEY_BITMAP_INACTIVE_BRANCH_AGE_INDAYS, getBitmapInactiveBranchAgeInDays())); @@ -1330,7 +1487,6 @@ public class PackConfig { CONFIG_KEY_PRUNE_PRESERVED, DEFAULT_PRUNE_PRESERVED)); } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder b = new StringBuilder(); @@ -1347,6 +1503,7 @@ public class PackConfig { b.append(", reuseDeltas=").append(isReuseDeltas()); //$NON-NLS-1$ b.append(", reuseObjects=").append(isReuseObjects()); //$NON-NLS-1$ b.append(", deltaCompress=").append(isDeltaCompress()); //$NON-NLS-1$ + b.append(", writeReverseIndex=").append(isWriteReverseIndex()); //$NON-NLS-1$ b.append(", buildBitmaps=").append(isBuildBitmaps()); //$NON-NLS-1$ b.append(", bitmapContiguousCommitCount=") //$NON-NLS-1$ .append(getBitmapContiguousCommitCount()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java index 4d18337fae..64a1eb2e1a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java @@ -111,6 +111,8 @@ public class PackStatistics { } /** + * Get total number of objects output + * * @return total number of objects output. This total includes the value * of {@link #getDeltas()}. */ @@ -119,6 +121,8 @@ public class PackStatistics { } /** + * Get total number of deltas output + * * @return total number of deltas output. This may be lower than the * actual number of deltas if a cached pack was reused. */ @@ -127,6 +131,9 @@ public class PackStatistics { } /** + * Get number of objects whose existing representation was reused in the + * output + * * @return number of objects whose existing representation was reused in * the output. This count includes {@link #getReusedDeltas()}. */ @@ -135,6 +142,9 @@ public class PackStatistics { } /** + * Get number of deltas whose existing representation was reused in the + * output + * * @return number of deltas whose existing representation was reused in * the output, as their base object was also output or was * assumed present for a thin pack. This may be lower than the @@ -145,6 +155,8 @@ public class PackStatistics { } /** + * Get total number of bytes written + * * @return total number of bytes written. This size includes the object * headers as well as the compressed data. This size also * includes all of {@link #getDeltaBytes()}. @@ -154,6 +166,8 @@ public class PackStatistics { } /** + * Get number of delta bytes written + * * @return number of delta bytes written. This size includes the object * headers for the delta objects. */ @@ -641,6 +655,8 @@ public class PackStatistics { } /** + * Get number of trees traversed + * * @return number of trees traversed in the walk when writing the pack. * @since 5.4 */ @@ -649,6 +665,8 @@ public class PackStatistics { } /** + * Get amount of packfiles offloaded + * * @return amount of packfiles offloaded (sent as "packfile-uri")/ * @since 5.6 */ @@ -657,6 +675,8 @@ public class PackStatistics { } /** + * Get total size (in bytes) offloaded + * * @return total size (in bytes) offloaded to HTTP downloads. * @since 5.6 */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleConflict.java b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleConflict.java index 856eb725dd..e324ab1afc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleConflict.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleConflict.java @@ -55,9 +55,11 @@ public class SubmoduleConflict extends Sequence { private final ObjectId objectId; /** - * Create a SubmoduleConflict for the given submodule object id - * @param objectId - */ + * Create a SubmoduleConflict for the given submodule object id + * + * @param objectId + * the id of the object to create a submodule conflict for + */ public SubmoduleConflict(ObjectId objectId) { super(); this.objectId = objectId; @@ -69,8 +71,10 @@ public class SubmoduleConflict extends Sequence { } /** - * @return the object id for the conflicting submodule - */ + * Get objectId for the conflicting submodule + * + * @return the object id for the conflicting submodule + */ public ObjectId getObjectId() { return objectId; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java index bf77021b0c..105cba7d28 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java @@ -87,6 +87,7 @@ public class SubmoduleWalk implements AutoCloseable { * @return generator over submodule index entries. The caller is responsible * for calling {@link #close()}. * @throws java.io.IOException + * if an IO error occurred */ public static SubmoduleWalk forIndex(Repository repository) throws IOException { @@ -116,6 +117,7 @@ public class SubmoduleWalk implements AutoCloseable { * @return generator at given path. The caller is responsible for calling * {@link #close()}. Null if no submodule at given path. * @throws java.io.IOException + * if an IO error occurred */ public static SubmoduleWalk forPath(Repository repository, AnyObjectId treeId, String path) throws IOException { @@ -150,6 +152,7 @@ public class SubmoduleWalk implements AutoCloseable { * @return generator at given path. The caller is responsible for calling * {@link #close()}. Null if no submodule at given path. * @throws java.io.IOException + * if an IO error occurred */ public static SubmoduleWalk forPath(Repository repository, AbstractTreeIterator iterator, String path) throws IOException { @@ -193,6 +196,7 @@ public class SubmoduleWalk implements AutoCloseable { * submodule path * @return repository or null if repository doesn't exist * @throws java.io.IOException + * if an IO error occurred */ public static Repository getSubmoduleRepository(final Repository parent, final String path) throws IOException { @@ -209,6 +213,7 @@ public class SubmoduleWalk implements AutoCloseable { * submodule path * @return repository or null if repository doesn't exist * @throws java.io.IOException + * if an IO error occurred */ public static Repository getSubmoduleRepository(final File parent, final String path) throws IOException { @@ -220,11 +225,14 @@ public class SubmoduleWalk implements AutoCloseable { * abstraction * * @param parent + * {@link Repository} that contains the submodule * @param path + * of the working tree of the submodule * @param fs * the file system abstraction to be used * @return repository or null if repository doesn't exist * @throws IOException + * if an IO error occurred * @since 4.10 */ public static Repository getSubmoduleRepository(final File parent, @@ -249,7 +257,7 @@ public class SubmoduleWalk implements AutoCloseable { * @return the {@link Repository} of the submodule, or {@code null} if it * doesn't exist * @throws IOException - * on errors + * if an IO error occurred * @since 5.6 */ public static Repository getSubmoduleRepository(File parent, String path, @@ -286,6 +294,7 @@ public class SubmoduleWalk implements AutoCloseable { * absolute or relative URL of the submodule repository * @return resolved URL * @throws java.io.IOException + * if an IO error occurred */ public static String getSubmoduleRemoteUrl(final Repository parent, final String url) throws IOException { @@ -369,6 +378,7 @@ public class SubmoduleWalk implements AutoCloseable { * @param repository * the {@link org.eclipse.jgit.lib.Repository}. * @throws java.io.IOException + * if an IO error occurred */ public SubmoduleWalk(Repository repository) throws IOException { this.repository = repository; @@ -419,13 +429,14 @@ public class SubmoduleWalk implements AutoCloseable { * The root tree is not read until the first submodule is encountered by the * walk. * <p> - * This method need only be called if constructing a walk manually instead of - * with one of the static factory methods above. + * This method need only be called if constructing a walk manually instead + * of with one of the static factory methods above. * * @param id * ID of a tree containing .gitmodules * @return this generator * @throws java.io.IOException + * if an IO error occurred */ public SubmoduleWalk setRootTree(AnyObjectId id) throws IOException { final CanonicalTreeParser p = new CanonicalTreeParser(); @@ -448,6 +459,7 @@ public class SubmoduleWalk implements AutoCloseable { * @throws java.io.IOException * if an error occurred, or if the repository is bare * @throws org.eclipse.jgit.errors.ConfigInvalidException + * if .gitmodules config is invalid */ public SubmoduleWalk loadModulesConfig() throws IOException, ConfigInvalidException { if (rootTree == null) { @@ -512,11 +524,13 @@ public class SubmoduleWalk implements AutoCloseable { * * @param repository * the repository to check - * @return <code>true</code> if the working tree contains a .gitmodules file, - * <code>false</code> otherwise. Always returns <code>false</code> - * for bare repositories. + * @return <code>true</code> if the working tree contains a .gitmodules + * file, <code>false</code> otherwise. Always returns + * <code>false</code> for bare repositories. * @throws java.io.IOException - * @throws CorruptObjectException if any. + * if an IO error occurred + * @throws CorruptObjectException + * if a corrupt object was found * @since 3.6 */ public static boolean containsGitModulesFile(Repository repository) @@ -526,7 +540,7 @@ public class SubmoduleWalk implements AutoCloseable { } File modulesFile = new File(repository.getWorkTree(), Constants.DOT_GIT_MODULES); - return (modulesFile.exists()); + return modulesFile.exists(); } private void lazyLoadModulesConfig() throws IOException, ConfigInvalidException { @@ -560,6 +574,7 @@ public class SubmoduleWalk implements AutoCloseable { * object. * @return this generator * @throws org.eclipse.jgit.errors.CorruptObjectException + * if a corrupt object was found */ public SubmoduleWalk setTree(AbstractTreeIterator iterator) throws CorruptObjectException { @@ -574,10 +589,11 @@ public class SubmoduleWalk implements AutoCloseable { * an {@link org.eclipse.jgit.lib.AnyObjectId} object. * @return this generator * @throws java.io.IOException + * if an IO error occurred * @throws IncorrectObjectTypeException - * if any. + * if object has unexpected type * @throws MissingObjectException - * if any. + * if object is missing */ public SubmoduleWalk setTree(AnyObjectId treeId) throws IOException { walk.addTree(treeId); @@ -614,6 +630,7 @@ public class SubmoduleWalk implements AutoCloseable { * * @return true if entry found, false otherwise * @throws java.io.IOException + * if an IO error occurred */ public boolean next() throws IOException { while (walk.next()) { @@ -656,9 +673,11 @@ public class SubmoduleWalk implements AutoCloseable { * name of .git/config) * * @since 4.10 - * @return name + * @return name of the submodule * @throws ConfigInvalidException + * if the .gitmodules config is invalid * @throws IOException + * if an IO error occurred */ public String getModuleName() throws IOException, ConfigInvalidException { lazyLoadModulesConfig(); @@ -680,7 +699,9 @@ public class SubmoduleWalk implements AutoCloseable { * * @return configured path * @throws org.eclipse.jgit.errors.ConfigInvalidException + * if .gitmodules config is invalid * @throws java.io.IOException + * if an IO error occurred */ public String getModulesPath() throws IOException, ConfigInvalidException { lazyLoadModulesConfig(); @@ -694,7 +715,9 @@ public class SubmoduleWalk implements AutoCloseable { * * @return configured URL * @throws org.eclipse.jgit.errors.ConfigInvalidException + * if .gitmodules config is invalid * @throws java.io.IOException + * if an IO error occurred */ public String getConfigUrl() throws IOException, ConfigInvalidException { return repoConfig.getString(ConfigConstants.CONFIG_SUBMODULE_SECTION, @@ -707,7 +730,9 @@ public class SubmoduleWalk implements AutoCloseable { * * @return configured URL * @throws org.eclipse.jgit.errors.ConfigInvalidException + * if .gitmodules config is invalid * @throws java.io.IOException + * if an IO error occurred */ public String getModulesUrl() throws IOException, ConfigInvalidException { lazyLoadModulesConfig(); @@ -721,7 +746,9 @@ public class SubmoduleWalk implements AutoCloseable { * * @return update value * @throws org.eclipse.jgit.errors.ConfigInvalidException + * if .gitmodules config is invalid * @throws java.io.IOException + * if an IO error occurred */ public String getConfigUpdate() throws IOException, ConfigInvalidException { return repoConfig.getString(ConfigConstants.CONFIG_SUBMODULE_SECTION, @@ -734,7 +761,9 @@ public class SubmoduleWalk implements AutoCloseable { * * @return update value * @throws org.eclipse.jgit.errors.ConfigInvalidException + * if .gitmodules config is invalid * @throws java.io.IOException + * if an IO error occurred */ public String getModulesUpdate() throws IOException, ConfigInvalidException { lazyLoadModulesConfig(); @@ -748,7 +777,9 @@ public class SubmoduleWalk implements AutoCloseable { * * @return ignore value * @throws org.eclipse.jgit.errors.ConfigInvalidException + * if .gitmodules config is invalid * @throws java.io.IOException + * if an IO error occurred * @since 3.6 */ public IgnoreSubmoduleMode getModulesIgnore() throws IOException, @@ -756,14 +787,14 @@ public class SubmoduleWalk implements AutoCloseable { IgnoreSubmoduleMode mode = repoConfig.getEnum( IgnoreSubmoduleMode.values(), ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(), - ConfigConstants.CONFIG_KEY_IGNORE, null); + ConfigConstants.CONFIG_KEY_IGNORE); if (mode != null) { return mode; } lazyLoadModulesConfig(); - return modulesConfig.getEnum(IgnoreSubmoduleMode.values(), - ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(), - ConfigConstants.CONFIG_KEY_IGNORE, IgnoreSubmoduleMode.NONE); + return modulesConfig.getEnum(ConfigConstants.CONFIG_SUBMODULE_SECTION, + getModuleName(), ConfigConstants.CONFIG_KEY_IGNORE, + IgnoreSubmoduleMode.NONE); } /** @@ -771,6 +802,7 @@ public class SubmoduleWalk implements AutoCloseable { * * @return repository or null if non-existent * @throws java.io.IOException + * if an IO error occurred */ public Repository getRepository() throws IOException { return getSubmoduleRepository(repository.getWorkTree(), path, @@ -782,6 +814,7 @@ public class SubmoduleWalk implements AutoCloseable { * * @return object id of HEAD reference * @throws java.io.IOException + * if an IO error occurred */ public ObjectId getHead() throws IOException { try (Repository subRepo = getRepository()) { @@ -797,6 +830,7 @@ public class SubmoduleWalk implements AutoCloseable { * * @return ref name, null on failures * @throws java.io.IOException + * if an IO error occurred */ public String getHeadRef() throws IOException { try (Repository subRepo = getRepository()) { @@ -816,7 +850,9 @@ public class SubmoduleWalk implements AutoCloseable { * * @return resolved remote URL * @throws java.io.IOException + * if an IO error occurred * @throws org.eclipse.jgit.errors.ConfigInvalidException + * if .gitmodules config is invalid */ public String getRemoteUrl() throws IOException, ConfigInvalidException { String url = getModulesUrl(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AbstractAdvertiseRefsHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AbstractAdvertiseRefsHook.java index fb9c14576a..260a85e0e4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AbstractAdvertiseRefsHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AbstractAdvertiseRefsHook.java @@ -58,7 +58,6 @@ import org.eclipse.jgit.revwalk.RevWalk; * @since 2.0 */ public abstract class AbstractAdvertiseRefsHook implements AdvertiseRefsHook { - /** {@inheritDoc} */ @Override public void advertiseRefs(UploadPack uploadPack) throws ServiceMayNotContinueException { @@ -67,7 +66,6 @@ public abstract class AbstractAdvertiseRefsHook implements AdvertiseRefsHook { } /** - * {@inheritDoc} */ @Override public void advertiseRefs(ReceivePack receivePack) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java index 84c36915a2..6381f1e946 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java @@ -92,6 +92,7 @@ public interface AdvertiseRefsHook { * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException * abort; the message will be sent to the user. * @throws IOException + * if an IO error occurred * @since 5.6 */ void advertiseRefs(ReceivePack receivePack) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHookChain.java index eb9c673ef3..6db0775769 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHookChain.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHookChain.java @@ -50,9 +50,6 @@ public class AdvertiseRefsHookChain implements AdvertiseRefsHook { } } - /** - * {@inheritDoc} - */ @Override public void advertiseRefs(ReceivePack rp) throws IOException { @@ -60,7 +57,6 @@ public class AdvertiseRefsHookChain implements AdvertiseRefsHook { hooks[i].advertiseRefs(rp); } - /** {@inheritDoc} */ @Override public void advertiseRefs(UploadPack rp) throws ServiceMayNotContinueException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java index d482521747..9d9f5495fe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java @@ -596,7 +596,7 @@ public class AmazonS3 { final String key, final Map<String, String> args) throws IOException { final StringBuilder urlstr = new StringBuilder(); - urlstr.append(protocol); //$NON-NLS-1$ + urlstr.append(protocol); urlstr.append("://"); //$NON-NLS-1$ urlstr.append(bucket); urlstr.append('.'); @@ -757,8 +757,19 @@ public class AmazonS3 { final XMLReader xr; try { - xr = SAXParserFactory.newInstance().newSAXParser() - .getXMLReader(); + SAXParserFactory saxParserFactory = SAXParserFactory + .newInstance(); + saxParserFactory.setNamespaceAware(true); + saxParserFactory.setFeature( + "http://xml.org/sax/features/external-general-entities", //$NON-NLS-1$ + false); + saxParserFactory.setFeature( + "http://xml.org/sax/features/external-parameter-entities", //$NON-NLS-1$ + false); + saxParserFactory.setFeature( + "http://apache.org/xml/features/disallow-doctype-decl", //$NON-NLS-1$ + true); + xr = saxParserFactory.newSAXParser().getXMLReader(); } catch (SAXException | ParserConfigurationException e) { throw new IOException( JGitText.get().noXMLParserAvailable, e); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java index ab3013762b..cf4420fe58 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.transport; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -145,7 +147,7 @@ public final class AwsRequestSignerV4 { canonicalRequest.getBytes(StandardCharsets.UTF_8))); // compute the signing key - byte[] secretKey = (SCHEME + new String(awsSecretKey)).getBytes(); + byte[] secretKey = (SCHEME + new String(awsSecretKey)).getBytes(UTF_8); byte[] dateKey = signStringWithKey(scopeDate, secretKey); byte[] regionKey = signStringWithKey(regionName, dateKey); byte[] serviceKey = signStringWithKey(serviceName, regionKey); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java index 9e229a18ad..3ac9f59ba6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java @@ -38,25 +38,21 @@ public abstract class BaseConnection implements Connection { private Writer messageWriter; - /** {@inheritDoc} */ @Override public Map<String, Ref> getRefsMap() { return advertisedRefs; } - /** {@inheritDoc} */ @Override public final Collection<Ref> getRefs() { return advertisedRefs.values(); } - /** {@inheritDoc} */ @Override public final Ref getRef(String name) { return advertisedRefs.get(name); } - /** {@inheritDoc} */ @Override public String getMessages() { return messageWriter != null ? messageWriter.toString() : ""; //$NON-NLS-1$ @@ -84,7 +80,6 @@ public abstract class BaseConnection implements Connection { peerUserAgent = agent; } - /** {@inheritDoc} */ @Override public abstract void close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseFetchConnection.java index 6954bd9a0e..48992f3e3b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseFetchConnection.java @@ -31,7 +31,6 @@ import org.eclipse.jgit.lib.Ref; */ abstract class BaseFetchConnection extends BaseConnection implements FetchConnection { - /** {@inheritDoc} */ @Override public final void fetch(final ProgressMonitor monitor, final Collection<Ref> want, final Set<ObjectId> have) @@ -39,7 +38,6 @@ abstract class BaseFetchConnection extends BaseConnection implements fetch(monitor, want, have, null); } - /** {@inheritDoc} */ @Override public final void fetch(final ProgressMonitor monitor, final Collection<Ref> want, final Set<ObjectId> have, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java index 09c559d7b5..bac5025f83 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java @@ -64,6 +64,7 @@ import org.eclipse.jgit.util.io.TimeoutOutputStream; */ abstract class BasePackConnection extends BaseConnection { + /** The capability prefix for a symlink */ protected static final String CAPABILITY_SYMREF_PREFIX = "symref="; //$NON-NLS-1$ /** The repository this transport fetches into, or pushes out of. */ @@ -72,7 +73,7 @@ abstract class BasePackConnection extends BaseConnection { /** Remote repository location. */ protected final URIish uri; - /** A transport connected to {@link #uri}. */ + /** A transport connected to {@link BasePackConnection#uri}. */ protected final Transport transport; /** Low-level input stream, if a timeout was configured. */ @@ -81,7 +82,10 @@ abstract class BasePackConnection extends BaseConnection { /** Low-level output stream, if a timeout was configured. */ protected TimeoutOutputStream timeoutOut; - /** Timer to manage {@link #timeoutIn} and {@link #timeoutOut}. */ + /** + * Timer to manage {@link #timeoutIn} and + * {@link BasePackConnection#timeoutOut}. + */ private InterruptTimer myTimer; /** Input stream reading from the remote. */ @@ -90,13 +94,16 @@ abstract class BasePackConnection extends BaseConnection { /** Output stream sending to the remote. */ protected OutputStream out; - /** Packet line decoder around {@link #in}. */ + /** Packet line decoder around {@link BasePackConnection#in}. */ protected PacketLineIn pckIn; - /** Packet line encoder around {@link #out}. */ + /** Packet line encoder around {@link BasePackConnection#out}. */ protected PacketLineOut pckOut; - /** Send {@link PacketLineOut#end()} before closing {@link #out}? */ + /** + * Send {@link PacketLineOut#end()} before closing + * {@link BasePackConnection#out}? + */ protected boolean outNeedsEnd; /** True if this is a stateless RPC connection. */ @@ -486,7 +493,6 @@ abstract class BasePackConnection extends BaseConnection { * <p> * If refMap already contains an entry for symRef.key, it is replaced. * </p> - * </p> * <p> * For example, given: * </p> @@ -627,7 +633,6 @@ abstract class BasePackConnection extends BaseConnection { } } - /** {@inheritDoc} */ @Override public String getPeerUserAgent() { String agent = remoteCapabilities.get(OPTION_AGENT); @@ -642,7 +647,6 @@ abstract class BasePackConnection extends BaseConnection { return new PackProtocolException(uri, MessageFormat.format(JGitText.get().invalidRefAdvertisementLine, line)); } - /** {@inheritDoc} */ @Override public void close() { if (out != 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 index 8909380176..be0d37b96e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java @@ -12,10 +12,10 @@ package org.eclipse.jgit.transport; -import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DELIM; import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN; import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_NOT; import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_SINCE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DELIM; import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DONE; import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_END; import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ERR; @@ -32,10 +32,11 @@ import java.time.Instant; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -334,7 +335,6 @@ public abstract class BasePackFetchConnection extends BasePackConnection } } - /** {@inheritDoc} */ @Override public final void fetch(final ProgressMonitor monitor, final Collection<Ref> want, final Set<ObjectId> have) @@ -342,7 +342,6 @@ public abstract class BasePackFetchConnection extends BasePackConnection fetch(monitor, want, have, null); } - /** {@inheritDoc} */ @Override public final void fetch(final ProgressMonitor monitor, final Collection<Ref> want, final Set<ObjectId> have, @@ -351,25 +350,21 @@ public abstract class BasePackFetchConnection extends BasePackConnection doFetch(monitor, want, have, outputStream); } - /** {@inheritDoc} */ @Override public boolean didFetchIncludeTags() { return false; } - /** {@inheritDoc} */ @Override public boolean didFetchTestConnectivity() { return false; } - /** {@inheritDoc} */ @Override public void setPackLockMessage(String message) { lockMessage = message; } - /** {@inheritDoc} */ @Override public Collection<PackLock> getPackLocks() { if (packLock != null) @@ -406,11 +401,14 @@ public abstract class BasePackFetchConnection extends BasePackConnection protected void doFetch(final ProgressMonitor monitor, final Collection<Ref> want, final Set<ObjectId> have, OutputStream outputStream) throws TransportException { + boolean hasObjects = !have.isEmpty(); try { noProgress = monitor == NullProgressMonitor.INSTANCE; - markRefsAdvertised(); - markReachable(want, have, maxTimeWanted(want)); + if (hasObjects) { + markRefsAdvertised(); + } + markReachable(want, have, maxTimeWanted(want, hasObjects)); if (TransferConfig.ProtocolVersion.V2 .equals(getProtocolVersion())) { @@ -422,7 +420,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection state = new TemporaryBuffer.Heap(Integer.MAX_VALUE); pckState = new PacketLineOut(state); try { - doFetchV2(monitor, want, outputStream); + doFetchV2(monitor, want, outputStream, hasObjects); } finally { clearState(); } @@ -434,7 +432,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection pckState = new PacketLineOut(state); } PacketLineOut output = statelessRPC ? pckState : pckOut; - if (sendWants(want, output)) { + if (sendWants(want, output, hasObjects)) { boolean mayHaveShallow = depth != null || deepenSince != null || !deepenNots.isEmpty(); Set<ObjectId> shallowCommits = local.getObjectDatabase().getShallowCommits(); if (isCapableOf(GitProtocolConstants.CAPABILITY_SHALLOW)) { @@ -461,7 +459,8 @@ public abstract class BasePackFetchConnection extends BasePackConnection } private void doFetchV2(ProgressMonitor monitor, Collection<Ref> want, - OutputStream outputStream) throws IOException, CancelledException { + OutputStream outputStream, boolean hasObjects) + throws IOException, CancelledException { sideband = true; negotiateBegin(); @@ -483,7 +482,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection pckState.writeString(capability); } - if (!sendWants(want, pckState)) { + if (!sendWants(want, pckState, hasObjects)) { // We already have everything we wanted. return; } @@ -659,7 +658,6 @@ public abstract class BasePackFetchConnection extends BasePackConnection return gotReady; } - /** {@inheritDoc} */ @Override public void close() { if (walk != null) @@ -671,8 +669,12 @@ public abstract class BasePackFetchConnection extends BasePackConnection return local.getConfig().get(FetchConfig::new); } - private int maxTimeWanted(Collection<Ref> wants) { + private int maxTimeWanted(Collection<Ref> wants, boolean hasObjects) { int maxTime = 0; + if (!hasObjects) { + // we don't have any objects locally, we can immediately bail out + return maxTime; + } for (Ref r : wants) { try { final RevObject obj = walk.parseAny(r.getObjectId()); @@ -689,36 +691,30 @@ public abstract class BasePackFetchConnection extends BasePackConnection } private void markReachable(Collection<Ref> want, Set<ObjectId> have, - int maxTime) - throws IOException { - Set<String> wantRefs = want.stream().map(Ref::getName) - .collect(Collectors.toSet()); - - for (Ref r : local.getRefDatabase().getRefs()) { - if (useNegotiationTip && !wantRefs.contains(r.getName())) { - continue; + int maxTime) throws IOException { + Collection<Ref> refsToMark; + if (useNegotiationTip) { + refsToMark = translateToLocalTips(want); + if (refsToMark.size() < want.size()) { + refsToMark.addAll(local.getRefDatabase().getRefs()); } - - ObjectId id = r.getPeeledObjectId(); - if (id == null) - id = r.getObjectId(); - if (id == null) - continue; - parseReachable(id); + } else { + refsToMark = local.getRefDatabase().getRefs(); } + markReachableRefTips(refsToMark); for (ObjectId id : local.getAdditionalHaves()) - parseReachable(id); + markReachable(id); for (ObjectId id : have) - parseReachable(id); + markReachable(id); 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); + Instant maxWhen = Instant.ofEpochSecond(maxTime); walk.sort(RevSort.COMMIT_TIME_DESC); walk.markStart(reachableCommits); walk.setRevFilter(CommitTimeRevFilter.after(maxWhen)); @@ -738,7 +734,37 @@ public abstract class BasePackFetchConnection extends BasePackConnection } } - private void parseReachable(ObjectId id) { + private Collection<Ref> translateToLocalTips(Collection<Ref> want) + throws IOException { + String[] refs = want.stream().map(Ref::getName) + .collect(Collectors.toSet()).toArray(String[]::new); + Map<String, Ref> wantRefMap = local.getRefDatabase().exactRef(refs); + return wantRefMap.values().stream() + .filter(r -> getRefObjectId(r) != null) + .collect(Collectors.toList()); + } + + /** + * Marks commits reachable. + * + * @param refsToMark + * references that client is requesting to be marked. + */ + private void markReachableRefTips(Collection<Ref> refsToMark) { + refsToMark.stream().map(BasePackFetchConnection::getRefObjectId) + .filter(Objects::nonNull) + .forEach(oid -> markReachable(oid)); + } + + private static ObjectId getRefObjectId(Ref ref) { + ObjectId id = ref.getPeeledObjectId(); + if (id == null) { + id = ref.getObjectId(); + } + return id; + } + + private void markReachable(ObjectId id) { try { RevCommit o = walk.parseCommit(id); if (!o.has(REACHABLE)) { @@ -750,7 +776,8 @@ public abstract class BasePackFetchConnection extends BasePackConnection } } - private boolean sendWants(Collection<Ref> want, PacketLineOut p) + private boolean sendWants(Collection<Ref> want, PacketLineOut p, + boolean hasObjects) throws IOException { boolean first = true; for (Ref r : want) { @@ -759,7 +786,9 @@ public abstract class BasePackFetchConnection extends BasePackConnection continue; } // if depth is set we need to fetch the objects even if they are already available - if (transport.getDepth() == null) { + if (transport.getDepth() == null + // only check reachable objects when we have objects + && hasObjects) { try { if (walk.parseAny(objectId).has(REACHABLE)) { // We already have this object. Asking for it is @@ -845,7 +874,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection if (statelessRPC && multiAck != MultiAck.DETAILED) { // Our stateless RPC implementation relies upon the detailed // ACK status to tell us common objects for reuse in future - // requests. If its not enabled, we can't talk to the peer. + // requests. If its not enabled, we can't talk to the peer. // throw new PackProtocolException(uri, MessageFormat.format( JGitText.get().statelessRPCRequiresOptionToBeEnabled, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java index adc1c9849d..9a96fdcf37 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java @@ -122,7 +122,6 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen useBitmaps = transport.isPushUseBitmaps(); } - /** {@inheritDoc} */ @Override public void push(final ProgressMonitor monitor, final Map<String, RemoteRefUpdate> refUpdates) @@ -130,7 +129,6 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen push(monitor, refUpdates, null); } - /** {@inheritDoc} */ @Override public void push(final ProgressMonitor monitor, final Map<String, RemoteRefUpdate> refUpdates, OutputStream outputStream) @@ -139,7 +137,6 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen doPush(monitor, refUpdates, outputStream); } - /** {@inheritDoc} */ @Override protected TransportException noRepository(Throwable cause) { // Sadly we cannot tell the "invalid URI" case from "push not allowed". diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java index f04e573feb..5299dfa981 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java @@ -147,13 +147,11 @@ class BundleFetchConnection extends BaseFetchConnection { return line.toString(); } - /** {@inheritDoc} */ @Override public boolean didFetchTestConnectivity() { return false; } - /** {@inheritDoc} */ @Override protected void doFetch(final ProgressMonitor monitor, final Collection<Ref> want, final Set<ObjectId> have) @@ -174,13 +172,11 @@ class BundleFetchConnection extends BaseFetchConnection { } } - /** {@inheritDoc} */ @Override public void setPackLockMessage(String message) { lockMessage = message; } - /** {@inheritDoc} */ @Override public Collection<PackLock> getPackLocks() { if (packLock != null) @@ -258,7 +254,6 @@ class BundleFetchConnection extends BaseFetchConnection { } } - /** {@inheritDoc} */ @Override public void close() { if (bin != null) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CapabilitiesV2Request.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CapabilitiesV2Request.java index 9c5a54e9cb..27afdc29be 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CapabilitiesV2Request.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CapabilitiesV2Request.java @@ -21,7 +21,11 @@ public final class CapabilitiesV2Request { private CapabilitiesV2Request() { } - /** @return A builder of {@link CapabilitiesV2Request}. */ + /** + * Create builder + * + * @return A builder of {@link CapabilitiesV2Request}. + */ public static Builder builder() { return new Builder(); } @@ -31,7 +35,11 @@ public final class CapabilitiesV2Request { private Builder() { } - /** @return CapabilitiesV2Request */ + /** + * Build the request + * + * @return CapabilitiesV2Request + */ public CapabilitiesV2Request build() { return new CapabilitiesV2Request(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java index 6a4cfca865..3bae0eae5a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java @@ -39,7 +39,6 @@ public class ChainingCredentialsProvider extends CredentialsProvider { Arrays.asList(providers)); } - /** {@inheritDoc} */ @Override public boolean isInteractive() { for (CredentialsProvider p : credentialProviders) @@ -48,7 +47,6 @@ public class ChainingCredentialsProvider extends CredentialsProvider { return false; } - /** {@inheritDoc} */ @Override public boolean supports(CredentialItem... items) { for (CredentialsProvider p : credentialProviders) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ConnectivityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ConnectivityChecker.java index dcffe48be8..cbb3420db2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ConnectivityChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ConnectivityChecker.java @@ -60,6 +60,8 @@ public interface ConnectivityChecker { private RevWalk walk; /** + * Get database we write the stored objects into + * * @return database we write the stored objects into. */ public Repository getRepository() { @@ -67,6 +69,8 @@ public interface ConnectivityChecker { } /** + * Set database we write the stored objects into + * * @param repository * set database we write the stored objects into. */ @@ -75,6 +79,8 @@ public interface ConnectivityChecker { } /** + * Get the parser used to parse pack + * * @return the parser used to parse pack. */ public PackParser getParser() { @@ -82,6 +88,8 @@ public interface ConnectivityChecker { } /** + * Set the parser + * * @param parser * the parser to set */ @@ -90,6 +98,8 @@ public interface ConnectivityChecker { } /** + * Whether checker should check objects + * * @return if checker should check objects. */ public boolean isCheckObjects() { @@ -97,6 +107,8 @@ public interface ConnectivityChecker { } /** + * Set whether objects should be checked + * * @param checkObjects * set if checker should check referenced objects outside of * the received pack are reachable. @@ -106,21 +118,27 @@ public interface ConnectivityChecker { } /** - * @return command received by the current request. + * Get commands received by the current request + * + * @return commands received by the current request. */ public List<ReceiveCommand> getCommands() { return commands; } /** + * Set commands received by the current request + * * @param commands - * set command received by the current request. + * commands received by the current request. */ public void setCommands(List<ReceiveCommand> commands) { this.commands = commands; } /** + * Set the walk to parse commits + * * @param walk * the walk to parse commits */ @@ -129,6 +147,8 @@ public interface ConnectivityChecker { } /** + * Get the walk to parse commits + * * @return the walk to parse commits */ public RevWalk getWalk() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialItem.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialItem.java index 2b09424526..32852890c0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialItem.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialItem.java @@ -109,14 +109,20 @@ public abstract class CredentialItem { value = null; } - /** @return the current value */ + /** + * Get value + * + * @return the current value + */ public String getValue() { return value; } /** + * Set value * * @param newValue + * the new value */ public void setValue(String newValue) { value = newValue; @@ -214,7 +220,11 @@ public abstract class CredentialItem { value = false; } - /** @return the current value */ + /** + * Get value + * + * @return the current value + */ public boolean getValue() { return value; } @@ -223,6 +233,7 @@ public abstract class CredentialItem { * Set the new value. * * @param newValue + * the new value */ public void setValue(boolean newValue) { value = newValue; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java index f02160e457..c510194ee6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java @@ -111,7 +111,7 @@ class FetchProcess { for (PackLock lock : packLocks) { lock.unlock(); } - } catch (IOException e) { + } catch (Throwable e) { if (e1 != null) { e.addSuppressed(e1); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java index 009a70b7b3..91eed96d4e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java @@ -85,6 +85,8 @@ abstract class FetchRequest { } /** + * Get object ids in the "want" lines + * * @return object ids in the "want" (and "want-ref") lines of the request */ @NonNull @@ -93,6 +95,8 @@ abstract class FetchRequest { } /** + * Get the depth set in a "deepen" line + * * @return the depth set in a "deepen" line. 0 by default. */ int getDepth() { @@ -112,6 +116,8 @@ abstract class FetchRequest { } /** + * Get the filter spec given in a "filter" line + * * @return the filter spec given in a "filter" line */ @NonNull @@ -152,6 +158,8 @@ abstract class FetchRequest { } /** + * Get refs received in "deepen-not" lines + * * @return refs received in "deepen-not" lines. */ @NonNull @@ -160,6 +168,8 @@ abstract class FetchRequest { } /** + * Get string identifying the agent + * * @return string identifying the agent (as sent in the request body by the * client) */ @@ -169,8 +179,10 @@ abstract class FetchRequest { } /** - * @return string identifying the client session ID (as sent in the request body by the - * client) + * Get string identifying the client session ID + * + * @return string identifying the client session ID (as sent in the request + * body by the client) */ @Nullable String getClientSID() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java index ca3639d03c..53b3e19263 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java @@ -58,6 +58,8 @@ final class FetchV0Request extends FetchRequest { String clientSID; /** + * Add wantId + * * @param objectId * object id received in a "want" line * @return this builder @@ -68,6 +70,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Set depth + * * @param d * depth set in a "deepen" line * @return this builder @@ -78,6 +82,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Get depth + * * @return depth set in the request (via a "deepen" line). Defaulting to * 0 if not set. */ @@ -86,6 +92,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Whether there's at least one "deepen not" line + * * @return true if there has been at least one "deepen not" line in the * request so far */ @@ -94,6 +102,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Add "deepen not" + * * @param deepenNot * reference received in a "deepen not" line * @return this builder @@ -104,6 +114,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Set "deepen since" + * * @param value * Unix timestamp received in a "deepen since" line * @return this builder @@ -114,6 +126,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Get "deepen since + * * @return shallow since value, sent before in a "deepen since" line. 0 * by default. */ @@ -122,6 +136,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Add client shallow commit + * * @param shallowOid * object id received in a "shallow" line * @return this builder @@ -132,6 +148,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Add client capabilities + * * @param clientCapabilities * client capabilities sent by the client in the first want * line of the request @@ -143,6 +161,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Set agent + * * @param clientAgent * agent line sent by the client in the request body * @return this builder @@ -153,6 +173,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Set client session id + * * @param clientSID * session-id line sent by the client in the request body * @return this builder @@ -163,6 +185,8 @@ final class FetchV0Request extends FetchRequest { } /** + * Set filter spec + * * @param filter * the filter set in a filter line * @return this builder diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java index 3d4f38131c..21284db2c1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java @@ -70,6 +70,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Get object ids received in the "have" lines + * * @return object ids received in the "have" lines */ @NonNull @@ -78,6 +80,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Get list of references received in "want-ref" lines + * * @return list of references received in "want-ref" lines * * @since 5.4 @@ -88,6 +92,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Whether the request had a "done" line + * * @return true if the request had a "done" line */ boolean wasDoneReceived() { @@ -95,6 +101,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Whether the request had a "wait-for-done" line + * * @return true if the request had a "wait-for-done" line */ boolean wasWaitForDoneReceived() { @@ -115,6 +123,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Whether "sideband-all" was received + * * @return true if "sideband-all" was received */ boolean getSidebandAll() { @@ -126,7 +136,11 @@ public final class FetchV2Request extends FetchRequest { return packfileUriProtocols; } - /** @return A builder of {@link FetchV2Request}. */ + /** + * Get builder + * + * @return A builder of {@link FetchV2Request}. + */ static Builder builder() { return new Builder(); } @@ -171,6 +185,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Add object the peer has + * * @param objectId * object id received in a "have" line * @return this builder @@ -181,7 +197,7 @@ public final class FetchV2Request extends FetchRequest { } /** - * Ref received in "want-ref" line and the object-id it refers to + * Add Ref received in "want-ref" line and the object-id it refers to * * @param refName * reference name @@ -193,6 +209,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Add client capability + * * @param clientCapability * capability line sent by the client * @return this builder @@ -203,6 +221,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Add object received in "want" line + * * @param wantId * object id received in a "want" line * @return this builder @@ -213,6 +233,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Add Object received in a "shallow" line + * * @param shallowOid * object id received in a "shallow" line * @return this builder @@ -223,6 +245,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Set depth received in "deepen" line + * * @param d * Depth received in a "deepen" line * @return this builder @@ -233,6 +257,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Get depth set in request + * * @return depth set in the request (via a "deepen" line). Defaulting to * 0 if not set. */ @@ -241,6 +267,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Whether there has been at least one ""deepen not" line + * * @return true if there has been at least one "deepen not" line in the * request so far */ @@ -249,6 +277,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Add reference received in a "deepen not" line + * * @param deepenNot * reference received in a "deepen not" line * @return this builder @@ -259,6 +289,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Set Unix timestamp received in a "deepen since" line + * * @param value * Unix timestamp received in a "deepen since" line * @return this builder @@ -269,6 +301,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Get shallow since value + * * @return shallow since value, sent before in a "deepen since" line. 0 * by default. */ @@ -277,6 +311,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Set filter spec + * * @param filter * spec set in a "filter" line * @return this builder @@ -321,6 +357,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Set value of client-supplied session capability + * * @param clientSIDValue * the client-supplied session capability, without the * leading "session-id=" @@ -347,7 +385,10 @@ public final class FetchV2Request extends FetchRequest { } /** - * @param value true if client sent "sideband-all" + * Set whether client sent "sideband-all + * + * @param value + * true if client sent "sideband-all" * @return this builder */ Builder setSidebandAll(boolean value) { @@ -361,6 +402,8 @@ public final class FetchV2Request extends FetchRequest { } /** + * Build initialized fetch request + * * @return Initialized fetch request */ FetchV2Request build() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FilterSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FilterSpec.java index a8cf849fed..442b963440 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FilterSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FilterSpec.java @@ -55,7 +55,6 @@ public final class FilterSpec { return val.testBit(type); } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (!(obj instanceof ObjectTypes)) { @@ -66,7 +65,6 @@ public final class FilterSpec { return other.val.equals(val); } - /** {@inheritDoc} */ @Override public int hashCode() { return val.hashCode(); @@ -139,6 +137,8 @@ public final class FilterSpec { } /** + * Specify permitted object types + * * @param types * set of permitted object types, for use in "blob:none" and * "object:none" filters @@ -149,6 +149,8 @@ public final class FilterSpec { } /** + * Specify blob limit + * * @param blobLimit * the blob limit in a "blob:[limit]" filter line * @return a filter spec which filters blobs above a certain size @@ -162,6 +164,8 @@ public final class FilterSpec { } /** + * Specify tree depth limit + * * @param treeDepthLimit * the tree depth limit in a "tree:[depth]" filter line * @return a filter spec which filters blobs and trees beyond a certain tree @@ -181,6 +185,8 @@ public final class FilterSpec { public static final FilterSpec NO_FILTER = new FilterSpec(ObjectTypes.ALL, -1, -1); /** + * Whether object type is allowed + * * @param type * a Git object type, such as * {@link org.eclipse.jgit.lib.Constants#OBJ_BLOB} @@ -193,6 +199,8 @@ public final class FilterSpec { } /** + * Get blob size limit + * * @return -1 if this filter does not filter blobs based on size, or a * non-negative integer representing the max size of blobs to allow */ @@ -201,6 +209,8 @@ public final class FilterSpec { } /** + * Get tree depth limit + * * @return -1 if this filter does not filter blobs and trees based on depth, * or a non-negative integer representing the max tree depth of * blobs and trees to fetch @@ -210,6 +220,8 @@ public final class FilterSpec { } /** + * Whether this filter is a no-op + * * @return true if this filter doesn't filter out anything */ public boolean isNoOp() { @@ -217,7 +229,10 @@ public final class FilterSpec { } /** - * @return the filter line which describes this spec, e.g. "filter blob:limit=42" + * Get filter line describing this spec + * + * @return the filter line which describes this spec, e.g. "filter + * blob:limit=42" */ @Nullable public String filterLine() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java index 44656c1397..5d334e67f7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java @@ -71,6 +71,7 @@ public interface FtpChannel { * @param unit * of the {@code timeout} * @throws IOException + * if an IO error occurred */ void connect(int timeout, TimeUnit unit) throws IOException; @@ -80,6 +81,8 @@ public interface FtpChannel { void disconnect(); /** + * Whether the FtpChannel is connected + * * @return whether the {@link FtpChannel} is connected */ boolean isConnected(); @@ -95,8 +98,11 @@ public interface FtpChannel { void cd(String path) throws IOException; /** + * Get current remote directory path + * * @return the current remote directory path * @throws IOException + * if an IO error occurred */ String pwd() throws IOException; @@ -118,6 +124,7 @@ public interface FtpChannel { * of the directory to list * @return the directory entries * @throws IOException + * if an IO error occurred */ Collection<DirEntry> ls(String path) throws IOException; @@ -128,6 +135,7 @@ public interface FtpChannel { * @param path * to delete * @throws IOException + * if an IO error occurred */ void rmdir(String path) throws IOException; @@ -137,6 +145,7 @@ public interface FtpChannel { * @param path * to create * @throws IOException + * if an IO error occurred */ void mkdir(String path) throws IOException; @@ -148,6 +157,7 @@ public interface FtpChannel { * * @return the stream to read from * @throws IOException + * if an IO error occurred */ InputStream get(String path) throws IOException; @@ -160,6 +170,7 @@ public interface FtpChannel { * * @return the stream to read from * @throws IOException + * if an IO error occurred */ OutputStream put(String path) throws IOException; @@ -204,6 +215,7 @@ public interface FtpChannel { * @param to * new name of the file * @throws IOException + * if an IO error occurred * @see <a href= * "http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html">stdio.h: * rename()</a> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java index 14d6c1ec14..655b4605e4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java @@ -35,9 +35,8 @@ public class HMACSHA1NonceGenerator implements NonceGenerator { * * @param seed * seed the generator - * @throws java.lang.IllegalStateException */ - public HMACSHA1NonceGenerator(String seed) throws IllegalStateException { + public HMACSHA1NonceGenerator(String seed) { try { byte[] keyBytes = seed.getBytes(ISO_8859_1); SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA1"); //$NON-NLS-1$ @@ -48,16 +47,13 @@ public class HMACSHA1NonceGenerator implements NonceGenerator { } } - /** {@inheritDoc} */ @Override - public synchronized String createNonce(Repository repo, long timestamp) - throws IllegalStateException { + public synchronized String createNonce(Repository repo, long timestamp) { String input = repo.getIdentifier() + ":" + String.valueOf(timestamp); //$NON-NLS-1$ byte[] rawHmac = mac.doFinal(input.getBytes(UTF_8)); return Long.toString(timestamp) + "-" + toHex(rawHmac); //$NON-NLS-1$ } - /** {@inheritDoc} */ @Override public NonceStatus verify(String received, String sent, Repository db, boolean allowSlop, int slop) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpAuthMethod.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpAuthMethod.java index aec5b89c7e..c2d8c1b73f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpAuthMethod.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpAuthMethod.java @@ -140,7 +140,7 @@ abstract class HttpAuthMethod { valuePart[0].toUpperCase(Locale.ROOT)); if ((ignoreTypes != null) - && (ignoreTypes.contains(methodType))) { + && ignoreTypes.contains(methodType)) { continue; } @@ -224,7 +224,9 @@ abstract class HttpAuthMethod { * Update this method with the given username and password pair. * * @param user + * username * @param pass + * password */ abstract void authorize(String user, String pass); @@ -232,7 +234,9 @@ abstract class HttpAuthMethod { * Update connection properties based on this authentication method. * * @param conn + * the connection to configure * @throws IOException + * if an IO error occurred */ abstract void configureRequest(HttpConnection conn) throws IOException; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java index dc82f46197..f10b7bf452 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java @@ -101,7 +101,7 @@ public class HttpConfig { private static final int DEFAULT_MAX_REDIRECTS = 5; - private static final int MAX_REDIRECTS = (new Supplier<Integer>() { + private static final int MAX_REDIRECTS = new Supplier<Integer>() { @Override public Integer get() { @@ -119,7 +119,7 @@ public class HttpConfig { } return value; } - }).get().intValue(); + }.get().intValue(); private static final String ENV_HTTP_USER_AGENT = "GIT_HTTP_USER_AGENT"; //$NON-NLS-1$ @@ -302,8 +302,7 @@ public class HttpConfig { int postBufferSize = config.getInt(HTTP, POST_BUFFER_KEY, 1 * 1024 * 1024); boolean sslVerifyFlag = config.getBoolean(HTTP, SSL_VERIFY_KEY, true); - HttpRedirectMode followRedirectsMode = config.getEnum( - HttpRedirectMode.values(), HTTP, null, + HttpRedirectMode followRedirectsMode = config.getEnum(HTTP, null, FOLLOW_REDIRECTS_KEY, HttpRedirectMode.INITIAL); int redirectLimit = config.getInt(HTTP, MAX_REDIRECTS_KEY, MAX_REDIRECTS); @@ -335,8 +334,8 @@ public class HttpConfig { postBufferSize); sslVerifyFlag = config.getBoolean(HTTP, match, SSL_VERIFY_KEY, sslVerifyFlag); - followRedirectsMode = config.getEnum(HttpRedirectMode.values(), - HTTP, match, FOLLOW_REDIRECTS_KEY, followRedirectsMode); + followRedirectsMode = config.getEnum(HTTP, match, + FOLLOW_REDIRECTS_KEY, followRedirectsMode); int newMaxRedirects = config.getInt(HTTP, match, MAX_REDIRECTS_KEY, redirectLimit); if (newMaxRedirects >= 0) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalFetchConnection.java index 18dc792eb1..05736e1107 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalFetchConnection.java @@ -70,6 +70,7 @@ class InternalFetchConnection<C> extends BasePackFetchConnection { worker = new Thread("JGit-Upload-Pack") { //$NON-NLS-1$ @Override + @SuppressWarnings("CatchAndPrintStackTrace") public void run() { try { final UploadPack rp = uploadPackFactory.create(req, remote); @@ -103,7 +104,6 @@ class InternalFetchConnection<C> extends BasePackFetchConnection { readAdvertisedRefs(); } - /** {@inheritDoc} */ @Override public void close() { super.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalPushConnection.java index d2b0ef4a29..a23fdc94ad 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalPushConnection.java @@ -99,7 +99,6 @@ class InternalPushConnection<C> extends BasePackPushConnection { readAdvertisedRefs(); } - /** {@inheritDoc} */ @Override public void close() { super.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/LsRefsV2Request.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/LsRefsV2Request.java index 856047ee19..008f1d9b20 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/LsRefsV2Request.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/LsRefsV2Request.java @@ -53,22 +53,36 @@ public final class LsRefsV2Request { this.clientSID = clientSID; } - /** @return ref prefixes that the client requested. */ + /** + * Get ref prefixes + * + * @return ref prefixes that the client requested. + */ public List<String> getRefPrefixes() { return refPrefixes; } - /** @return true if the client requests symbolic references. */ + /** + * Whether the client requests symbolic references + * + * @return true if the client requests symbolic references. + */ public boolean getSymrefs() { return symrefs; } - /** @return true if the client requests tags to be peeled. */ + /** + * Whether the client requests tags to be peeled + * + * @return true if the client requests tags to be peeled. + */ public boolean getPeel() { return peel; } /** + * Get agent reported by the client + * * @return agent as reported by the client * * @since 5.2 @@ -79,6 +93,8 @@ public final class LsRefsV2Request { } /** + * Get session-id reported by the client + * * @return session-id as reported by the client * * @since 6.4 @@ -106,7 +122,11 @@ public final class LsRefsV2Request { return serverOptions; } - /** @return A builder of {@link LsRefsV2Request}. */ + /** + * Create builder + * + * @return A builder of {@link LsRefsV2Request}. + */ public static Builder builder() { return new Builder(); } @@ -129,7 +149,10 @@ public final class LsRefsV2Request { } /** + * Set ref prefixes + * * @param value + * ref prefix values * @return the Builder */ public Builder setRefPrefixes(List<String> value) { @@ -138,7 +161,10 @@ public final class LsRefsV2Request { } /** + * Set symrefs + * * @param value + * of symrefs * @return the Builder */ public Builder setSymrefs(boolean value) { @@ -147,7 +173,10 @@ public final class LsRefsV2Request { } /** + * Set whether to peel tags + * * @param value + * of peel * @return the Builder */ public Builder setPeel(boolean value) { @@ -203,7 +232,11 @@ public final class LsRefsV2Request { return this; } - /** @return LsRefsV2Request */ + /** + * Builds the request + * + * @return LsRefsV2Request the request + */ public LsRefsV2Request build() { return new LsRefsV2Request( Collections.unmodifiableList(refPrefixes), symrefs, peel, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java index 6531b17e23..e32530cc9b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java @@ -67,7 +67,6 @@ public class NetRCCredentialsProvider extends CredentialsProvider { CredentialsProvider.setDefault(new NetRCCredentialsProvider()); } - /** {@inheritDoc} */ @Override public boolean supports(CredentialItem... items) { for (CredentialItem i : items) { @@ -81,7 +80,6 @@ public class NetRCCredentialsProvider extends CredentialsProvider { return true; } - /** {@inheritDoc} */ @Override public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem { @@ -112,7 +110,6 @@ public class NetRCCredentialsProvider extends CredentialsProvider { return !isAnyNull(items); } - /** {@inheritDoc} */ @Override public boolean isInteractive() { return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NonceGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NonceGenerator.java index 2541ff1393..30e80e0783 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NonceGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NonceGenerator.java @@ -31,10 +31,8 @@ public interface NonceGenerator { * @param timestamp * The current time in seconds. * @return The nonce to be signed by the pusher - * @throws java.lang.IllegalStateException */ - String createNonce(Repository db, long timestamp) - throws IllegalStateException; + String createNonce(Repository db, long timestamp); /** * Verify trustworthiness of the received nonce. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ObjectInfoRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ObjectInfoRequest.java index 86a2716675..241f1e749f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ObjectInfoRequest.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ObjectInfoRequest.java @@ -34,12 +34,20 @@ public final class ObjectInfoRequest { this.objectIDs = objectIDs; } - /** @return object IDs that the client requested. */ + /** + * Get object ids requested by the client + * + * @return object IDs that the client requested. + */ public List<ObjectId> getObjectIDs() { return this.objectIDs; } - /** @return A builder of {@link ObjectInfoRequest}. */ + /** + * Create builder + * + * @return A builder of {@link ObjectInfoRequest}. + */ public static Builder builder() { return new Builder(); } @@ -52,7 +60,10 @@ public final class ObjectInfoRequest { } /** + * Set object ids + * * @param value + * of objectIds * @return the Builder */ public Builder setObjectIDs(List<ObjectId> value) { @@ -60,7 +71,11 @@ public final class ObjectInfoRequest { return this; } - /** @return ObjectInfoRequest */ + /** + * Build the request + * + * @return ObjectInfoRequest the request + */ public ObjectInfoRequest build() { return new ObjectInfoRequest( Collections.unmodifiableList(objectIDs)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java index d9669044c7..e1f2b19ce5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java @@ -530,7 +530,7 @@ public abstract class PackParser { receiving.beginTask(JGitText.get().receivingObjects, (int) expectedObjectCount); try { - for (int done = 0; done < expectedObjectCount; done++) { + for (long done = 0; done < expectedObjectCount; done++) { indexOneObject(); receiving.update(1); if (receiving.isCancelled()) @@ -1102,6 +1102,7 @@ public abstract class PackParser { * @param data * raw content of the object. * @throws org.eclipse.jgit.errors.CorruptObjectException + * if a corrupt object was found * @since 4.9 */ protected void verifySafeObject(final AnyObjectId id, final int type, @@ -1565,7 +1566,7 @@ public abstract class PackParser { * @param baseStreamPosition * position of the base object in the incoming stream. The base * must be before the delta, therefore {@code baseStreamPosition - * < deltaStreamPosition}. This is <b>not</b> the position + * < deltaStreamPosition}. This is <b>not</b> the position * returned by a prior end object event. * @param inflatedSize * size of the delta when fully inflated. The size stored within @@ -1667,17 +1668,27 @@ public abstract class PackParser { long sizeBeforeInflating; - /** @return offset within the input stream. */ + /** + * Get offset within the input stream + * + * @return offset within the input stream. + */ public long getOffset() { return position; } - /** @return the CRC-32 checksum of the stored delta data. */ + /** + * Get the CRC-32 checksum of the stored delta data + * + * @return the CRC-32 checksum of the stored delta data. + */ public int getCRC() { return crc; } /** + * Set the CRC-32 checksum of the stored delta data + * * @param crc32 * the CRC-32 checksum of the stored delta data. */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java index bf7997ec62..fae26cda95 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java @@ -117,6 +117,7 @@ public class PackedObjectInfo extends ObjectIdOwnerMap.Entry { * Size in storage * * @param sizeBeforeInflating + * size before inflating */ void setSize(long sizeBeforeInflating) { this.sizeBeforeInflating = sizeBeforeInflating; @@ -147,6 +148,8 @@ public class PackedObjectInfo extends ObjectIdOwnerMap.Entry { } /** + * Get full size (inflated, undeltified) + * * @return size of the object (inflated, undeltified) * * @since 6.4 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java index ed33eaed07..614ad88246 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java @@ -43,24 +43,13 @@ public class PacketLineIn { /** * Magic return from {@link #readString()} when a flush packet is found. - * - * @deprecated Callers should use {@link #isEnd(String)} to check if a - * string is the end marker, or - * {@link PacketLineIn#readStrings()} to iterate over all - * strings in the input stream until the marker is reached. */ - @Deprecated - public static final String END = new String(); /* must not string pool */ + private static final String END = new String(); /* must not string pool */ /** * Magic return from {@link #readString()} when a delim packet is found. - * - * @since 5.0 - * @deprecated Callers should use {@link #isDelimiter(String)} to check if a - * string is the delimiter. */ - @Deprecated - public static final String DELIM = new String(); /* must not string pool */ + private static final String DELIM = new String(); /* must not string pool */ enum AckNackResult { /** NAK */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java index 77f0a7a516..43b742b02d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java @@ -85,8 +85,10 @@ public class PacketLineOut { } /** + * Whether is using sideband + * * @return whether to add a sideband designator to each non-flush and - * non-delim packet + * non-delim packet * @see #setUsingSideband * @since 5.5 */ @@ -95,11 +97,14 @@ public class PacketLineOut { } /** - * @param value If true, when writing packet lines, add, as the first - * byte, a sideband designator to each non-flush and non-delim - * packet. See pack-protocol.txt and protocol-v2.txt from the Git - * project for more information, specifically the "side-band" and - * "sideband-all" sections. + * Set whether to use sideband + * + * @param value + * If true, when writing packet lines, add, as the first byte, a + * sideband designator to each non-flush and non-delim packet. + * See pack-protocol.txt and protocol-v2.txt from the Git project + * for more information, specifically the "side-band" and + * "sideband-all" sections. * @since 5.5 */ public void setUsingSideband(boolean value) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHookChain.java index 4d53e1b261..f9d2c38ad3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHookChain.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHookChain.java @@ -47,7 +47,6 @@ public class PostReceiveHookChain implements PostReceiveHook { } } - /** {@inheritDoc} */ @Override public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java index 4334888a9a..072f43f41b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java @@ -80,7 +80,6 @@ public class PostUploadHookChain implements PostUploadHook { } } - /** {@inheritDoc} */ @Override public void onPostUpload(PackStatistics stats) { for (PostUploadHook hook : hooks) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHookChain.java index dffa30da8e..2a5522bb4c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHookChain.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHookChain.java @@ -46,7 +46,6 @@ public class PreReceiveHookChain implements PreReceiveHook { } } - /** {@inheritDoc} */ @Override public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHookChain.java index 9c28abed95..dc703d0b11 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHookChain.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHookChain.java @@ -48,7 +48,6 @@ public class PreUploadHookChain implements PreUploadHook { } } - /** {@inheritDoc} */ @Override public void onBeginNegotiateRound(UploadPack up, Collection<? extends ObjectId> wants, int cntOffered) @@ -58,7 +57,6 @@ public class PreUploadHookChain implements PreUploadHook { } } - /** {@inheritDoc} */ @Override public void onEndNegotiateRound(UploadPack up, Collection<? extends ObjectId> wants, int cntCommon, @@ -69,7 +67,6 @@ public class PreUploadHookChain implements PreUploadHook { } } - /** {@inheritDoc} */ @Override public void onSendPack(UploadPack up, Collection<? extends ObjectId> wants, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java index 9d055519a5..f1afeab020 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java @@ -51,7 +51,9 @@ final class ProtocolV0Parser { * incoming lines. This method will read until an END line. * @return a FetchV0Request with the data received in the wire. * @throws PackProtocolException + * if a protocol occurred * @throws IOException + * if an IO error occurred */ FetchV0Request recvWants(PacketLineIn pckIn) throws PackProtocolException, IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Hook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Hook.java index d7626df3fa..2caecbe843 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Hook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Hook.java @@ -24,6 +24,8 @@ public interface ProtocolV2Hook { }; /** + * Handle capabilities request + * * @param req * the capabilities request * @throws ServiceMayNotContinueException @@ -36,6 +38,8 @@ public interface ProtocolV2Hook { } /** + * Handle ls-refs request + * * @param req * the ls-refs request * @throws ServiceMayNotContinueException @@ -48,8 +52,12 @@ public interface ProtocolV2Hook { } /** - * @param req the fetch request - * @throws ServiceMayNotContinueException abort; the message will be sent to the user + * Handle fetch request + * + * @param req + * the fetch request + * @throws ServiceMayNotContinueException + * abort; the message will be sent to the user */ default void onFetch(FetchV2Request req) throws ServiceMayNotContinueException { @@ -57,6 +65,8 @@ public interface ProtocolV2Hook { } /** + * Handle object-info request + * * @param req * the object-info request * @throws ServiceMayNotContinueException diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java index d9831c7701..437abb0639 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java @@ -222,13 +222,11 @@ public class PushCertificate { return sb; } - /** {@inheritDoc} */ @Override public int hashCode() { return signature.hashCode(); } - /** {@inheritDoc} */ @Override public boolean equals(Object o) { if (!(o instanceof PushCertificate)) { @@ -259,7 +257,6 @@ public class PushCertificate { return true; } - /** {@inheritDoc} */ @Override public String toString() { return getClass().getSimpleName() + '[' diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateIdent.java index 94d1169abb..bd76b34aef 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateIdent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateIdent.java @@ -228,20 +228,17 @@ public class PushCertificateIdent { return tzOffset; } - /** {@inheritDoc} */ @Override public boolean equals(Object o) { return (o instanceof PushCertificateIdent) && raw.equals(((PushCertificateIdent) o).raw); } - /** {@inheritDoc} */ @Override public int hashCode() { return raw.hashCode(); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java index 5009ecf8f3..463d05393f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java @@ -10,8 +10,8 @@ package org.eclipse.jgit.transport; -import static org.eclipse.jgit.transport.ReceivePack.parseCommand; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT; +import static org.eclipse.jgit.transport.ReceivePack.parseCommand; import java.io.EOFException; import java.io.IOException; @@ -317,7 +317,7 @@ public class PushCertificateParser { * certificate. * <p> * This method doesn't parse the first line {@code "push-cert \NUL - * <capabilities>"}, but assumes the first line including the + * <capabilities>"}, but assumes the first line including the * capabilities has already been handled by the caller. * * @param pckIn diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java index a9e93b6be6..6bdaf0e234 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java @@ -24,6 +24,7 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -329,7 +330,7 @@ public class PushCertificateStore implements AutoCloseable { if (newId == null) { return RefUpdate.Result.NO_CHANGE; } - try (ObjectInserter inserter = db.newObjectInserter()) { + try { RefUpdate.Result result = updateRef(newId); switch (result) { case FAST_FORWARD: @@ -404,8 +405,8 @@ public class PushCertificateStore implements AutoCloseable { } private static void sortPending(List<PendingCert> pending) { - Collections.sort(pending, (PendingCert a, PendingCert b) -> Long.signum( - a.ident.getWhen().getTime() - b.ident.getWhen().getTime())); + Collections.sort(pending, + Comparator.comparing((PendingCert a) -> a.ident.getWhenAsInstant())); } private DirCache newDirCache() throws IOException { @@ -503,7 +504,7 @@ public class PushCertificateStore implements AutoCloseable { } else { sb.append(MessageFormat.format( JGitText.get().storePushCertMultipleRefs, - Integer.valueOf(cert.getCommands().size()))); + cert.getCommands().size())); } return sb.append('\n').toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java index 51c8558bfd..32c443f0f3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java @@ -43,17 +43,14 @@ public interface PushConnection extends Connection { * 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 org.eclipse.jgit.errors.TransportException}. - * </p> * <p> * Implementation may use local repository to send a minimum set of objects * needed by remote repository in efficient way. * {@link org.eclipse.jgit.transport.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 @@ -90,17 +87,14 @@ public interface PushConnection extends Connection { * 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 org.eclipse.jgit.errors.TransportException}. - * </p> * <p> * Implementation may use local repository to send a minimum set of objects * needed by remote repository in efficient way. * {@link org.eclipse.jgit.transport.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 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java index b59ae0c450..8f90f326d3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java @@ -76,6 +76,7 @@ class PushProcess { * {@link PrePushHook} to run after the remote advertisement has * been gotten * @throws TransportException + * if a protocol error occurred during push/fetch */ PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush, PrePushHook prePush) throws TransportException { @@ -96,6 +97,7 @@ class PushProcess { * @param out * OutputStream to write messages to * @throws TransportException + * if a protocol error occurred during push/fetch */ PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush, PrePushHook prePush, OutputStream out) throws TransportException { @@ -272,6 +274,7 @@ class PushProcess { * {@link ObjectId} of the new commit * @return {@code true} if the update fast-forwards, {@code false} otherwise * @throws TransportException + * if a protocol error occurred during push/fetch */ private boolean isFastForward(ObjectId oldOid, ObjectId newOid) throws TransportException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java index ab411600f1..bfc75f036c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java @@ -849,7 +849,6 @@ public class ReceiveCommand { JGitText.get().lockError, err.getMessage())); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java index 816cec89af..6f211e0794 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -88,44 +88,6 @@ import org.eclipse.jgit.util.io.TimeoutOutputStream; * Implements the server side of a push connection, receiving objects. */ public class ReceivePack { - /** - * Data in the first line of a request, the line itself plus capabilities. - * - * @deprecated Use {@link FirstCommand} instead. - * @since 5.6 - */ - @Deprecated - public static class FirstLine { - private final FirstCommand command; - - /** - * Parse the first line of a receive-pack request. - * - * @param line - * line from the client. - */ - public FirstLine(String line) { - command = FirstCommand.fromLine(line); - } - - /** @return non-capabilities part of the line. */ - public String getLine() { - return command.getLine(); - } - - /** @return capabilities parsed from the line. */ - public Set<String> getCapabilities() { - Set<String> reconstructedCapabilites = new HashSet<>(); - for (Map.Entry<String, String> e : command.getCapabilities() - .entrySet()) { - String cap = e.getValue() == null ? e.getKey() - : e.getKey() + "=" + e.getValue(); //$NON-NLS-1$ - reconstructedCapabilites.add(cap); - } - - return reconstructedCapabilites; - } - } /** Database we write the stored objects into. */ private final Repository db; @@ -460,6 +422,7 @@ public class ReceivePack { * null, assumes the default set of additional haves from the * repository. * @throws IOException + * if an IO error occurred */ public void setAdvertisedRefs(Map<String, Ref> allRefs, Set<ObjectId> additionalHaves) throws IOException { @@ -1024,6 +987,7 @@ public class ReceivePack { * Set an error handler for {@link ReceiveCommand}. * * @param receiveCommandErrorHandler + * the error handler * @since 5.7 */ public void setReceiveCommandErrorHandler( @@ -1212,6 +1176,7 @@ public class ReceivePack { * * @return advertised refs, or the default if not explicitly advertised. * @throws IOException + * if an IO error occurred */ private Map<String, Ref> getAdvertisedOrDefaultRefs() throws IOException { if (refs == null) @@ -1348,7 +1313,8 @@ public class ReceivePack { /** * Receive a list of commands from the input. * - * @throws java.io.IOException + * @throws IOException + * if an IO error occurred */ private void recvCommands() throws IOException { PacketLineIn pck = maxCommandBytes > 0 @@ -1936,7 +1902,8 @@ public class ReceivePack { /** * Close and flush (if necessary) the underlying streams. * - * @throws java.io.IOException + * @throws IOException + * if an IO error occurred */ private void close() throws IOException { if (sideBand) { @@ -2125,8 +2092,10 @@ public class ReceivePack { } /** + * Set the unpackErrorHandler + * * @param unpackErrorHandler - * the unpackErrorHandler to set + * the unpackErrorHandler * @since 5.7 */ public void setUnpackErrorHandler(UnpackErrorHandler unpackErrorHandler) { @@ -2134,22 +2103,8 @@ public class ReceivePack { } /** - * Set whether this class will report command failures as warning messages - * before sending the command results. + * Get the client session-id * - * @param echo - * if true this class will report command failures as warning - * messages before sending the command results. This is usually - * not necessary, but may help buggy Git clients that discard the - * errors when all branches fail. - * @deprecated no widely used Git versions need this any more - */ - @Deprecated - public void setEchoCommandFailures(boolean echo) { - // No-op. - } - - /** * @return The client session-id. * @since 6.4 */ @@ -2174,6 +2129,7 @@ public class ReceivePack { * standard error channel of the command execution. For most * other network connections this should be null. * @throws java.io.IOException + * if an IO error occurred */ public void receive(final InputStream input, final OutputStream output, final OutputStream messages) throws IOException { @@ -2217,6 +2173,7 @@ public class ReceivePack { * standard error channel of the command execution. For most * other network connections this should be null. * @throws java.io.IOException + * if an IO error occurred * @since 5.7 */ public void receiveWithExceptionPropagation(InputStream input, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivedPackStatistics.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivedPackStatistics.java index d7bc40006b..8887e263ea 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivedPackStatistics.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivedPackStatistics.java @@ -174,7 +174,10 @@ public class ReceivedPackStatistics { private long numDeltaTag; /** - * @param numBytesRead number of bytes read from the input stream + * Set number of bytes read from the input stream + * + * @param numBytesRead + * number of bytes read from the input stream * @return this */ public Builder setNumBytesRead(long numBytesRead) { @@ -183,6 +186,8 @@ public class ReceivedPackStatistics { } /** + * Increment additional bytes already in the local database + * * @param size * additional bytes already in the local database * @return this @@ -220,13 +225,21 @@ public class ReceivedPackStatistics { return this; } - /** @return this */ + /** + * Increment offset delta + * + * @return this + */ public Builder addOffsetDelta() { numOfsDelta++; return this; } - /** @return this */ + /** + * Increment ref delta + * + * @return this + */ public Builder addRefDelta() { numRefDelta++; return this; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java index c525e66848..3d4bea2e48 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java @@ -161,9 +161,11 @@ public abstract class RefAdvertiser { } /** + * Set whether this advertiser should use protocol v2 + * * @param b - * true if this advertiser should advertise using the protocol - * v2 format, false otherwise + * true if this advertiser should advertise using the protocol v2 + * format, false otherwise * @since 5.0 */ public void setUseProtocolV2(boolean b) { @@ -173,10 +175,9 @@ public abstract class RefAdvertiser { /** * Toggle tag peeling. * <p> - * <p> + * * This method must be invoked prior to any of the following: * <ul> - * <li>{@link #send(Map)}</li> * <li>{@link #send(Collection)}</li> * </ul> * @@ -193,7 +194,6 @@ public abstract class RefAdvertiser { * <p> * This method must be invoked prior to any of the following: * <ul> - * <li>{@link #send(Map)}</li> * <li>{@link #send(Collection)}</li> * <li>{@link #advertiseHave(AnyObjectId)}</li> * </ul> @@ -228,7 +228,6 @@ public abstract class RefAdvertiser { * <p> * This method must be invoked prior to any of the following: * <ul> - * <li>{@link #send(Map)}</li> * <li>{@link #send(Collection)}</li> * <li>{@link #advertiseHave(AnyObjectId)}</li> * </ul> @@ -258,24 +257,6 @@ public abstract class RefAdvertiser { * @throws java.io.IOException * the underlying output stream failed to write out an * advertisement record. - * @deprecated use {@link #send(Collection)} instead. - */ - @Deprecated - public Set<ObjectId> send(Map<String, Ref> refs) throws IOException { - return send(refs.values()); - } - - /** - * Format an advertisement for the supplied refs. - * - * @param refs - * zero or more refs to format for the client. The collection is - * sorted before display if necessary, and therefore may appear - * in any order. - * @return set of ObjectIds that were advertised to the client. - * @throws java.io.IOException - * the underlying output stream failed to write out an - * advertisement record. * @since 5.0 */ public Set<ObjectId> send(Collection<Ref> refs) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefLeaseSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefLeaseSpec.java index 1af3fadadb..8a131c8971 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefLeaseSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefLeaseSpec.java @@ -58,7 +58,6 @@ public class RefLeaseSpec implements Serializable { return expected; } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java index 61d193593a..0466085b32 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java @@ -616,7 +616,6 @@ public class RefSpec implements Serializable { return true; } - /** {@inheritDoc} */ @Override public int hashCode() { int hc = 0; @@ -627,7 +626,6 @@ public class RefSpec implements Serializable { return hc; } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (!(obj instanceof RefSpec)) @@ -649,7 +647,6 @@ public class RefSpec implements Serializable { && Objects.equals(getDestination(), b.getDestination()); } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java index 218e62c10a..fb3cd21d1a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java @@ -541,7 +541,6 @@ public class RemoteRefUpdate { trackingRefUpdate.setResult(localUpdate.update(walk)); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java index 96c7be5b97..1f96be8e47 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java @@ -94,7 +94,6 @@ public class SideBandInputStream extends InputStream { out = outputStream; } - /** {@inheritDoc} */ @Override public int read() throws IOException { needDataPacket(); @@ -104,7 +103,6 @@ public class SideBandInputStream extends InputStream { return rawIn.read(); } - /** {@inheritDoc} */ @Override public int read(byte[] b, int off, int len) throws IOException { int r = 0; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandOutputStream.java index bb80299fad..b6d840dd81 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandOutputStream.java @@ -94,14 +94,12 @@ public class SideBandOutputStream extends OutputStream { writeBuffer(); } - /** {@inheritDoc} */ @Override public void flush() throws IOException { flushBuffer(); out.flush(); } - /** {@inheritDoc} */ @Override public void write(byte[] b, int off, int len) throws IOException { while (0 < len) { @@ -128,7 +126,6 @@ public class SideBandOutputStream extends OutputStream { } } - /** {@inheritDoc} */ @Override public void write(int b) throws IOException { if (cnt == buffer.length) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java index 33308600d9..1e85d81084 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java @@ -28,7 +28,6 @@ class SideBandProgressMonitor extends BatchingProgressMonitor { write = true; } - /** {@inheritDoc} */ @Override protected void onUpdate(String taskName, int workCurr, Duration duration) { StringBuilder s = new StringBuilder(); @@ -37,7 +36,6 @@ class SideBandProgressMonitor extends BatchingProgressMonitor { send(s); } - /** {@inheritDoc} */ @Override protected void onEndTask(String taskName, int workCurr, Duration duration) { StringBuilder s = new StringBuilder(); @@ -54,7 +52,6 @@ class SideBandProgressMonitor extends BatchingProgressMonitor { appendDuration(s, duration); } - /** {@inheritDoc} */ @Override protected void onUpdate(String taskName, int cmp, int totalWork, int pcnt, Duration duration) { @@ -64,7 +61,6 @@ class SideBandProgressMonitor extends BatchingProgressMonitor { send(s); } - /** {@inheritDoc} */ @Override protected void onEndTask(String taskName, int cmp, int totalWork, int pcnt, Duration duration) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigStore.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigStore.java index 1226a6b5ea..b2e2549f12 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigStore.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigStore.java @@ -43,6 +43,7 @@ public interface SshConfigStore { * {@link SshConstants#CONNECTION_ATTEMPTS}, fill those values with defaults * from the arguments: * <table> + * <caption>Description of arguments</caption> * <tr> * <th>ssh config key</th> * <th>value from argument</th> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java index 48cacf0964..41dbdcab51 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java @@ -125,6 +125,25 @@ public final class SshConstants { /** Key in an ssh config file. */ public static final String NUMBER_OF_PASSWORD_PROMPTS = "NumberOfPasswordPrompts"; + /** + * Path to a shared library of a PKCS11 key provider, or "none". + * <p> + * If set and not "none", the provider's keys should be used. + * </p> + * + * @since 6.7 + */ + public static final String PKCS11_PROVIDER = "PKCS11Provider"; + + /** + * Non-standard JGit addition: specify the PKCS#11 slot list index of the + * token to use. A positive number; defaults to zero; ignored if negative + * (in which case zero is used, too). + * + * @since 6.7 + */ + public static final String PKCS11_SLOT_LIST_INDEX = "PKCS11SlotListIndex"; + /** Key in an ssh config file. */ public static final String PORT = "Port"; @@ -146,8 +165,8 @@ public final class SshConstants { * Comma-separated list of jump hosts, defining a jump host chain <em>in * reverse order</em>. Each jump host is a SSH URI or "[user@]host[:port]". * <p> - * Reverse order means: to connect A->B->target, one can do in - * {@code ~/.ssh/config} either of: + * Reverse order means: to connect {@literal A -> B -> target}, one can do + * in {@code ~/.ssh/config} either of: * </p> * * <pre> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java index a0194ea8b1..8120df0698 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java @@ -11,8 +11,6 @@ package org.eclipse.jgit.transport; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Iterator; import java.util.ServiceLoader; @@ -99,9 +97,8 @@ public abstract class SshSessionFactory { * @since 5.2 */ public static String getLocalUserName() { - return AccessController - .doPrivileged((PrivilegedAction<String>) () -> SystemReader - .getInstance().getProperty(Constants.OS_USER_NAME_KEY)); + return SystemReader.getInstance() + .getProperty(Constants.OS_USER_NAME_KEY); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshTransport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshTransport.java index 28ec92c706..8d93977a1d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshTransport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshTransport.java @@ -108,7 +108,6 @@ public abstract class SshTransport extends TcpTransport { return sock; } - /** {@inheritDoc} */ @Override public void close() { if (sock != null) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TestProtocol.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TestProtocol.java index 1985b66d87..77ab0f676d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TestProtocol.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TestProtocol.java @@ -81,19 +81,16 @@ public class TestProtocol<C> extends TransportProtocol { this.handles = new HashMap<>(); } - /** {@inheritDoc} */ @Override public String getName() { return JGitText.get().transportProtoTest; } - /** {@inheritDoc} */ @Override public Set<String> getSchemes() { return Collections.singleton(SCHEME); } - /** {@inheritDoc} */ @Override public Transport open(URIish uri, Repository local, String remoteName) throws NotSupportedException, TransportException { @@ -105,13 +102,11 @@ public class TestProtocol<C> extends TransportProtocol { return new TransportInternal(local, uri, h); } - /** {@inheritDoc} */ @Override public Set<URIishField> getRequiredFields() { return EnumSet.of(URIishField.HOST, URIishField.PATH); } - /** {@inheritDoc} */ @Override public Set<URIishField> getOptionalFields() { return Collections.emptySet(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java index 51bc07cb94..26045a2c7c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java @@ -174,7 +174,6 @@ public class TrackingRefUpdate { } } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java index 064201a629..5333beff4b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java @@ -297,6 +297,8 @@ public class TransferConfig { } /** + * Whether clients are allowed to specify "filter" line + * * @return true if clients are allowed to specify a "filter" line * @since 5.0 */ @@ -305,6 +307,8 @@ public class TransferConfig { } /** + * Whether clients are allowed to specify "want-ref" line + * * @return true if clients are allowed to specify a "want-ref" line * @since 5.1 */ @@ -313,6 +317,8 @@ public class TransferConfig { } /** + * Whether the server accepts sideband-all requests + * * @return true if the server accepts sideband-all requests (see * {{@link #isAdvertiseSidebandAll()} for the advertisement) * @since 5.5 @@ -322,6 +328,8 @@ public class TransferConfig { } /** + * Whether to advertise sideband all to the clients + * * @return true to advertise sideband all to the clients * @since 5.6 */ @@ -330,6 +338,8 @@ public class TransferConfig { } /** + * Whether to advertise wait-for-done all to the clients + * * @return true to advertise wait-for-done all to the clients * @since 5.13 */ @@ -338,6 +348,8 @@ public class TransferConfig { } /** + * Whether to advertise object-info to all clients + * * @return true to advertise object-info to all clients * @since 5.13 */ @@ -346,6 +358,8 @@ public class TransferConfig { } /** + * Whether to advertise and receive session-id capability + * * @return true to advertise and receive session-id capability * @since 6.4 */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java index ee35f4866e..ac76e83d34 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -33,10 +33,8 @@ import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Vector; import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.jgit.annotations.NonNull; @@ -76,6 +74,7 @@ public abstract class Transport implements AutoCloseable { PUSH; } + // Use weak references to enable unloading dynamically loaded protocols private static final List<WeakReference<TransportProtocol>> protocols = new CopyOnWriteArrayList<>(); @@ -108,7 +107,7 @@ public abstract class Transport implements AutoCloseable { String name = prefix + Transport.class.getName(); return ldr.getResources(name); } catch (IOException err) { - return new Vector<URL>().elements(); + return Collections.emptyEnumeration(); } } @@ -191,11 +190,13 @@ public abstract class Transport implements AutoCloseable { * @param proto * the exact object previously given to register. */ + @SuppressWarnings("ModifyCollectionInEnhancedForLoop") public static void unregister(TransportProtocol proto) { for (WeakReference<TransportProtocol> ref : protocols) { TransportProtocol refProto = ref.get(); - if (refProto == null || refProto == proto) + if (refProto == null || refProto == proto) { protocols.remove(ref); + } } } @@ -204,15 +205,17 @@ public abstract class Transport implements AutoCloseable { * * @return an immutable copy of the currently registered protocols. */ + @SuppressWarnings("ModifyCollectionInEnhancedForLoop") public static List<TransportProtocol> getTransportProtocols() { int cnt = protocols.size(); List<TransportProtocol> res = new ArrayList<>(cnt); for (WeakReference<TransportProtocol> ref : protocols) { TransportProtocol proto = ref.get(); - if (proto != null) + if (proto != null) { res.add(proto); - else + } else { protocols.remove(ref); + } } return Collections.unmodifiableList(res); } @@ -508,6 +511,7 @@ public abstract class Transport implements AutoCloseable { * @throws org.eclipse.jgit.errors.TransportException * the transport cannot open this URI. */ + @SuppressWarnings("ModifyCollectionInEnhancedForLoop") public static Transport open(Repository local, URIish uri, String remoteName) throws NotSupportedException, TransportException { for (WeakReference<TransportProtocol> ref : protocols) { @@ -533,11 +537,15 @@ public abstract class Transport implements AutoCloseable { * Note that the resulting transport instance can not be used for fetching * or pushing, but only for reading remote refs. * - * @param uri a {@link org.eclipse.jgit.transport.URIish} object. + * @param uri + * a {@link org.eclipse.jgit.transport.URIish} object. * @return new Transport instance * @throws org.eclipse.jgit.errors.NotSupportedException + * case that is not supported by JGit * @throws org.eclipse.jgit.errors.TransportException + * if transport failed */ + @SuppressWarnings("ModifyCollectionInEnhancedForLoop") public static Transport open(URIish uri) throws NotSupportedException, TransportException { for (WeakReference<TransportProtocol> ref : protocols) { TransportProtocol proto = ref.get(); @@ -546,8 +554,9 @@ public abstract class Transport implements AutoCloseable { continue; } - if (proto.canHandle(uri, null, null)) + if (proto.canHandle(uri, null, null)) { return proto.open(uri); + } } throw new NotSupportedException(MessageFormat.format(JGitText.get().URINotSupported, uri)); @@ -584,7 +593,7 @@ public abstract class Transport implements AutoCloseable { Collection<RefSpec> fetchSpecs) throws IOException { if (fetchSpecs == null) fetchSpecs = Collections.emptyList(); - final List<RemoteRefUpdate> result = new LinkedList<>(); + final List<RemoteRefUpdate> result = new ArrayList<>(); final Collection<RefSpec> procRefs = expandPushWildcardsFor(db, specs); for (RefSpec spec : procRefs) { @@ -1112,28 +1121,8 @@ public abstract class Transport implements AutoCloseable { } /** - * @return the blob limit value set with {@link #setFilterBlobLimit} or - * {@link #setFilterSpec(FilterSpec)}, or -1 if no blob limit value - * was set - * @since 5.0 - * @deprecated Use {@link #getFilterSpec()} instead - */ - @Deprecated - public final long getFilterBlobLimit() { - return filterSpec.getBlobLimit(); - } - - /** - * @param bytes exclude blobs of size greater than this - * @since 5.0 - * @deprecated Use {@link #setFilterSpec(FilterSpec)} instead - */ - @Deprecated - public final void setFilterBlobLimit(long bytes) { - setFilterSpec(FilterSpec.withBlobLimit(bytes)); - } - - /** + * Get filter spec + * * @return the last filter spec set with {@link #setFilterSpec(FilterSpec)}, * or {@link FilterSpec#NO_FILTER} if it was never invoked. * @since 5.4 @@ -1143,7 +1132,10 @@ public abstract class Transport implements AutoCloseable { } /** - * @param filter a new filter to use for this transport + * Set filter spec + * + * @param filter + * a new filter to use for this transport * @since 5.4 */ public final void setFilterSpec(@NonNull FilterSpec filter) { @@ -1192,6 +1184,8 @@ public abstract class Transport implements AutoCloseable { } /** + * Get deepen-since + * * @return the deepen-since for a shallow clone * @since 6.3 */ @@ -1210,7 +1204,9 @@ public abstract class Transport implements AutoCloseable { } /** - * @return the deepen-not for a shallow clone + * Get list of deepen-not + * + * @return the list of deepen-not for a shallow clone * @since 6.3 */ public final List<String> getDeepenNots() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java index 784f566159..58232a7dde 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java @@ -170,7 +170,6 @@ public class TransportAmazonS3 extends HttpTransport implements WalkTransport { } } - /** {@inheritDoc} */ @Override public FetchConnection openFetch() throws TransportException { final DatabaseS3 c = new DatabaseS3(bucket, keyPrefix + "/objects"); //$NON-NLS-1$ @@ -179,7 +178,6 @@ public class TransportAmazonS3 extends HttpTransport implements WalkTransport { return r; } - /** {@inheritDoc} */ @Override public PushConnection openPush() throws TransportException { final DatabaseS3 c = new DatabaseS3(bucket, keyPrefix + "/objects"); //$NON-NLS-1$ @@ -188,7 +186,6 @@ public class TransportAmazonS3 extends HttpTransport implements WalkTransport { return r; } - /** {@inheritDoc} */ @Override public void close() { // No explicit connections are maintained. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java index 04ebddb107..0f84f7e21e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java @@ -105,7 +105,6 @@ class TransportBundleFile extends Transport implements TransportBundle { bundle = bundlePath; } - /** {@inheritDoc} */ @Override public FetchConnection openFetch() throws NotSupportedException, TransportException { @@ -121,14 +120,12 @@ class TransportBundleFile extends Transport implements TransportBundle { return new BundleFetchConnection(this, src); } - /** {@inheritDoc} */ @Override public PushConnection openPush() throws NotSupportedException { throw new NotSupportedException( JGitText.get().pushIsNotSupportedForBundleTransport); } - /** {@inheritDoc} */ @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 index eac9208a3a..0366bf3aa2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleStream.java @@ -57,7 +57,6 @@ public class TransportBundleStream extends Transport implements TransportBundle src = in; } - /** {@inheritDoc} */ @Override public FetchConnection openFetch() throws TransportException { if (src == null) @@ -69,14 +68,12 @@ public class TransportBundleStream extends Transport implements TransportBundle } } - /** {@inheritDoc} */ @Override public PushConnection openPush() throws NotSupportedException { throw new NotSupportedException( JGitText.get().pushIsNotSupportedForBundleTransport); } - /** {@inheritDoc} */ @Override public void close() { if (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 index a1914b6182..43b3f6cce0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java @@ -89,7 +89,6 @@ class TransportGitAnon extends TcpTransport implements PackTransport { super(uri); } - /** {@inheritDoc} */ @Override public FetchConnection openFetch() throws TransportException { return new TcpFetchConnection(); @@ -102,13 +101,11 @@ class TransportGitAnon extends TcpTransport implements PackTransport { return new TcpFetchConnection(refSpecs, additionalPatterns); } - /** {@inheritDoc} */ @Override public PushConnection openPush() throws TransportException { return new TcpPushConnection(); } - /** {@inheritDoc} */ @Override public void close() { // Resources must be established per-connection. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java index 19ed4fbcc1..f77b04110d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java @@ -140,7 +140,6 @@ public class TransportGitSsh extends SshTransport implements PackTransport { } } - /** {@inheritDoc} */ @Override public FetchConnection openFetch() throws TransportException { return new SshFetchConnection(); @@ -153,7 +152,6 @@ public class TransportGitSsh extends SshTransport implements PackTransport { return new SshFetchConnection(refSpecs, additionalPatterns); } - /** {@inheritDoc} */ @Override public PushConnection openPush() throws TransportException { return new SshPushConnection(); @@ -162,7 +160,7 @@ public class TransportGitSsh extends SshTransport implements PackTransport { String commandFor(String exe) { String path = uri.getPath(); if (uri.getScheme() != null && uri.getPath().startsWith("/~")) //$NON-NLS-1$ - path = (uri.getPath().substring(1)); + path = uri.getPath().substring(1); final StringBuilder cmd = new StringBuilder(); cmd.append(exe); @@ -256,6 +254,12 @@ public class TransportGitSsh extends SshTransport implements PackTransport { pb.environment().put(Constants.GIT_DIR_KEY, directory.getPath()); } + File commonDirectory = local != null ? local.getCommonDirectory() + : null; + if (commonDirectory != null) { + pb.environment().put(Constants.GIT_COMMON_DIR_KEY, + commonDirectory.getPath()); + } return pb; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index 405373a0f9..c9a48bf0e3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -66,7 +66,6 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -95,7 +94,6 @@ import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.lib.SymbolicRef; -import org.eclipse.jgit.transport.HttpAuthMethod.Type; import org.eclipse.jgit.transport.HttpConfig.HttpRedirectMode; import org.eclipse.jgit.transport.http.HttpConnection; import org.eclipse.jgit.transport.http.HttpConnectionFactory; @@ -108,6 +106,7 @@ import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.io.DisabledOutputStream; +import org.eclipse.jgit.util.io.SilentInputStream; import org.eclipse.jgit.util.io.UnionInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -311,6 +310,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, * @param uri * a {@link org.eclipse.jgit.transport.URIish} object. * @throws org.eclipse.jgit.errors.NotSupportedException + * if URI is not supported by JGit * @since 4.9 */ protected void setURI(URIish uri) throws NotSupportedException { @@ -327,7 +327,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport, * Create a minimal HTTP transport with default configuration values. * * @param uri + * URI to create a HTTP transport for * @throws NotSupportedException + * if URI is not supported by JGit */ TransportHttp(URIish uri) throws NotSupportedException { super(uri); @@ -445,7 +447,6 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } } - /** {@inheritDoc} */ @Override public FetchConnection openFetch() throws TransportException, NotSupportedException { @@ -532,7 +533,6 @@ public class TransportHttp extends HttpTransport implements WalkTransport, return new BufferedReader(new InputStreamReader(in, UTF_8)); } - /** {@inheritDoc} */ @Override public PushConnection openPush() throws NotSupportedException, TransportException { @@ -567,7 +567,6 @@ public class TransportHttp extends HttpTransport implements WalkTransport, return p; } - /** {@inheritDoc} */ @Override public void close() { if (gitSession != null) { @@ -588,6 +587,17 @@ public class TransportHttp extends HttpTransport implements WalkTransport, this.headers = headers; } + /** + * Get additional headers on the HTTP connection + * + * @return unmodifiable map of additional name:values that are set as + * headers on the HTTP connection + * @since 6.6 + */ + public Map<String, String> getAdditionalHeaders() { + return Collections.unmodifiableMap(headers); + } + private NoRemoteRepositoryException createNotFoundException(URIish u, URL url, String msg) { String text; @@ -636,7 +646,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } int authAttempts = 1; int redirects = 0; - Collection<Type> ignoreTypes = null; + Collection<HttpAuthMethod.Type> ignoreTypes = null; for (;;) { try { final HttpConnection conn = httpOpen(METHOD_GET, u, AcceptEncoding.GZIP); @@ -753,7 +763,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, void processResponseCookies(HttpConnection conn) { if (cookieFile != null && http.getSaveCookies()) { - List<HttpCookie> foundCookies = new LinkedList<>(); + List<HttpCookie> foundCookies = new ArrayList<>(); List<String> cookieHeaderValues = conn .getHeaderFields(HDR_SET_COOKIE); @@ -785,7 +795,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, private List<HttpCookie> extractCookies(String headerKey, List<String> headerValues) { - List<HttpCookie> foundCookies = new LinkedList<>(); + List<HttpCookie> foundCookies = new ArrayList<>(); for (String headerValue : headerValues) { foundCookies .addAll(HttpCookie.parse(headerKey + ':' + headerValue)); @@ -811,7 +821,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, /** * Trust the server for all git operations from this repository; may be * {@code null} if the transport was created via - * {@link #TransportHttp(URIish)}. + * {@link TransportHttp#TransportHttp(URIish)}. */ CredentialItem.YesNoType forRepo; @@ -1024,11 +1034,15 @@ public class TransportHttp extends HttpTransport implements WalkTransport, /** * Open an HTTP connection. * - * @param method HTTP request method - * @param u url of the HTTP connection - * @param acceptEncoding accept-encoding header option + * @param method + * HTTP request method + * @param u + * url of the HTTP connection + * @param acceptEncoding + * accept-encoding header option * @return the HTTP connection * @throws java.io.IOException + * if an IO error occurred * @since 4.6 */ protected HttpConnection httpOpen(String method, URL u, @@ -1545,7 +1559,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport, throws TransportException { svc = new MultiRequestService(SVC_UPLOAD_PACK, getProtocolVersion()); - try (InputStream svcIn = svc.getInputStream(); + try (InputStream svcIn = new SilentInputStream( + svc.getInputStream()); OutputStream svcOut = svc.getOutputStream()) { init(svcIn, svcOut); super.doFetch(monitor, want, have, outputStream); @@ -1651,7 +1666,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } HttpAuthMethod authenticator = null; - Collection<Type> ignoreTypes = EnumSet.noneOf(Type.class); + Collection<HttpAuthMethod.Type> ignoreTypes = EnumSet + .noneOf(HttpAuthMethod.Type.class); // Counts number of repeated authentication attempts using the same // authentication scheme int authAttempts = 1; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java index 77d1419ea2..1b9431ce6e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java @@ -151,7 +151,6 @@ class TransportLocal extends Transport implements PackTransport { } } - /** {@inheritDoc} */ @Override public FetchConnection openFetch() throws TransportException { return openFetch(Collections.emptyList()); @@ -170,7 +169,6 @@ class TransportLocal extends Transport implements PackTransport { return new InternalFetchConnection<>(this, upf, null, openRepo()); } - /** {@inheritDoc} */ @Override public PushConnection openPush() throws TransportException { final String rp = getOptionReceivePack(); @@ -183,7 +181,6 @@ class TransportLocal extends Transport implements PackTransport { return new InternalPushConnection<>(this, rpf, null, openRepo()); } - /** {@inheritDoc} */ @Override public void close() { // Resources must be established per-connection. @@ -228,6 +225,7 @@ class TransportLocal extends Transport implements PackTransport { env.remove("GIT_CONFIG"); //$NON-NLS-1$ env.remove("GIT_CONFIG_PARAMETERS"); //$NON-NLS-1$ env.remove("GIT_DIR"); //$NON-NLS-1$ + env.remove("GIT_COMMON_DIR"); //$NON-NLS-1$ env.remove("GIT_WORK_TREE"); //$NON-NLS-1$ env.remove("GIT_GRAFT_FILE"); //$NON-NLS-1$ env.remove("GIT_INDEX_FILE"); //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java index 912a90a1bf..bfb840d75f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java @@ -251,7 +251,9 @@ public abstract class TransportProtocol { * a {@link org.eclipse.jgit.transport.URIish} object. * @return new Transport * @throws org.eclipse.jgit.errors.NotSupportedException + * this protocol does not support the URI. * @throws org.eclipse.jgit.errors.TransportException + * the transport cannot open this URI. */ public Transport open(URIish uri) throws NotSupportedException, TransportException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java index b9feeb9c02..a59d352e0d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java @@ -104,7 +104,6 @@ public class TransportSftp extends SshTransport implements WalkTransport { super(local, uri); } - /** {@inheritDoc} */ @Override public FetchConnection openFetch() throws TransportException { final SftpObjectDB c = new SftpObjectDB(uri.getPath()); @@ -113,7 +112,6 @@ public class TransportSftp extends SshTransport implements WalkTransport { return r; } - /** {@inheritDoc} */ @Override public PushConnection openPush() throws TransportException { final SftpObjectDB c = new SftpObjectDB(uri.getPath()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java index c9bb89a436..7b5842b712 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java @@ -82,7 +82,7 @@ public class URIish implements Serializable { * Part of a pattern which matches a relative path. Relative paths don't * start with slash or drive letters. Defines no capturing group. */ - private static final String RELATIVE_PATH_P = "(?:(?:[^\\\\/]+[\\\\/]+)*[^\\\\/]+[\\\\/]*)"; //$NON-NLS-1$ + private static final String RELATIVE_PATH_P = "(?:(?:[^\\\\/]+[\\\\/]+)*+[^\\\\/]*)"; //$NON-NLS-1$ /** * Part of a pattern which matches a relative or absolute path. Defines no @@ -94,7 +94,7 @@ public class URIish implements Serializable { private static final long serialVersionUID = 1L; /** - * A pattern matching standard URI: </br> + * A pattern matching standard URI: <br> * <code>scheme "://" user_password? hostname? portnumber? path</code> */ private static final Pattern FULL_URI = Pattern.compile("^" // //$NON-NLS-1$ @@ -120,7 +120,7 @@ public class URIish implements Serializable { * path (maybe even containing windows drive-letters) or a relative path. */ private static final Pattern LOCAL_FILE = Pattern.compile("^" // //$NON-NLS-1$ - + "([\\\\/]?" + PATH_P + ")" // //$NON-NLS-1$ //$NON-NLS-2$ + + "([\\\\/]?+" + PATH_P + ")" // //$NON-NLS-1$ //$NON-NLS-2$ + "$"); //$NON-NLS-1$ /** @@ -175,6 +175,7 @@ public class URIish implements Serializable { * @param s * a {@link java.lang.String} object. * @throws java.net.URISyntaxException + * if {@code s} was null or couldn't be parsed */ public URIish(String s) throws URISyntaxException { if (StringUtils.isEmptyOrNull(s)) { @@ -477,6 +478,7 @@ public class URIish implements Serializable { * the new value for path. * @return a new URI with the updated value. * @throws java.net.URISyntaxException + * if URI couldn't be parsed from String */ public URIish setRawPath(String n) throws URISyntaxException { final URIish r = new URIish(this); @@ -551,7 +553,6 @@ public class URIish implements Serializable { return r; } - /** {@inheritDoc} */ @Override public int hashCode() { int hc = 0; @@ -570,7 +571,6 @@ public class URIish implements Serializable { return hc; } - /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (!(obj instanceof URIish)) @@ -611,7 +611,6 @@ public class URIish implements Serializable { return format(true, false); } - /** {@inheritDoc} */ @Override public String toString() { return format(false, false); @@ -683,7 +682,8 @@ public class URIish implements Serializable { /** * Get the "humanish" part of the path. Some examples of a 'humanish' part * for a full path: - * <table summary="path vs humanish path" border="1"> + * <table border="1"> + * <caption>path vs. humanish path</caption> * <tr> * <th>Path</th> * <th>Humanish part</th> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java index f245eae39f..41ab8acf05 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -30,11 +30,11 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_MULTI_ACK_D import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_NO_DONE; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_NO_PROGRESS; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_OFS_DELTA; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SESSION_ID; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SHALLOW; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDEBAND_ALL; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND_64K; -import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SESSION_ID; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_THIN_PACK; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_WAIT_FOR_DONE; import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ACK; @@ -80,7 +80,6 @@ import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.CachedPackUriProvider; import org.eclipse.jgit.internal.storage.pack.PackWriter; -import org.eclipse.jgit.internal.transport.parser.FirstWant; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; @@ -118,13 +117,13 @@ public class UploadPack implements Closeable { /** Policy the server uses to validate client requests */ public enum RequestPolicy { /** Client may only ask for objects the server advertised a reference for. */ - ADVERTISED, + ADVERTISED(0x08), /** * Client may ask for any commit reachable from a reference advertised by * the server. */ - REACHABLE_COMMIT, + REACHABLE_COMMIT(0x02), /** * Client may ask for objects that are the tip of any reference, even if not @@ -134,18 +133,36 @@ public class UploadPack implements Closeable { * * @since 3.1 */ - TIP, + TIP(0x01), /** * Client may ask for any commit reachable from any reference, even if that - * reference wasn't advertised. + * reference wasn't advertised, implies REACHABLE_COMMIT and TIP. * * @since 3.1 */ - REACHABLE_COMMIT_TIP, + REACHABLE_COMMIT_TIP(0x03), + + /** Client may ask for any SHA-1 in the repository, implies REACHABLE_COMMIT_TIP. */ + ANY(0x07); + + private final int bitmask; - /** Client may ask for any SHA-1 in the repository. */ - ANY; + RequestPolicy(int bitmask) { + this.bitmask = bitmask; + } + + /** + * Check if the current policy implies another, based on its bitmask. + * + * @param implied + * the implied policy based on its bitmask. + * @return true if the policy is implied. + * @since 6.10.1 + */ + public boolean implies(RequestPolicy implied) { + return (bitmask & implied.bitmask) != 0; + } } /** @@ -172,44 +189,6 @@ public class UploadPack implements Closeable { throws PackProtocolException, IOException; } - /** - * Data in the first line of a want-list, the line itself plus options. - * - * @deprecated Use {@link FirstWant} instead - */ - @Deprecated - public static class FirstLine { - - private final FirstWant firstWant; - - /** - * @param line - * line from the client. - */ - public FirstLine(String line) { - try { - firstWant = FirstWant.fromLine(line); - } catch (PackProtocolException e) { - throw new UncheckedIOException(e); - } - } - - /** @return non-capabilities part of the line. */ - public String getLine() { - return firstWant.getLine(); - } - - /** @return capabilities parsed from the line. */ - public Set<String> getOptions() { - if (firstWant.getAgent() != null) { - Set<String> caps = new HashSet<>(firstWant.getCapabilities()); - caps.add(OPTION_AGENT + '=' + firstWant.getAgent()); - return caps; - } - return firstWant.getCapabilities(); - } - } - /* * {@link java.util.function.Consumer} doesn't allow throwing checked * exceptions. Define our own to propagate IOExceptions. @@ -732,8 +711,11 @@ public class UploadPack implements Closeable { } /** - * @param p provider of URIs corresponding to cached packs (to support - * the packfile URIs feature) + * Set provider of cached pack URIs + * + * @param p + * provider of URIs corresponding to cached packs (to support the + * packfile URIs feature) * @since 5.5 */ public void setCachedPackUriProvider(@Nullable CachedPackUriProvider p) { @@ -771,9 +753,13 @@ public class UploadPack implements Closeable { * its own error handling mechanism. * * @param input + * input stream * @param output + * output stream * @param messages + * stream for messages * @throws java.io.IOException + * if an IO error occurred */ public void upload(InputStream input, OutputStream output, @Nullable OutputStream messages) throws IOException { @@ -1175,6 +1161,11 @@ public class UploadPack implements Closeable { } private void fetchV2(PacketLineOut pckOut) throws IOException { + ProtocolV2Parser parser = new ProtocolV2Parser(transferConfig); + FetchV2Request req = parser.parseFetchRequest(pckIn); + currentRequest = req; + Map<String, ObjectId> wantedRefs = wantedRefs(req); + // Depending on the requestValidator, #processHaveLines may // require that advertised be set. Set it only in the required // circumstances (to avoid a full ref lookup in the case that @@ -1184,15 +1175,25 @@ public class UploadPack implements Closeable { requestValidator instanceof AnyRequestValidator) { advertised = Collections.emptySet(); } else { - advertised = refIdSet(getAdvertisedOrDefaultRefs().values()); + if (req.wantIds.isEmpty()) { + // Only refs-in-wants in request. These ref-in-wants where used as + // filters already in the ls-refs, there is no need to use a full + // advertisement now in fetch. This improves performance and also + // accuracy: when the ref db prioritize and truncates the returned + // refs (e.g. Gerrit hides too old refs), applying a filter can + // return different results than a plain listing. + advertised = refIdSet(getFilteredRefs(wantedRefs.keySet()).values()); + } else { + // At least one SHA1 in wants, so we need to take the full + // advertisement as base for a reachability check. + advertised = refIdSet(getAdvertisedOrDefaultRefs().values()); + } } PackStatistics.Accumulator accumulator = new PackStatistics.Accumulator(); Instant negotiateStart = Instant.now(); + accumulator.advertised = advertised.size(); - ProtocolV2Parser parser = new ProtocolV2Parser(transferConfig); - FetchV2Request req = parser.parseFetchRequest(pckIn); - currentRequest = req; rawOut.stopBuffering(); protocolV2Hook.onFetch(req); @@ -1205,10 +1206,10 @@ public class UploadPack implements Closeable { // copying data back to class fields List<ObjectId> deepenNots = parseDeepenNots(req.getDeepenNots()); - Map<String, ObjectId> wantedRefs = wantedRefs(req); // TODO(ifrade): Avoid mutating the parsed request. req.getWantIds().addAll(wantedRefs.values()); wantIds = req.getWantIds(); + accumulator.wants = wantIds.size(); boolean sectionSent = false; boolean mayHaveShallow = req.getDepth() != 0 @@ -1393,6 +1394,7 @@ public class UploadPack implements Closeable { if (transferConfig.isAdvertiseObjectInfo()) { caps.add(COMMAND_OBJECT_INFO); } + caps.add(OPTION_AGENT + "=" + UserAgent.get()); return caps; } @@ -1599,13 +1601,9 @@ public class UploadPack implements Closeable { if (!biDirectionalPipe) adv.advertiseCapability(OPTION_NO_DONE); RequestPolicy policy = getRequestPolicy(); - if (policy == RequestPolicy.TIP - || policy == RequestPolicy.REACHABLE_COMMIT_TIP - || policy == null) + if (policy == null || policy.implies(RequestPolicy.TIP)) adv.advertiseCapability(OPTION_ALLOW_TIP_SHA1_IN_WANT); - if (policy == RequestPolicy.REACHABLE_COMMIT - || policy == RequestPolicy.REACHABLE_COMMIT_TIP - || policy == null) + if (policy == null || policy.implies(RequestPolicy.REACHABLE_COMMIT)) adv.advertiseCapability(OPTION_ALLOW_REACHABLE_SHA1_IN_WANT); adv.advertiseCapability(OPTION_AGENT, UserAgent.get()); if (transferConfig.isAllowFilter()) { @@ -1663,18 +1661,6 @@ public class UploadPack implements Closeable { } /** - * Deprecated synonym for {@code getFilterSpec().getBlobLimit()}. - * - * @return filter blob limit requested by the client, or -1 if no limit - * @since 5.3 - * @deprecated Use {@link #getFilterSpec()} instead - */ - @Deprecated - public final long getFilterBlobLimit() { - return getFilterSpec().getBlobLimit(); - } - - /** * Returns the filter spec for the current request. Valid only after * calling recvWants(). This may be a no-op filter spec, but it won't be * null. @@ -1766,7 +1752,6 @@ public class UploadPack implements Closeable { && line.length() == PACKET_HAVE.length() + 40) { peerHas.add(ObjectId .fromString(line.substring(PACKET_HAVE.length()))); - accumulator.haves++; } else if (line.equals(PACKET_DONE)) { last = processHaveLines(peerHas, last, pckOut, accumulator, Option.NONE); @@ -1798,6 +1783,7 @@ public class UploadPack implements Closeable { parseWants(accumulator); if (peerHas.isEmpty()) return last; + accumulator.haves += peerHas.size(); sentReady = false; int haveCnt = 0; @@ -1966,10 +1952,9 @@ public class UploadPack implements Closeable { @Override public void checkWants(UploadPack up, List<ObjectId> wants) throws PackProtocolException, IOException { - if (!up.isBiDirectionalPipe()) + if (!up.isBiDirectionalPipe() || !wants.isEmpty()) { new ReachableCommitRequestValidator().checkWants(up, wants); - else if (!wants.isEmpty()) - throw new WantNotValidException(wants.iterator().next()); + } } } @@ -2241,7 +2226,7 @@ public class UploadPack implements Closeable { walk.resetRetain(SAVE); walk.markStart((RevCommit) want); if (oldestTime != 0) - walk.setRevFilter(CommitTimeRevFilter.after(oldestTime * 1000L)); + walk.setRevFilter(CommitTimeRevFilter.after(Instant.ofEpochSecond(oldestTime))); for (;;) { final RevCommit c = walk.next(); if (c == null) @@ -2264,7 +2249,8 @@ public class UploadPack implements Closeable { * request in process * @param allTags * refs to search for annotated tags to include in the pack if - * the {@link #OPTION_INCLUDE_TAG} capability was requested. + * the {@link GitProtocolConstants#OPTION_INCLUDE_TAG} capability + * was requested. * @param unshallowCommits * shallow commits on the client that are now becoming unshallow * @param deepenNots @@ -2325,7 +2311,8 @@ public class UploadPack implements Closeable { * where to write statistics about the content of the pack. * @param allTags * refs to search for annotated tags to include in the pack if - * the {@link #OPTION_INCLUDE_TAG} capability was requested. + * the {@link GitProtocolConstants#OPTION_INCLUDE_TAG} capability + * was requested. * @param unshallowCommits * shallow commits on the client that are now becoming unshallow * @param deepenNots @@ -2345,11 +2332,6 @@ public class UploadPack implements Closeable { } msgOut.flush(); - // Advertised objects and refs are not used from here on and can be - // cleared. - advertised = null; - refs = null; - PackConfig cfg = packConfig; if (cfg == null) cfg = new PackConfig(db); @@ -2391,13 +2373,19 @@ public class UploadPack implements Closeable { pw.setTagTargets(tagTargets); } + // Advertised objects and refs are not used from here on and can be + // cleared. + advertised = null; + refs = null; + RevWalk rw = walk; if (req.getDepth() > 0 || req.getDeepenSince() != 0 || !deepenNots.isEmpty()) { int walkDepth = req.getDepth() == 0 ? Integer.MAX_VALUE : req.getDepth() - 1; pw.setShallowPack(req.getDepth(), unshallowCommits); - // Ownership is transferred below + // dw borrows the reader from walk which is closed by #close + @SuppressWarnings("resource") DepthWalk.RevWalk dw = new DepthWalk.RevWalk( walk.getObjectReader(), walkDepth); dw.setDeepenSince(req.getDeepenSince()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java index df98d0cfd5..b23ee97dcb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java @@ -10,10 +10,6 @@ package org.eclipse.jgit.transport; -import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; - -import java.util.Set; - import org.eclipse.jgit.util.StringUtils; /** @@ -91,40 +87,6 @@ public class UserAgent { userAgent = StringUtils.isEmptyOrNull(agent) ? null : clean(agent); } - /** - * - * @param options - * @param transportAgent - * @return The transport agent. - * @deprecated Capabilities with <key>=<value> shape are now parsed - * alongside other capabilities in the ReceivePack flow. - */ - @Deprecated - static String getAgent(Set<String> options, String transportAgent) { - if (options == null || options.isEmpty()) { - return transportAgent; - } - for (String o : options) { - if (o.startsWith(OPTION_AGENT) - && o.length() > OPTION_AGENT.length() - && o.charAt(OPTION_AGENT.length()) == '=') { - return o.substring(OPTION_AGENT.length() + 1); - } - } - return transportAgent; - } - - /** - * - * @param options - * @return True if the transport agent is set. False otherwise. - * @deprecated Capabilities with <key>=<value> shape are now parsed - * alongside other capabilities in the ReceivePack flow. - */ - @Deprecated - static boolean hasAgent(Set<String> options) { - return getAgent(options, null) != null; - } private UserAgent() { } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java index c0de42cb57..da8af5a0fe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java @@ -48,13 +48,11 @@ public class UsernamePasswordCredentialsProvider extends CredentialsProvider { this.password = password; } - /** {@inheritDoc} */ @Override public boolean isInteractive() { return false; } - /** {@inheritDoc} */ @Override public boolean supports(CredentialItem... items) { for (CredentialItem i : items) { @@ -77,7 +75,6 @@ public class UsernamePasswordCredentialsProvider extends CredentialsProvider { return true; } - /** {@inheritDoc} */ @Override public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java index c8cdb5a549..3bfc5234ee 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java @@ -130,6 +130,7 @@ abstract class WalkEncryption { * EncryptionUtil.java</a> * <p> * Note: EncryptionUtil is inadequate: + * <ul> * <li>EncryptionUtil.isCipherAvailableForUse checks encryption only which * "always works", but in JetS3t both encryption and decryption use non-IV * aware algorithm parameters for all PBE specs, which breaks in case of AES @@ -137,6 +138,7 @@ abstract class WalkEncryption { * JetS3t, such as PBEWithMD5AndDES and PBEWithSHAAndTwofish-CBC * <li>any AES based algorithms such as "PBE...With...And...AES" will not * work, since they need proper IV setup + * </ul> */ static class JetS3tV2 extends WalkEncryption { @@ -516,8 +518,10 @@ abstract class WalkEncryption { * Encryption factory. * * @param props - * @return instance + * configuration properties + * @return instance this object * @throws GeneralSecurityException + * if generic security failure occurred */ static WalkEncryption instance(Properties props) throws GeneralSecurityException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java index ed8f450c53..b7bb0cbce3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java @@ -16,13 +16,16 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.text.MessageFormat; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.eclipse.jgit.errors.CompoundException; @@ -112,16 +115,16 @@ class WalkFetchConnection extends BaseFetchConnection { private final DateRevQueue localCommitQueue; /** Objects we need to copy from the remote repository. */ - private LinkedList<ObjectId> workQueue; + private Deque<ObjectId> workQueue; /** Databases we have not yet obtained the list of packs from. */ - private final LinkedList<WalkRemoteObjectDatabase> noPacksYet; + private final Deque<WalkRemoteObjectDatabase> noPacksYet; /** Databases we have not yet obtained the alternates from. */ - private final LinkedList<WalkRemoteObjectDatabase> noAlternatesYet; + private final Deque<WalkRemoteObjectDatabase> noAlternatesYet; /** Packs we have discovered, but have not yet fetched locally. */ - private final LinkedList<RemotePack> unfetchedPacks; + private final Map<String, RemotePack> unfetchedPacks; /** * Packs whose indexes we have looked at in {@link #unfetchedPacks}. @@ -163,13 +166,13 @@ class WalkFetchConnection extends BaseFetchConnection { remotes = new ArrayList<>(); remotes.add(w); - unfetchedPacks = new LinkedList<>(); + unfetchedPacks = new LinkedHashMap<>(); packsConsidered = new HashSet<>(); - noPacksYet = new LinkedList<>(); + noPacksYet = new ArrayDeque<>(); noPacksYet.add(w); - noAlternatesYet = new LinkedList<>(); + noAlternatesYet = new ArrayDeque<>(); noAlternatesYet.add(w); fetchErrors = new HashMap<>(); @@ -183,16 +186,14 @@ class WalkFetchConnection extends BaseFetchConnection { LOCALLY_SEEN = revWalk.newFlag("LOCALLY_SEEN"); //$NON-NLS-1$ localCommitQueue = new DateRevQueue(); - workQueue = new LinkedList<>(); + workQueue = new ArrayDeque<>(); } - /** {@inheritDoc} */ @Override public boolean didFetchTestConnectivity() { return true; } - /** {@inheritDoc} */ @Override protected void doFetch(final ProgressMonitor monitor, final Collection<Ref> want, final Set<ObjectId> have) @@ -214,24 +215,21 @@ class WalkFetchConnection extends BaseFetchConnection { } } - /** {@inheritDoc} */ @Override public Collection<PackLock> getPackLocks() { return packLocks; } - /** {@inheritDoc} */ @Override public void setPackLockMessage(String message) { lockMessage = message; } - /** {@inheritDoc} */ @Override public void close() { inserter.close(); reader.close(); - for (RemotePack p : unfetchedPacks) { + for (RemotePack p : unfetchedPacks.values()) { if (p.tmpIdx != null) p.tmpIdx.delete(); } @@ -426,8 +424,9 @@ class WalkFetchConnection extends BaseFetchConnection { if (packNameList == null || packNameList.isEmpty()) continue; for (String packName : packNameList) { - if (packsConsidered.add(packName)) - unfetchedPacks.add(new RemotePack(wrr, packName)); + if (packsConsidered.add(packName)) { + unfetchedPacks.put(packName, new RemotePack(wrr, packName)); + } } if (downloadPackedObject(pm, id)) return; @@ -470,15 +469,27 @@ class WalkFetchConnection extends BaseFetchConnection { } } + private boolean downloadPackedObject(ProgressMonitor monitor, + AnyObjectId id) throws TransportException { + Set<String> brokenPacks = new HashSet<>(); + try { + return downloadPackedObject(monitor, id, brokenPacks); + } finally { + brokenPacks.forEach(unfetchedPacks::remove); + } + } + @SuppressWarnings("Finally") private boolean downloadPackedObject(final ProgressMonitor monitor, - final AnyObjectId id) throws TransportException { + final AnyObjectId id, Set<String> brokenPacks) 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(); + for (Entry<String, RemotePack> entry : unfetchedPacks.entrySet()) { + if (monitor.isCancelled()) { + break; + } + final RemotePack pack = entry.getValue(); try { pack.openIndex(monitor); } catch (IOException err) { @@ -488,7 +499,7 @@ class WalkFetchConnection extends BaseFetchConnection { // another source, so don't consider it a failure. // recordError(id, err); - packItr.remove(); + brokenPacks.add(entry.getKey()); continue; } @@ -530,15 +541,16 @@ class WalkFetchConnection extends BaseFetchConnection { // are unusable and we shouldn't consult them again. // try { - if (pack.tmpIdx != null) + if (pack.tmpIdx != null) { FileUtils.delete(pack.tmpIdx); - } catch (IOException e) { + } + } catch (Throwable e) { if (e1 != null) { e.addSuppressed(e1); } throw new TransportException(e.getMessage(), e); } - packItr.remove(); + brokenPacks.add(entry.getKey()); } if (!alreadyHave(id)) { @@ -553,11 +565,9 @@ class WalkFetchConnection extends BaseFetchConnection { // Complete any other objects that we can. // - final Iterator<ObjectId> pending = swapFetchQueue(); - while (pending.hasNext()) { - final ObjectId p = pending.next(); + final Deque<ObjectId> pending = swapFetchQueue(); + for (ObjectId p : pending) { if (pack.index.hasObject(p)) { - pending.remove(); process(p); } else { workQueue.add(p); @@ -569,9 +579,9 @@ class WalkFetchConnection extends BaseFetchConnection { return false; } - private Iterator<ObjectId> swapFetchQueue() { - final Iterator<ObjectId> r = workQueue.iterator(); - workQueue = new LinkedList<>(); + private Deque<ObjectId> swapFetchQueue() { + final Deque<ObjectId> r = workQueue; + workQueue = new ArrayDeque<>(); return r; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java index a54fd8e14d..464017a84d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java @@ -104,7 +104,6 @@ class WalkPushConnection extends BaseConnection implements PushConnection { dest = w; } - /** {@inheritDoc} */ @Override public void push(final ProgressMonitor monitor, final Map<String, RemoteRefUpdate> refUpdates) @@ -112,7 +111,6 @@ class WalkPushConnection extends BaseConnection implements PushConnection { push(monitor, refUpdates, null); } - /** {@inheritDoc} */ @Override public void push(final ProgressMonitor monitor, final Map<String, RemoteRefUpdate> refUpdates, OutputStream out) @@ -184,7 +182,6 @@ class WalkPushConnection extends BaseConnection implements PushConnection { } } - /** {@inheritDoc} */ @Override public void close() { dest.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java index 98c231a46d..95b8221a8b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java @@ -36,29 +36,39 @@ import org.eclipse.jgit.annotations.NonNull; */ public interface HttpConnection { /** + * HttpURLConnection#HTTP_OK + * * @see HttpURLConnection#HTTP_OK */ int HTTP_OK = java.net.HttpURLConnection.HTTP_OK; /** + * HttpURLConnection#HTTP_NOT_AUTHORITATIVE + * * @see HttpURLConnection#HTTP_NOT_AUTHORITATIVE * @since 5.8 */ int HTTP_NOT_AUTHORITATIVE = java.net.HttpURLConnection.HTTP_NOT_AUTHORITATIVE; /** + * HttpURLConnection#HTTP_MOVED_PERM + * * @see HttpURLConnection#HTTP_MOVED_PERM * @since 4.7 */ int HTTP_MOVED_PERM = java.net.HttpURLConnection.HTTP_MOVED_PERM; /** + * HttpURLConnection#HTTP_MOVED_TEMP + * * @see HttpURLConnection#HTTP_MOVED_TEMP * @since 4.9 */ int HTTP_MOVED_TEMP = java.net.HttpURLConnection.HTTP_MOVED_TEMP; /** + * HttpURLConnection#HTTP_SEE_OTHER + * * @see HttpURLConnection#HTTP_SEE_OTHER * @since 4.9 */ @@ -85,16 +95,22 @@ public interface HttpConnection { int HTTP_11_MOVED_PERM = 308; /** + * HttpURLConnection#HTTP_NOT_FOUND + * * @see HttpURLConnection#HTTP_NOT_FOUND */ int HTTP_NOT_FOUND = java.net.HttpURLConnection.HTTP_NOT_FOUND; /** + * HttpURLConnection#HTTP_UNAUTHORIZED + * * @see HttpURLConnection#HTTP_UNAUTHORIZED */ int HTTP_UNAUTHORIZED = java.net.HttpURLConnection.HTTP_UNAUTHORIZED; /** + * HttpURLConnection#HTTP_FORBIDDEN + * * @see HttpURLConnection#HTTP_FORBIDDEN */ int HTTP_FORBIDDEN = java.net.HttpURLConnection.HTTP_FORBIDDEN; @@ -105,6 +121,7 @@ public interface HttpConnection { * @see HttpURLConnection#getResponseCode() * @return the HTTP Status-Code, or -1 * @throws java.io.IOException + * if an IO error occurred */ int getResponseCode() throws IOException; @@ -122,6 +139,7 @@ public interface HttpConnection { * @see HttpURLConnection#getResponseMessage() * @return the HTTP response message, or <code>null</code> * @throws java.io.IOException + * if an IO error occurred */ String getResponseMessage() throws IOException; @@ -207,14 +225,14 @@ public interface HttpConnection { * @exception IOException * if an I/O error occurs while creating the input stream. * @throws java.io.IOException - * if any. + * if an IO error occurred */ InputStream getInputStream() throws IOException; /** * Get header field. According to - * {@link <a href="https://tools.ietf.org/html/rfc2616#section-4.2">RFC - * 2616</a>} header field names are case insensitive. Header fields defined + * <a href="https://tools.ietf.org/html/rfc2616#section-4.2">RFC 2616</a> + * header field names are case insensitive. Header fields defined * as a comma separated list can have multiple header fields with the same * field name. This method only returns one of these header fields. If you * want the union of all values of all multiple header fields with the same @@ -230,8 +248,8 @@ public interface HttpConnection { /** * Get all values of given header field. According to - * {@link <a href="https://tools.ietf.org/html/rfc2616#section-4.2">RFC - * 2616</a>} header field names are case insensitive. Header fields defined + * <a href="https://tools.ietf.org/html/rfc2616#section-4.2">RFC 2616</a> + * header field names are case insensitive. Header fields defined * as a comma separated list can have multiple header fields with the same * field name. This method does not validate if the given header field is * defined as a comma separated list. @@ -287,6 +305,7 @@ public interface HttpConnection { * @see HttpURLConnection#getOutputStream() * @return an output stream that writes to this connection. * @throws java.io.IOException + * if an IO error occurred */ OutputStream getOutputStream() throws IOException; @@ -321,6 +340,7 @@ public interface HttpConnection { * * @see HttpURLConnection#connect() * @throws java.io.IOException + * if an IO error occurred */ void connect() throws IOException; @@ -338,7 +358,9 @@ public interface HttpConnection { * the source of randomness for this generator or null. See * {@link javax.net.ssl.SSLContext#init(KeyManager[], TrustManager[], SecureRandom)} * @throws java.security.NoSuchAlgorithmException + * if algorithm isn't available * @throws java.security.KeyManagementException + * if key management failed */ void configure(KeyManager[] km, TrustManager[] tm, SecureRandom random) throws NoSuchAlgorithmException, @@ -351,7 +373,9 @@ public interface HttpConnection { * @param hostnameverifier * a {@link javax.net.ssl.HostnameVerifier} object. * @throws java.security.NoSuchAlgorithmException + * if algorithm isn't available * @throws java.security.KeyManagementException + * if key management failed */ void setHostnameVerifier(HostnameVerifier hostnameverifier) throws NoSuchAlgorithmException, KeyManagementException; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory.java index bd0aa3f7ad..b50eaa4186 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory.java @@ -28,6 +28,7 @@ public interface HttpConnectionFactory { * a {@link java.net.URL} object. * @return a {@link org.eclipse.jgit.transport.http.HttpConnection} * @throws java.io.IOException + * if an IO error occurred */ HttpConnection create(URL url) throws IOException; @@ -41,6 +42,7 @@ public interface HttpConnectionFactory { * the proxy to be used * @return a {@link org.eclipse.jgit.transport.http.HttpConnection} * @throws java.io.IOException + * if an IO error occurred */ HttpConnection create(URL url, Proxy proxy) throws IOException; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory2.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory2.java index 88abc60162..71d5c1b6f7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory2.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory2.java @@ -48,7 +48,7 @@ public interface HttpConnectionFactory2 extends HttpConnectionFactory { * {@link HttpConnectionFactory} instance * @param sslVerify * whether SSL is to be verified - * @return the configured {@connection} + * @return the configured {@code connection} * @throws IOException * if the connection cannot be configured * @throws GeneralSecurityException diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnection.java index 3b0bae21ef..e20acadc4a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnection.java @@ -20,7 +20,7 @@ import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -55,7 +55,9 @@ public class JDKHttpConnection implements HttpConnection { * @param url * a {@link java.net.URL} object. * @throws java.net.MalformedURLException + * if URL is malformed * @throws java.io.IOException + * if an IO error occurred */ protected JDKHttpConnection(URL url) throws MalformedURLException, @@ -71,7 +73,9 @@ public class JDKHttpConnection implements HttpConnection { * @param proxy * a {@link java.net.Proxy} object. * @throws java.net.MalformedURLException + * if URL is malformed * @throws java.io.IOException + * if an IO error occurred */ protected JDKHttpConnection(URL url, Proxy proxy) throws MalformedURLException, IOException { @@ -79,73 +83,61 @@ public class JDKHttpConnection implements HttpConnection { .openConnection(proxy); } - /** {@inheritDoc} */ @Override public int getResponseCode() throws IOException { return wrappedUrlConnection.getResponseCode(); } - /** {@inheritDoc} */ @Override public URL getURL() { return wrappedUrlConnection.getURL(); } - /** {@inheritDoc} */ @Override public String getResponseMessage() throws IOException { return wrappedUrlConnection.getResponseMessage(); } - /** {@inheritDoc} */ @Override public Map<String, List<String>> getHeaderFields() { return wrappedUrlConnection.getHeaderFields(); } - /** {@inheritDoc} */ @Override public void setRequestProperty(String key, String value) { wrappedUrlConnection.setRequestProperty(key, value); } - /** {@inheritDoc} */ @Override public void setRequestMethod(String method) throws ProtocolException { wrappedUrlConnection.setRequestMethod(method); } - /** {@inheritDoc} */ @Override public void setUseCaches(boolean usecaches) { wrappedUrlConnection.setUseCaches(usecaches); } - /** {@inheritDoc} */ @Override public void setConnectTimeout(int timeout) { wrappedUrlConnection.setConnectTimeout(timeout); } - /** {@inheritDoc} */ @Override public void setReadTimeout(int timeout) { wrappedUrlConnection.setReadTimeout(timeout); } - /** {@inheritDoc} */ @Override public String getContentType() { return wrappedUrlConnection.getContentType(); } - /** {@inheritDoc} */ @Override public InputStream getInputStream() throws IOException { return wrappedUrlConnection.getInputStream(); } - /** {@inheritDoc} */ @Override public String getHeaderField(@NonNull String name) { return wrappedUrlConnection.getHeaderField(name); @@ -160,75 +152,64 @@ public class JDKHttpConnection implements HttpConnection { private static List<String> mapValuesToListIgnoreCase(String keyName, Map<String, List<String>> m) { - List<String> fields = new LinkedList<>(); + List<String> fields = new ArrayList<>(); m.entrySet().stream().filter(e -> keyName.equalsIgnoreCase(e.getKey())) .filter(e -> e.getValue() != null) .forEach(e -> fields.addAll(e.getValue())); return fields; } - /** {@inheritDoc} */ @Override public int getContentLength() { return wrappedUrlConnection.getContentLength(); } - /** {@inheritDoc} */ @Override public void setInstanceFollowRedirects(boolean followRedirects) { wrappedUrlConnection.setInstanceFollowRedirects(followRedirects); } - /** {@inheritDoc} */ @Override public void setDoOutput(boolean dooutput) { wrappedUrlConnection.setDoOutput(dooutput); } - /** {@inheritDoc} */ @Override public void setFixedLengthStreamingMode(int contentLength) { wrappedUrlConnection.setFixedLengthStreamingMode(contentLength); } - /** {@inheritDoc} */ @Override public OutputStream getOutputStream() throws IOException { return wrappedUrlConnection.getOutputStream(); } - /** {@inheritDoc} */ @Override public void setChunkedStreamingMode(int chunklen) { wrappedUrlConnection.setChunkedStreamingMode(chunklen); } - /** {@inheritDoc} */ @Override public String getRequestMethod() { return wrappedUrlConnection.getRequestMethod(); } - /** {@inheritDoc} */ @Override public boolean usingProxy() { return wrappedUrlConnection.usingProxy(); } - /** {@inheritDoc} */ @Override public void connect() throws IOException { wrappedUrlConnection.connect(); } - /** {@inheritDoc} */ @Override public void setHostnameVerifier(HostnameVerifier hostnameverifier) { ((HttpsURLConnection) wrappedUrlConnection) .setHostnameVerifier(hostnameverifier); } - /** {@inheritDoc} */ @Override public void configure(KeyManager[] km, TrustManager[] tm, SecureRandom random) throws NoSuchAlgorithmException, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java index 046f395049..36731a5fa6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java @@ -60,7 +60,6 @@ public class FileResolver<C> implements RepositoryResolver<C> { setExportAll(exportAll); } - /** {@inheritDoc} */ @Override public Repository open(C req, String name) throws RepositoryNotFoundException, ServiceNotEnabledException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java index 3ef5b29a55..65b18086b4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java @@ -718,7 +718,6 @@ public abstract class AbstractTreeIterator { System.arraycopy(path, pathOffset, buffer, offset, pathLen - pathOffset); } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java index 5c3f6aefe1..c6d50d35f1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java @@ -191,7 +191,6 @@ public class CanonicalTreeParser extends AbstractTreeIterator { reset(reader.open(id, OBJ_TREE).getCachedBytes()); } - /** {@inheritDoc} */ @Override public CanonicalTreeParser createSubtreeIterator(final ObjectReader reader, final MutableObjectId idBuffer) @@ -227,51 +226,43 @@ public class CanonicalTreeParser extends AbstractTreeIterator { return p; } - /** {@inheritDoc} */ @Override public CanonicalTreeParser createSubtreeIterator(ObjectReader reader) throws IncorrectObjectTypeException, IOException { return createSubtreeIterator(reader, new MutableObjectId()); } - /** {@inheritDoc} */ @Override public boolean hasId() { return true; } - /** {@inheritDoc} */ @Override public byte[] idBuffer() { return raw; } - /** {@inheritDoc} */ @Override public int idOffset() { return nextPtr - OBJECT_ID_LENGTH; } - /** {@inheritDoc} */ @Override public void reset() { if (!first()) reset(raw); } - /** {@inheritDoc} */ @Override public boolean first() { return currPtr == 0; } - /** {@inheritDoc} */ @Override public boolean eof() { return currPtr == raw.length; } - /** {@inheritDoc} */ @Override public void next(int delta) { if (delta == 1) { @@ -301,7 +292,6 @@ public class CanonicalTreeParser extends AbstractTreeIterator { parseEntry(); } - /** {@inheritDoc} */ @Override public void back(int delta) { if (delta == 1 && 0 <= prevPtr) { @@ -376,6 +366,7 @@ public class CanonicalTreeParser extends AbstractTreeIterator { * @return {@link org.eclipse.jgit.attributes.AttributesNode} for the * current entry. * @throws java.io.IOException + * if an IO error occurred * @since 4.2 */ public AttributesNode getEntryAttributesNode(ObjectReader reader) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java index 0661c9044a..32368dcfb8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java @@ -57,75 +57,63 @@ public class EmptyTreeIterator extends AbstractTreeIterator { pathLen = childPathOffset - 1; } - /** {@inheritDoc} */ @Override public AbstractTreeIterator createSubtreeIterator(ObjectReader reader) throws IncorrectObjectTypeException, IOException { return new EmptyTreeIterator(this); } - /** {@inheritDoc} */ @Override public boolean hasId() { return false; } - /** {@inheritDoc} */ @Override public ObjectId getEntryObjectId() { return ObjectId.zeroId(); } - /** {@inheritDoc} */ @Override public byte[] idBuffer() { return zeroid; } - /** {@inheritDoc} */ @Override public int idOffset() { return 0; } - /** {@inheritDoc} */ @Override public void reset() { // Do nothing. } - /** {@inheritDoc} */ @Override public boolean first() { return true; } - /** {@inheritDoc} */ @Override public boolean eof() { return true; } - /** {@inheritDoc} */ @Override public void next(int delta) throws CorruptObjectException { // Do nothing. } - /** {@inheritDoc} */ @Override public void back(int delta) throws CorruptObjectException { // Do nothing. } - /** {@inheritDoc} */ @Override public void stopWalk() { if (parent != null) parent.stopWalk(); } - /** {@inheritDoc} */ @Override protected boolean needsStopWalk() { return parent != null && parent.needsStopWalk(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java index 60b92d7271..0cac374844 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java @@ -175,7 +175,6 @@ public class FileTreeIterator extends WorkingTreeIterator { init(entries()); } - /** {@inheritDoc} */ @Override public AbstractTreeIterator createSubtreeIterator(ObjectReader reader) throws IncorrectObjectTypeException, IOException { @@ -372,12 +371,6 @@ public class FileTreeIterator extends WorkingTreeIterator { return attributes.getLength(); } - @Override - @Deprecated - public long getLastModified() { - return attributes.getLastModifiedInstant().toEpochMilli(); - } - /** * @since 5.1.9 */ @@ -425,13 +418,11 @@ public class FileTreeIterator extends WorkingTreeIterator { return ((FileEntry) current()).getFile(); } - /** {@inheritDoc} */ @Override protected byte[] idSubmodule(Entry e) { return idSubmodule(getDirectory(), e); } - /** {@inheritDoc} */ @Override protected String readSymlinkTarget(Entry entry) throws IOException { return fs.readSymLink(getEntryFile()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java index ece945232e..31c216b4a8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -38,12 +38,12 @@ import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; @@ -78,6 +78,8 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { private static final AbstractTreeIterator[] NO_TREES = {}; /** + * Type of operation to retrieve git attributes for. + * * @since 4.2 */ public enum OperationType { @@ -548,7 +550,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * <p> * Retrieve the git attributes for the current entry. * - * <h3>Git attribute computation</h3> + * <h4>Git attribute computation</h4> * * <ul> * <li>Get the attributes matching the current path entry from the info file @@ -563,11 +565,10 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * <li>In the end, completes the list of matching attributes using the * global attribute file define in the configuration (see * {@link AttributesNodeProvider#getGlobalAttributesNode()})</li> - * * </ul> * * - * <h3>Iterator constraints</h3> + * <h4>Iterator constraints</h4> * * <p> * In order to have a correct list of attributes for the current entry, this @@ -960,6 +961,8 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * iterators to manage only one list of items, with the diving handled by * recursive trees. * + * @param <T> + * Type of returned {@code AbstractTreeIterator} * @param nth * tree to obtain the current iterator of. * @param clazz @@ -1376,12 +1379,14 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } /** - * Returns an AbstractTreeIterator from {@code trees} with the smallest name, and sets its - * {@code matches} field. This may clobber {@code matches} in other {@code tree}s. Other iterators - * at the same name will have their {@code matches} pointing to the same {@code min()} value. + * Returns an AbstractTreeIterator from {@code trees} with the smallest + * name, and sets its {@code matches} field. This may clobber + * {@code matches} in other {@code tree}s. Other iterators at the same name + * will have their {@code matches} pointing to the same {@code min()} value. * * @return the smallest tree iterator available. * @throws CorruptObjectException + * if an object is corrupt */ @SuppressWarnings("unused") AbstractTreeIterator min() throws CorruptObjectException { @@ -1488,6 +1493,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * {{@link #getSmudgeCommand(int)} instead. * @return a filter command * @throws java.io.IOException + * if an IO error occurred * @since 4.2 */ public String getFilterCommand(String filterCommandType) @@ -1510,7 +1516,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } return filterCommand.replaceAll("%f", //$NON-NLS-1$ Matcher.quoteReplacement( - QuotedString.BOURNE.quote((getPathString())))); + QuotedString.BOURNE.quote(getPathString()))); } /** @@ -1521,6 +1527,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * of the tree the item to be smudged is in * @return a filter command * @throws java.io.IOException + * if an IO error occurred * @since 6.1 */ public String getSmudgeCommand(int index) @@ -1536,6 +1543,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * to use * @return a filter command * @throws java.io.IOException + * if an IO error occurred * @since 6.1 */ public String getSmudgeCommand(Attributes attributes) throws IOException { @@ -1558,7 +1566,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } return filterCommand.replaceAll("%f", //$NON-NLS-1$ Matcher.quoteReplacement( - QuotedString.BOURNE.quote((getPathString())))); + QuotedString.BOURNE.quote(getPathString()))); } /** @@ -1579,10 +1587,16 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { */ private String getFilterCommandDefinition(String filterDriverName, String filterCommandType) { + if (config == null) { + return null; + } String key = filterDriverName + "." + filterCommandType; //$NON-NLS-1$ String filterCommand = filterCommandsByNameDotType.get(key); if (filterCommand != null) return filterCommand; + if (config == null) { + return null; + } filterCommand = config.getString(ConfigConstants.CONFIG_FILTER_SECTION, filterDriverName, filterCommandType); boolean useBuiltin = config.getBoolean( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java index b5d6610d52..f16d800f63 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -15,7 +15,6 @@ package org.eclipse.jgit.treewalk; import static java.nio.charset.StandardCharsets.UTF_8; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -25,6 +24,7 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetEncoder; +import java.nio.file.Files; import java.nio.file.Path; import java.text.MessageFormat; import java.time.Instant; @@ -68,8 +68,10 @@ import org.eclipse.jgit.util.Holder; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.Paths; import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer.LocalFile; +import org.eclipse.jgit.util.io.ByteBufferInputStream; import org.eclipse.jgit.util.io.EolStreamTypeUtil; import org.eclipse.jgit.util.sha1.SHA1; @@ -270,7 +272,6 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { return state.walkIgnored; } - /** {@inheritDoc} */ @Override public boolean hasId() { if (contentIdFromPtr == ptr) @@ -278,7 +279,6 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { return (mode & FileMode.TYPE_MASK) == FileMode.TYPE_FILE; } - /** {@inheritDoc} */ @Override public byte[] idBuffer() { if (contentIdFromPtr == ptr) @@ -316,7 +316,6 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { return zeroid; } - /** {@inheritDoc} */ @Override public boolean isWorkTree() { return true; @@ -408,9 +407,9 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { if (len <= MAXIMUM_FILE_SIZE_TO_READ_FULLY) { InputStream is = e.openInputStream(); try { - ByteBuffer rawbuf = IO.readWholeStream(is, (int) len); - rawbuf = filterClean(rawbuf.array(), rawbuf.limit()); - return rawbuf.limit(); + ByteBuffer filteredData = IO.readWholeStream(filterClean(is), + (int) len); + return filteredData.remaining(); } finally { safeClose(is); } @@ -439,10 +438,9 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } if (len <= MAXIMUM_FILE_SIZE_TO_READ_FULLY) { - ByteBuffer rawbuf = IO.readWholeStream(is, (int) len); - rawbuf = filterClean(rawbuf.array(), rawbuf.limit()); - canonLen = rawbuf.limit(); - return new ByteArrayInputStream(rawbuf.array(), 0, (int) canonLen); + ByteBuffer filteredData = IO.readWholeStream(filterClean(is), (int) len); + canonLen = filteredData.remaining(); + return new ByteBufferInputStream(filteredData); } if (getCleanFilterCommand() == null && isBinary(e)) { @@ -478,16 +476,6 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } } - private ByteBuffer filterClean(byte[] src, int n) - throws IOException { - InputStream in = new ByteArrayInputStream(src); - try { - return IO.readWholeStream(filterClean(in), n); - } finally { - safeClose(in); - } - } - private InputStream filterClean(InputStream in) throws IOException { in = EolStreamTypeUtil.wrapInputStream(in, @@ -510,6 +498,8 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { filterProcessBuilder.directory(repository.getWorkTree()); filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY, repository.getDirectory().getAbsolutePath()); + filterProcessBuilder.environment().put(Constants.GIT_COMMON_DIR_KEY, + repository.getCommonDirectory().getAbsolutePath()); ExecutionResult result; try { result = fs.execute(filterProcessBuilder, in); @@ -549,13 +539,11 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { return repository; } - /** {@inheritDoc} */ @Override public int idOffset() { return contentIdOffset; } - /** {@inheritDoc} */ @Override public void reset() { if (!first()) { @@ -565,19 +553,16 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } } - /** {@inheritDoc} */ @Override public boolean first() { return ptr == 0; } - /** {@inheritDoc} */ @Override public boolean eof() { return ptr == entryCnt; } - /** {@inheritDoc} */ @Override public void next(int delta) throws CorruptObjectException { ptr += delta; @@ -586,7 +571,6 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } } - /** {@inheritDoc} */ @Override public void back(int delta) throws CorruptObjectException { ptr -= delta; @@ -620,6 +604,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * * @return size of the content, in bytes * @throws java.io.IOException + * if an IO error occurred */ public long getEntryContentLength() throws IOException { if (canonLen == -1) { @@ -637,18 +622,6 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { /** * Get the last modified time of this entry. * - * @return last modified time of this file, in milliseconds since the epoch - * (Jan 1, 1970 UTC). - * @deprecated use {@link #getEntryLastModifiedInstant()} instead - */ - @Deprecated - public long getEntryLastModified() { - return current().getLastModified(); - } - - /** - * Get the last modified time of this entry. - * * @return last modified time of this file * @since 5.1.9 */ @@ -772,6 +745,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * @return the {@link org.eclipse.jgit.attributes.AttributesNode} for the * current entry. * @throws IOException + * if an IO error occurred */ public AttributesNode getEntryAttributesNode() throws IOException { if (attributesNode instanceof PerDirectoryAttributesNode) @@ -964,6 +938,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * access to repository objects if necessary. Should not be null. * @return true if content is most likely different. * @throws java.io.IOException + * if an IO error occurred * @since 3.3 */ public boolean isModified(DirCacheEntry entry, boolean forceContentCheck, @@ -1070,6 +1045,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * @return <code>true</code> if the content doesn't match, * <code>false</code> if it matches * @throws IOException + * if an IO error occurred */ private boolean contentCheck(DirCacheEntry entry, ObjectReader reader) throws IOException { @@ -1243,21 +1219,6 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * 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. - * @deprecated use {@link #getLastModifiedInstant()} instead - */ - @Deprecated - public abstract long getLastModified(); - - /** - * 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 of the last change. * @since 5.1.9 */ @@ -1334,7 +1295,11 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_EXCLUDESFILE, fs, null, null); if (path != null) { - loadRulesFromFile(coreExclude, path.toFile()); + if (Files.exists(path)) { + loadRulesFromFile(coreExclude, path.toFile()); + } + } else { + loadRulesFromDefaultFile(coreExclude, fs); } if (coreExclude.getRules().isEmpty()) { coreExclude = parent; @@ -1342,9 +1307,11 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { IgnoreNode infoExclude = new IgnoreNodeWithParent( coreExclude); - File exclude = fs.resolve(repository.getDirectory(), + File exclude = fs.resolve(repository.getCommonDirectory(), Constants.INFO_EXCLUDE); - loadRulesFromFile(infoExclude, exclude); + if (fs.exists(exclude)) { + loadRulesFromFile(infoExclude, exclude); + } if (infoExclude.getRules().isEmpty()) { infoExclude = null; } @@ -1366,9 +1333,19 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { private static void loadRulesFromFile(IgnoreNode r, File exclude) throws FileNotFoundException, IOException { - if (FS.DETECTED.exists(exclude)) { - try (FileInputStream in = new FileInputStream(exclude)) { - r.parse(exclude.getAbsolutePath(), in); + try (FileInputStream in = new FileInputStream(exclude)) { + r.parse(exclude.getAbsolutePath(), in); + } + } + + private static void loadRulesFromDefaultFile(IgnoreNode r, + FS fileSystem) throws FileNotFoundException, IOException { + Path cfg = SystemReader.getInstance() + .getXdgConfigDirectory(fileSystem); + if (cfg != null) { + Path cfgPath = cfg.resolve("git").resolve("ignore"); //$NON-NLS-1$ //$NON-NLS-2$ + if (Files.exists(cfgPath)) { + loadRulesFromFile(r, cfgPath.toFile()); } } } @@ -1450,6 +1427,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * @return the clean filter command for the current entry or * <code>null</code> if no such command is defined * @throws java.io.IOException + * if an IO error occurred * @since 4.2 */ public String getCleanFilterCommand() throws IOException { @@ -1472,6 +1450,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * {@link org.eclipse.jgit.treewalk.TreeWalk} is not based on a * {@link org.eclipse.jgit.lib.Repository} then null is returned. * @throws java.io.IOException + * if an IO error occurred * @since 4.3 */ public EolStreamType getEolStreamType() throws IOException { @@ -1486,6 +1465,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * {@link TreeWalk} is not based on a {@link Repository} then null * is returned. * @throws IOException + * if an IO error occurred */ private EolStreamType getEolStreamType(OperationType opType) 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 index c6804da039..b35dbebd17 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/AndTreeFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/AndTreeFilter.java @@ -12,11 +12,14 @@ package org.eclipse.jgit.treewalk.filter; import java.io.IOException; +import java.util.Arrays; import java.util.Collection; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; /** @@ -100,6 +103,13 @@ public abstract class AndTreeFilter extends TreeFilter { } @Override + public boolean shouldTreeWalk(RevCommit c, RevWalk rw, + MutableBoolean cpfUsed) { + return a.shouldTreeWalk(c, rw, cpfUsed) + && b.shouldTreeWalk(c, rw, cpfUsed); + } + + @Override public int matchFilter(TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -174,6 +184,13 @@ public abstract class AndTreeFilter extends TreeFilter { } @Override + public boolean shouldTreeWalk(RevCommit c, RevWalk rw, + MutableBoolean cpfUsed) { + return Arrays.stream(subfilters) + .allMatch(t -> t.shouldTreeWalk(c, rw, cpfUsed)); + } + + @Override public TreeFilter clone() { final TreeFilter[] s = new TreeFilter[subfilters.length]; for (int i = 0; i < s.length; i++) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java index cafa926ffc..33db6ea661 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java @@ -13,6 +13,10 @@ package org.eclipse.jgit.treewalk.filter; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + import org.eclipse.jgit.util.RawParseUtils; /** @@ -39,6 +43,7 @@ class ByteArraySet { * Create an empty set. * * @param capacity + * initial capacity of the set */ ByteArraySet(int capacity) { initTable(1 << Integer.highestOneBit((capacity * 2) - 1)); @@ -136,13 +141,19 @@ class ByteArraySet { } /** + * Returns number of arrays in the set + * * @return number of arrays in the set */ int size() { return size; } - /** @return true if {@link #size()} is 0. */ + /** + * Returns true if {@link #size()} is 0 + * + * @return true if {@link #size()} is 0 + */ boolean isEmpty() { return size == 0; } @@ -180,7 +191,6 @@ class ByteArraySet { table = new byte[sz][]; } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -291,4 +301,8 @@ class ByteArraySet { return ret; } + Set<byte[]> toSet() { + return Arrays.stream(toArray()).collect(Collectors.toSet()); + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilter.java new file mode 100644 index 0000000000..a74b9b617f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilter.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2025, Google LLC and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.treewalk.filter; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.commitgraph.ChangedPathFilter; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.StringUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Filter tree entries that modified the contents of particular file paths. + * <p> + * Equivalent to AndTreeFilter(PathFilter, AnyDiffFilter). This filter uses + * {@link org.eclipse.jgit.internal.storage.commitgraph.ChangedPathFilter} + * (bloom filters) when available to discard commits without diffing their + * trees. + * + * @since 7.3 + */ +public class ChangedPathTreeFilter extends TreeFilter { + + private TreeFilter pathFilter; + + private List<String> paths; + + private List<byte[]> rawPaths; + + /** + * Create a TreeFilter for trees modifying one or more user supplied paths. + * <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 ChangedPathTreeFilter create(String... paths) { + return new ChangedPathTreeFilter(paths); + } + + private ChangedPathTreeFilter(String... paths) { + List<String> filtered = Arrays.stream(paths) + .map(s -> StringUtils.trim(s, '/')) + .collect(Collectors.toList()); + + if (filtered.size() == 0) + throw new IllegalArgumentException( + JGitText.get().atLeastOnePathIsRequired); + + if (filtered.stream().anyMatch(s -> s.isEmpty() || s.isBlank())) { + throw new IllegalArgumentException( + JGitText.get().emptyPathNotPermitted); + } + + this.paths = filtered; + this.rawPaths = this.paths.stream().map(Constants::encode) + .collect(Collectors.toList()); + if (filtered.size() == 1) { + this.pathFilter = PathFilter.create(paths[0]); + } else { + this.pathFilter = OrTreeFilter.create(Arrays.stream(paths) + .map(PathFilter::create).collect(Collectors.toList())); + } + } + + @Override + public boolean shouldTreeWalk(RevCommit c, RevWalk rw, + MutableBoolean cpfUsed) { + ChangedPathFilter cpf = c.getChangedPathFilter(rw); + if (cpf == null) { + return true; + } + if (cpfUsed != null) { + cpfUsed.orValue(true); + } + // return true if at least one path might exist in cpf + return rawPaths.stream().anyMatch(cpf::maybeContains); + } + + @Override + public boolean include(TreeWalk walker) throws IOException { + return pathFilter.include(walker) && ANY_DIFF.include(walker); + } + + @Override + public boolean shouldBeRecursive() { + return pathFilter.shouldBeRecursive() || ANY_DIFF.shouldBeRecursive(); + } + + @Override + public ChangedPathTreeFilter clone() { + return this; + } + + /** + * Get the paths this filter matches. + * + * @return the paths this filter matches. + */ + public List<String> getPaths() { + return paths; + } + + @SuppressWarnings("nls") + @Override + public String toString() { + return "(CHANGED_PATH(" + pathFilter.toString() + ")" // + + " AND " // + + ANY_DIFF.toString() + ")"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/IndexDiffFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/IndexDiffFilter.java index 4731f345bc..cfdc4dd358 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/IndexDiffFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/IndexDiffFilter.java @@ -10,8 +10,9 @@ package org.eclipse.jgit.treewalk.filter; import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -62,9 +63,9 @@ public class IndexDiffFilter extends TreeFilter { private final Set<String> ignoredPaths = new HashSet<>(); - private final LinkedList<String> untrackedParentFolders = new LinkedList<>(); + private final ArrayDeque<String> untrackedParentFolders = new ArrayDeque<>(); - private final LinkedList<String> untrackedFolders = new LinkedList<>(); + private final ArrayDeque<String> untrackedFolders = new ArrayDeque<>(); /** * Creates a new instance of this filter. Do not use an instance of this @@ -106,7 +107,6 @@ public class IndexDiffFilter extends TreeFilter { this.honorIgnores = honorIgnores; } - /** {@inheritDoc} */ @Override public boolean include(TreeWalk tw) throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -234,7 +234,6 @@ public class IndexDiffFilter extends TreeFilter { return tw.getTree(workingTree, WorkingTreeIterator.class); } - /** {@inheritDoc} */ @Override public boolean shouldBeRecursive() { // We cannot compare subtrees in the working tree, so encourage @@ -242,13 +241,11 @@ public class IndexDiffFilter extends TreeFilter { return true; } - /** {@inheritDoc} */ @Override public TreeFilter clone() { return this; } - /** {@inheritDoc} */ @Override public String toString() { return "INDEX_DIFF_FILTER"; //$NON-NLS-1$ @@ -276,12 +273,14 @@ public class IndexDiffFilter extends TreeFilter { * empty list will be returned. */ public List<String> getUntrackedFolders() { - LinkedList<String> ret = new LinkedList<>(untrackedFolders); + ArrayList<String> ret = new ArrayList<>(untrackedFolders); if (!untrackedParentFolders.isEmpty()) { String toBeAdded = untrackedParentFolders.getLast(); - while (!ret.isEmpty() && ret.getLast().startsWith(toBeAdded)) - ret.removeLast(); - ret.addLast(toBeAdded); + while (!ret.isEmpty() + && ret.get(ret.size() - 1).startsWith(toBeAdded)) { + ret.remove(ret.size() - 1); + } + ret.add(toBeAdded); } return ret; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/InterIndexDiffFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/InterIndexDiffFilter.java index 163dc71daa..0c0b09e7bf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/InterIndexDiffFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/InterIndexDiffFilter.java @@ -28,7 +28,6 @@ public final class InterIndexDiffFilter extends TreeFilter { */ public static final TreeFilter INSTANCE = new InterIndexDiffFilter(); - /** {@inheritDoc} */ @Override public boolean include(TreeWalk walker) { final int n = walker.getTreeCount(); @@ -57,19 +56,16 @@ public final class InterIndexDiffFilter extends TreeFilter { return false; } - /** {@inheritDoc} */ @Override public boolean shouldBeRecursive() { return false; } - /** {@inheritDoc} */ @Override public TreeFilter clone() { return this; } - /** {@inheritDoc} */ @Override public String toString() { return "INTERINDEX_DIFF"; //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotIgnoredFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotIgnoredFilter.java index 7d04f27f3c..25947da08f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotIgnoredFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotIgnoredFilter.java @@ -33,7 +33,6 @@ public class NotIgnoredFilter extends TreeFilter { this.index = workdirTreeIndex; } - /** {@inheritDoc} */ @Override public boolean include(TreeWalk tw) throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -41,20 +40,17 @@ public class NotIgnoredFilter extends TreeFilter { return i == null || !i.isEntryIgnored(); } - /** {@inheritDoc} */ @Override public boolean shouldBeRecursive() { return false; } - /** {@inheritDoc} */ @Override public TreeFilter clone() { // immutable return this; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String 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 index 4fb615321f..e9cd83c2e2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotTreeFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotTreeFilter.java @@ -38,13 +38,11 @@ public class NotTreeFilter extends TreeFilter { a = one; } - /** {@inheritDoc} */ @Override public TreeFilter negate() { return a; } - /** {@inheritDoc} */ @Override public boolean include(TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, @@ -52,7 +50,6 @@ public class NotTreeFilter extends TreeFilter { return matchFilter(walker) == 0; } - /** {@inheritDoc} */ @Override public int matchFilter(TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, @@ -69,20 +66,17 @@ public class NotTreeFilter extends TreeFilter { return -1; } - /** {@inheritDoc} */ @Override public boolean shouldBeRecursive() { return a.shouldBeRecursive(); } - /** {@inheritDoc} */ @Override public TreeFilter clone() { final TreeFilter n = a.clone(); return n == a ? this : new NotTreeFilter(n); } - /** {@inheritDoc} */ @Override public String toString() { return "NOT " + a.toString(); //$NON-NLS-1$ 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 index 3c18a9f98d..ce2382552b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/OrTreeFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/OrTreeFilter.java @@ -12,11 +12,14 @@ package org.eclipse.jgit.treewalk.filter; import java.io.IOException; +import java.util.Arrays; import java.util.Collection; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; /** @@ -116,6 +119,13 @@ public abstract class OrTreeFilter extends TreeFilter { } @Override + public boolean shouldTreeWalk(RevCommit c, RevWalk rw, + MutableBoolean cpfUsed) { + return a.shouldTreeWalk(c, rw, cpfUsed) + || b.shouldTreeWalk(c, rw, cpfUsed); + } + + @Override public boolean shouldBeRecursive() { return a.shouldBeRecursive() || b.shouldBeRecursive(); } @@ -164,6 +174,13 @@ public abstract class OrTreeFilter extends TreeFilter { } @Override + public boolean shouldTreeWalk(RevCommit c, RevWalk rw, + MutableBoolean cpfUsed) { + return Arrays.stream(subfilters) + .anyMatch(t -> t.shouldTreeWalk(c, rw, cpfUsed)); + } + + @Override public boolean shouldBeRecursive() { for (TreeFilter f : subfilters) if (f.shouldBeRecursive()) 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 index c94215fcb1..62422817ac 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilter.java @@ -11,6 +11,10 @@ package org.eclipse.jgit.treewalk.filter; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.treewalk.TreeWalk; @@ -45,7 +49,8 @@ public class PathFilter extends TreeFilter { while (path.endsWith("/")) //$NON-NLS-1$ path = path.substring(0, path.length() - 1); if (path.length() == 0) - throw new IllegalArgumentException(JGitText.get().emptyPathNotPermitted); + throw new IllegalArgumentException( + JGitText.get().emptyPathNotPermitted); return new PathFilter(path); } @@ -67,19 +72,16 @@ public class PathFilter extends TreeFilter { return pathStr; } - /** {@inheritDoc} */ @Override public boolean include(TreeWalk walker) { return matchFilter(walker) <= 0; } - /** {@inheritDoc} */ @Override public int matchFilter(TreeWalk walker) { return walker.isPathMatch(pathRaw, pathRaw.length); } - /** {@inheritDoc} */ @Override public boolean shouldBeRecursive() { for (byte b : pathRaw) @@ -88,13 +90,18 @@ public class PathFilter extends TreeFilter { return false; } + @Override + public Optional<Set<byte[]>> getPathsBestEffort() { + Set<byte[]> s = Collections.singleton(pathRaw); + return Optional.of(s); + } + /** {@inheritDoc} */ @Override public PathFilter clone() { return this; } - /** {@inheritDoc} */ @Override @SuppressWarnings("nls") public String toString() { 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 index 59855572f2..4c0604ad56 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java @@ -12,6 +12,8 @@ package org.eclipse.jgit.treewalk.filter; import java.util.Collection; +import java.util.Optional; +import java.util.Set; import org.eclipse.jgit.errors.StopWalkException; import org.eclipse.jgit.internal.JGitText; @@ -232,6 +234,15 @@ public class PathFilterGroup { } @Override + public Optional<Set<byte[]>> getPathsBestEffort() { + Set<byte[]> result = fullpaths.toSet(); + if (result.isEmpty()) { + return Optional.empty(); + } + return Optional.of(result); + } + + @Override public TreeFilter clone() { return this; } 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 index 3816d5ed00..ec25903211 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathSuffixFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathSuffixFilter.java @@ -56,13 +56,11 @@ public class PathSuffixFilter extends TreeFilter { pathRaw = Constants.encode(pathStr); } - /** {@inheritDoc} */ @Override public TreeFilter clone() { return this; } - /** {@inheritDoc} */ @Override public boolean include(TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException { @@ -82,7 +80,6 @@ public class PathSuffixFilter extends TreeFilter { return super.matchFilter(walker); } - /** {@inheritDoc} */ @Override public boolean shouldBeRecursive() { return true; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/SkipWorkTreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/SkipWorkTreeFilter.java index 1ed2ef32cd..e311523033 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/SkipWorkTreeFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/SkipWorkTreeFilter.java @@ -36,7 +36,6 @@ public class SkipWorkTreeFilter extends TreeFilter { this.treeIdx = treeIdx; } - /** {@inheritDoc} */ @Override public boolean include(TreeWalk walker) { DirCacheIterator i = walker.getTree(treeIdx, DirCacheIterator.class); @@ -47,19 +46,16 @@ public class SkipWorkTreeFilter extends TreeFilter { return e == null || !e.isSkipWorkTree(); } - /** {@inheritDoc} */ @Override public boolean shouldBeRecursive() { return false; } - /** {@inheritDoc} */ @Override public TreeFilter clone() { return this; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { 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 index 6dbd508e48..8159843312 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilter.java @@ -11,10 +11,15 @@ package org.eclipse.jgit.treewalk.filter; import java.io.IOException; +import java.util.Optional; +import java.util.Set; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.dircache.DirCacheIterator; 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.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeIterator; @@ -188,10 +193,8 @@ public abstract class TreeFilter { * as thrown by {@link #include(TreeWalk)} * @since 4.7 */ - public int matchFilter(TreeWalk walker) - throws MissingObjectException, IncorrectObjectTypeException, - IOException - { + public int matchFilter(TreeWalk walker) throws MissingObjectException, + IncorrectObjectTypeException, IOException { return include(walker) ? 0 : 1; } @@ -210,6 +213,43 @@ public abstract class TreeFilter { public abstract boolean shouldBeRecursive(); /** + * Return true if the tree entries within this commit require + * {@link #include(TreeWalk)} to correctly determine whether they are + * interesting to report. + * <p> + * Otherwise, all tree entries within this commit are UNINTERESTING for this + * tree filter. + * + * @param c + * the commit being considered by the TreeFilter. + * @param rw + * the RevWalk used in retrieving relevant commit data. + * @param cpfUsed + * if not null, it reports if the changedPathFilter was used in + * this method + * @return True if the tree entries within c require + * {@link #include(TreeWalk)}. + * @since 7.3 + */ + public boolean shouldTreeWalk(RevCommit c, RevWalk rw, + @Nullable MutableBoolean cpfUsed) { + return true; + } + + /** + * If this filter checks that a specific set of paths have all been + * modified, returns that set of paths to be checked against a changed path + * filter. Otherwise, returns empty. + * + * @return a set of paths, or empty + * @deprecated use {@code shouldTreeWalk} instead. + */ + @Deprecated(since = "7.3") + public Optional<Set<byte[]>> getPathsBestEffort() { + return Optional.empty(); + } + + /** * {@inheritDoc} * * Clone this tree filter, including its parameters. @@ -220,7 +260,6 @@ public abstract class TreeFilter { @Override public abstract TreeFilter clone(); - /** {@inheritDoc} */ @Override public String toString() { String n = getClass().getName(); @@ -230,4 +269,33 @@ public abstract class TreeFilter { } return n.replace('$', '.'); } + + /** + * Mutable wrapper to return a boolean in a function parameter. + * + * @since 7.3 + */ + public static class MutableBoolean { + private boolean value; + + /** + * Return the boolean value. + * + * @return The state of the internal boolean value. + */ + public boolean get() { + return value; + } + + void orValue(boolean v) { + value = value || v; + } + + /** + * Reset the boolean value. + */ + public void reset() { + value = false; + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java index 119c96e02e..48d1c50c7d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java @@ -96,6 +96,7 @@ public class Base64 { * @param destOffset * the index where output will be put */ + @SuppressWarnings("UnnecessaryParentheses") private static void encode3to4(byte[] source, int srcOffset, int numSigBytes, byte[] destination, int destOffset) { // We have to shift left 24 in order to flush out the 1's that appear @@ -201,6 +202,7 @@ public class Base64 { * the index where output will be put * @return the number of decoded bytes converted */ + @SuppressWarnings("UnnecessaryParentheses") private static int decode4to3(byte[] source, int srcOffset, byte[] destination, int destOffset) { // Example: Dk== diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/BlockList.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/BlockList.java index 527c5a69df..557e2cde3b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/BlockList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/BlockList.java @@ -75,13 +75,11 @@ public class BlockList<T> extends AbstractList<T> { tailBlock = directory[0]; } - /** {@inheritDoc} */ @Override public int size() { return size; } - /** {@inheritDoc} */ @Override public void clear() { for (T[] block : directory) { @@ -94,7 +92,6 @@ public class BlockList<T> extends AbstractList<T> { tailBlock = directory[0]; } - /** {@inheritDoc} */ @Override public T get(int index) { if (index < 0 || size <= index) @@ -102,7 +99,6 @@ public class BlockList<T> extends AbstractList<T> { return directory[toDirectoryIndex(index)][toBlockIndex(index)]; } - /** {@inheritDoc} */ @Override public T set(int index, T element) { if (index < 0 || size <= index) @@ -160,7 +156,6 @@ public class BlockList<T> extends AbstractList<T> { } } - /** {@inheritDoc} */ @Override public boolean add(T element) { int i = tailBlkIdx; @@ -191,7 +186,6 @@ public class BlockList<T> extends AbstractList<T> { return true; } - /** {@inheritDoc} */ @Override public void add(int index, T element) { if (index == size) { @@ -213,7 +207,6 @@ public class BlockList<T> extends AbstractList<T> { } } - /** {@inheritDoc} */ @Override public T remove(int index) { if (index == size - 1) { @@ -253,7 +246,6 @@ public class BlockList<T> extends AbstractList<T> { tailBlock = directory[tailDirIdx]; } - /** {@inheritDoc} */ @Override public Iterator<T> iterator() { return new MyIterator(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/CachedAuthenticator.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/CachedAuthenticator.java index 5815c62e89..d8183eb8be 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/CachedAuthenticator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/CachedAuthenticator.java @@ -32,7 +32,6 @@ public abstract class CachedAuthenticator extends Authenticator { cached.add(ca); } - /** {@inheritDoc} */ @Override protected final PasswordAuthentication getPasswordAuthentication() { final String host = getRequestingHost(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java index 12af374b2e..c8421d6012 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java @@ -86,8 +86,8 @@ public class ChangeIdUtil { } } - private static final Pattern issuePattern = Pattern - .compile("^(Bug|Issue)[a-zA-Z0-9-]*:.*$"); //$NON-NLS-1$ + private static final Pattern signedOffByPattern = Pattern + .compile("^Signed-off-by:.*$"); //$NON-NLS-1$ private static final Pattern footerPattern = Pattern .compile("(^[a-zA-Z0-9-]+:(?!//).*$)"); //$NON-NLS-1$ @@ -159,7 +159,7 @@ public class ChangeIdUtil { int footerFirstLine = indexOfFirstFooterLine(lines); int insertAfter = footerFirstLine; for (int i = footerFirstLine; i < lines.length; ++i) { - if (issuePattern.matcher(lines[i]).matches()) { + if (!signedOffByPattern.matcher(lines[i]).matches()) { insertAfter = i + 1; continue; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java index da1684630b..ff136f7b3b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java @@ -13,22 +13,24 @@ package org.eclipse.jgit.util; /** * Equality utilities. * - * @since: 6.2 + * @since 6.2 */ public class Equality { /** - * Compare by reference - * - * @param a - * First object to compare - * @param b - * Second object to compare - * @return {@code true} if the objects are identical, {@code false} - * otherwise - * - * @since 6.2 - */ + * Compare by reference + * + * @param <T> + * type of the objects to compare + * @param a + * First object to compare + * @param b + * Second object to compare + * @return {@code true} if the objects are identical, {@code false} + * otherwise + * + * @since 6.2 + */ @SuppressWarnings("ReferenceEquality") public static <T> boolean isSameInstance(T a, T b) { return a == b; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java index aef9e64e02..6a40fad1db 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -30,9 +30,6 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; -import java.security.AccessControlException; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.text.MessageFormat; import java.time.Duration; import java.time.Instant; @@ -69,6 +66,7 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.LockFailedException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.FileSnapshot; +import org.eclipse.jgit.internal.util.ShutdownHook; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; @@ -98,6 +96,9 @@ public abstract class FS { private static final Pattern VERSION = Pattern .compile("\\s(\\d+)\\.(\\d+)\\.(\\d+)"); //$NON-NLS-1$ + private static final Pattern EMPTY_PATH = Pattern + .compile("^[\\p{javaWhitespace}" + File.pathSeparator + "]*$"); //$NON-NLS-1$ //$NON-NLS-2$ + private volatile Boolean supportSymlinks; /** @@ -118,6 +119,7 @@ public abstract class FS { * Detect the file system * * @param cygwinUsed + * whether cygwin is used * @return FS instance */ public FS detect(Boolean cygwinUsed) { @@ -149,8 +151,11 @@ public abstract class FS { /** * @param stdout + * stdout stream * @param stderr + * stderr stream * @param rc + * return code */ public ExecutionResult(TemporaryBuffer stdout, TemporaryBuffer stderr, int rc) { @@ -160,6 +165,8 @@ public abstract class FS { } /** + * Get buffered standard output stream + * * @return buffered standard output stream */ public TemporaryBuffer getStdout() { @@ -167,6 +174,8 @@ public abstract class FS { } /** + * Get buffered standard error stream + * * @return buffered standard error stream */ public TemporaryBuffer getStderr() { @@ -174,6 +183,8 @@ public abstract class FS { } /** + * Get the return code of the process + * * @return the return code of the process */ public int getRc() { @@ -202,7 +213,7 @@ public abstract class FS { * </p> */ public static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration - .ofMillis(2000); + .ofSeconds(2); /** * Fallback FileStore attributes used when we can't measure the @@ -250,31 +261,6 @@ public abstract class FS { private static final AtomicInteger threadNumber = new AtomicInteger(1); /** - * Don't use the default thread factory of the ForkJoinPool for the - * CompletableFuture; it runs without any privileges, which causes - * trouble if a SecurityManager is present. - * <p> - * Instead use normal daemon threads. They'll belong to the - * SecurityManager's thread group, or use the one of the calling thread, - * as appropriate. - * </p> - * - * @see java.util.concurrent.Executors#newCachedThreadPool() - */ - private static final ExecutorService FUTURE_RUNNER = new ThreadPoolExecutor( - 0, 5, 30L, TimeUnit.SECONDS, - new LinkedBlockingQueue<Runnable>(), - runnable -> { - Thread t = new Thread(runnable, - "JGit-FileStoreAttributeReader-" //$NON-NLS-1$ - + threadNumber.getAndIncrement()); - // Make sure these threads don't prevent application/JVM - // shutdown. - t.setDaemon(true); - return t; - }); - - /** * Use a separate executor with at most one thread to synchronize * writing to the config. We write asynchronously since the config * itself might be on a different file system, which might otherwise @@ -287,7 +273,7 @@ public abstract class FS { */ private static final ExecutorService SAVE_RUNNER = new ThreadPoolExecutor( 0, 1, 1L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<Runnable>(), + new LinkedBlockingQueue<>(), runnable -> { Thread t = new Thread(runnable, "JGit-FileStoreAttributeWriter-" //$NON-NLS-1$ @@ -299,18 +285,16 @@ public abstract class FS { static { // Shut down the SAVE_RUNNER on System.exit() + ShutdownHook.INSTANCE + .register(FileStoreAttributes::shutdownSafeRunner); + } + + private static void shutdownSafeRunner() { try { - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - SAVE_RUNNER.shutdownNow(); - SAVE_RUNNER.awaitTermination(100, - TimeUnit.MILLISECONDS); - } catch (Exception e) { - // Ignore; we're shutting down - } - })); - } catch (IllegalStateException e) { - // ignore - may fail if shutdown is already in progress + SAVE_RUNNER.shutdownNow(); + SAVE_RUNNER.awaitTermination(100, TimeUnit.MILLISECONDS); + } catch (Exception e) { + // Ignore; we're shutting down } } @@ -379,6 +363,7 @@ public abstract class FS { private static FileStoreAttributes getFileStoreAttributes(Path dir) { FileStore s; + CompletableFuture<Optional<FileStoreAttributes>> f = null; try { if (Files.exists(dir)) { s = Files.getFileStore(dir); @@ -401,7 +386,7 @@ public abstract class FS { return FALLBACK_FILESTORE_ATTRIBUTES; } - CompletableFuture<Optional<FileStoreAttributes>> f = CompletableFuture + f = CompletableFuture .supplyAsync(() -> { Lock lock = locks.computeIfAbsent(s, l -> new ReentrantLock()); @@ -453,7 +438,7 @@ public abstract class FS { locks.remove(s); } return attributes; - }, FUTURE_RUNNER); + }); f = f.exceptionally(e -> { LOG.error(e.getLocalizedMessage(), e); return Optional.empty(); @@ -471,10 +456,13 @@ public abstract class FS { } // fall through and return fallback } catch (IOException | ExecutionException | CancellationException e) { + cancel(f); LOG.error(e.getMessage(), e); } catch (TimeoutException | SecurityException e) { + cancel(f); // use fallback } catch (InterruptedException e) { + cancel(f); LOG.error(e.getMessage(), e); Thread.currentThread().interrupt(); } @@ -483,6 +471,13 @@ public abstract class FS { return FALLBACK_FILESTORE_ATTRIBUTES; } + private static void cancel( + CompletableFuture<Optional<FileStoreAttributes>> f) { + if (f != null) { + f.cancel(true); + } + } + @SuppressWarnings("boxing") private static Duration measureMinimalRacyInterval(Path dir) { LOG.debug("{}: start measure minimal racy interval in {}", //$NON-NLS-1$ @@ -828,6 +823,8 @@ public abstract class FS { private Duration minimalRacyInterval; /** + * Get the minimal racy interval + * * @return the measured minimal interval after a file has been modified * in which we cannot rely on lastModified to detect * modifications @@ -837,6 +834,8 @@ public abstract class FS { } /** + * Get the measured filesystem timestamp resolution + * * @return the measured filesystem timestamp resolution */ @NonNull @@ -849,6 +848,7 @@ public abstract class FS { * timestamp resolution * * @param fsTimestampResolution + * resolution of filesystem timestamps */ public FileStoreAttributes( @NonNull Duration fsTimestampResolution) { @@ -883,21 +883,6 @@ public abstract class FS { } /** - * Whether FileStore attributes should be determined asynchronously - * - * @param asynch - * whether FileStore attributes should be determined - * asynchronously. If false access to cached attributes may block - * for some seconds for the first call per FileStore - * @since 5.1.9 - * @deprecated Use {@link FileStoreAttributes#setBackground} instead - */ - @Deprecated - public static void setAsyncFileStoreAttributes(boolean asynch) { - FileStoreAttributes.setBackground(asynch); - } - - /** * Auto-detect the appropriate file system abstraction, taking into account * the presence of a Cygwin installation on the system. Using jgit in * combination with Cygwin requires a more elaborate (and possibly slower) @@ -1009,7 +994,7 @@ public abstract class FS { File tempFile = null; try { tempFile = File.createTempFile("tempsymlinktarget", ""); //$NON-NLS-1$ //$NON-NLS-2$ - File linkName = new File(tempFile.getParentFile(), "tempsymlink"); //$NON-NLS-1$ + File linkName = new File(tempFile.getPath() + "-tempsymlink"); //$NON-NLS-1$ createSymLink(linkName, tempFile.getPath()); supportSymlinks = Boolean.TRUE; linkName.delete(); @@ -1070,23 +1055,6 @@ public abstract class FS { * symbolic links, the modification time of the link is returned, rather * than that of the link target. * - * @param f - * a {@link java.io.File} object. - * @return last modified time of f - * @throws java.io.IOException - * @since 3.0 - * @deprecated use {@link #lastModifiedInstant(Path)} instead - */ - @Deprecated - public long lastModified(File f) throws IOException { - return FileUtils.lastModified(f); - } - - /** - * Get the last modified time of a file system object. If the OS/JRE support - * symbolic links, the modification time of the link is returned, rather - * than that of the link target. - * * @param p * a {@link Path} object. * @return last modified time of p @@ -1115,29 +1083,12 @@ public abstract class FS { * <p> * For symlinks it sets the modified time of the link target. * - * @param f - * a {@link java.io.File} object. - * @param time - * last modified time - * @throws java.io.IOException - * @since 3.0 - * @deprecated use {@link #setLastModified(Path, Instant)} instead - */ - @Deprecated - public void setLastModified(File f, long time) throws IOException { - FileUtils.setLastModified(f, time); - } - - /** - * Set the last modified time of a file system object. - * <p> - * For symlinks it sets the modified time of the link target. - * * @param p * a {@link Path} object. * @param time * last modified time * @throws java.io.IOException + * if an IO error occurred * @since 5.1.9 */ public void setLastModified(Path p, Instant time) throws IOException { @@ -1152,6 +1103,7 @@ public abstract class FS { * a {@link java.io.File} object. * @return length of a file * @throws java.io.IOException + * if an IO error occurred * @since 3.0 */ public long length(File path) throws IOException { @@ -1164,7 +1116,7 @@ public abstract class FS { * @param f * a {@link java.io.File} object. * @throws java.io.IOException - * this may be a Java7 subclass with detailed information + * if an IO error occurred * @since 3.3 */ public void delete(File f) throws IOException { @@ -1264,8 +1216,10 @@ public abstract class FS { * Return all the attributes of a file, without following symbolic links. * * @param file + * the file * @return {@link BasicFileAttributes} of the file - * @throws IOException in case of any I/O errors accessing the file + * @throws IOException + * in case of any I/O errors accessing the file * * @since 4.5.6 */ @@ -1283,11 +1237,10 @@ public abstract class FS { } private File defaultUserHomeImpl() { - String home = AccessController.doPrivileged( - (PrivilegedAction<String>) () -> System.getProperty("user.home") //$NON-NLS-1$ - ); - if (home == null || home.length() == 0) + String home = SystemReader.getInstance().getProperty("user.home"); //$NON-NLS-1$ + if (StringUtils.isEmptyOrNull(home)) { return null; + } return new File(home).getAbsoluteFile(); } @@ -1303,8 +1256,10 @@ public abstract class FS { * @return the first match found, or null * @since 3.0 */ + @SuppressWarnings("StringSplitter") protected static File searchPath(String path, String... lookFor) { - if (path == null) { + if (StringUtils.isEmptyOrNull(path) + || EMPTY_PATH.matcher(path).find()) { return null; } @@ -1421,13 +1376,6 @@ public abstract class FS { } } catch (IOException e) { LOG.error("Caught exception in FS.readPipe()", e); //$NON-NLS-1$ - } catch (AccessControlException e) { - LOG.warn(MessageFormat.format( - JGitText.get().readPipeIsNotAllowedRequiredPermission, - command, dir, e.getPermission())); - } catch (SecurityException e) { - LOG.warn(MessageFormat.format(JGitText.get().readPipeIsNotAllowed, - command, dir)); } if (debug) { LOG.debug("readpipe returns null"); //$NON-NLS-1$ @@ -1671,6 +1619,7 @@ public abstract class FS { * a {@link java.io.File} object. * @return target of link or null * @throws java.io.IOException + * if an IO error occurred * @since 3.0 */ public String readSymLink(File path) throws IOException { @@ -1684,6 +1633,7 @@ public abstract class FS { * a {@link java.io.File} object. * @return true if the path is a symbolic link (and we support these) * @throws java.io.IOException + * if an IO error occurred * @since 3.0 */ public boolean isSymLink(File path) throws IOException { @@ -1738,6 +1688,7 @@ public abstract class FS { * @return true if path is hidden, either starts with . on unix or has the * hidden attribute in windows * @throws java.io.IOException + * if an IO error occurred * @since 3.0 */ public boolean isHidden(File path) throws IOException { @@ -1752,6 +1703,7 @@ public abstract class FS { * @param hidden * whether to set the file hidden * @throws java.io.IOException + * if an IO error occurred * @since 3.0 */ public void setHidden(File path, boolean hidden) throws IOException { @@ -1766,6 +1718,7 @@ public abstract class FS { * @param target * target path of the symlink * @throws java.io.IOException + * if an IO error occurred * @since 3.0 */ public void createSymLink(File path, String target) throws IOException { @@ -1773,24 +1726,6 @@ public abstract class FS { } /** - * Create a new file. See {@link java.io.File#createNewFile()}. Subclasses - * of this class may take care to provide a safe implementation for this - * even if {@link #supportsAtomicCreateNewFile()} is <code>false</code> - * - * @param path - * the file to be created - * @return <code>true</code> if the file was created, <code>false</code> if - * the file already existed - * @throws java.io.IOException - * @deprecated use {@link #createNewFileAtomic(File)} instead - * @since 4.5 - */ - @Deprecated - public boolean createNewFile(File path) throws IOException { - return path.createNewFile(); - } - - /** * A token representing a file created by * {@link #createNewFileAtomic(File)}. The token must be retained until the * file has been deleted in order to guarantee that the unique file was @@ -1810,6 +1745,8 @@ public abstract class FS { } /** + * Whether the file was created successfully + * * @return {@code true} if the file was created successfully */ public boolean isCreated() { @@ -1852,6 +1789,7 @@ public abstract class FS { * @return LockToken this token must be closed after the created file was * deleted * @throws IOException + * if an IO error occurred * @since 4.7 */ public LockToken createNewFileAtomic(File path) throws IOException { @@ -2011,6 +1949,8 @@ public abstract class FS { environment.put(Constants.GIT_DIR_KEY, repository.getDirectory().getAbsolutePath()); if (!repository.isBare()) { + environment.put(Constants.GIT_COMMON_DIR_KEY, + repository.getCommonDirectory().getAbsolutePath()); environment.put(Constants.GIT_WORK_TREE_KEY, repository.getWorkTree().getAbsolutePath()); } @@ -2106,7 +2046,7 @@ public abstract class FS { case "post-receive": //$NON-NLS-1$ case "post-update": //$NON-NLS-1$ case "push-to-checkout": //$NON-NLS-1$ - return repository.getDirectory(); + return repository.getCommonDirectory(); default: return repository.getWorkTree(); } @@ -2119,7 +2059,7 @@ public abstract class FS { if (hooksDir != null) { return new File(hooksDir); } - File dir = repository.getDirectory(); + File dir = repository.getCommonDirectory(); return dir == null ? null : new File(dir, Constants.HOOKS); } @@ -2317,7 +2257,9 @@ public abstract class FS { * The standard input stream passed to the process * @return The result of the executed command * @throws java.lang.InterruptedException + * if thread was interrupted * @throws java.io.IOException + * if an IO error occurred * @since 4.2 */ public ExecutionResult execute(ProcessBuilder pb, InputStream in) @@ -2346,6 +2288,8 @@ public abstract class FS { public static class Attributes { /** + * Whether this are attributes of a directory + * * @return true if this are the attributes of a directory */ public boolean isDirectory() { @@ -2353,6 +2297,8 @@ public abstract class FS { } /** + * Whether this are attributes of an executable file + * * @return true if this are the attributes of an executable file */ public boolean isExecutable() { @@ -2360,6 +2306,8 @@ public abstract class FS { } /** + * Whether this are the attributes of a symbolic link + * * @return true if this are the attributes of a symbolic link */ public boolean isSymbolicLink() { @@ -2367,6 +2315,8 @@ public abstract class FS { } /** + * Whether this are the attributes of a regular file + * * @return true if this are the attributes of a regular file */ public boolean isRegularFile() { @@ -2374,6 +2324,8 @@ public abstract class FS { } /** + * Get the file creation time + * * @return the time when the file was created */ public long getCreationTime() { @@ -2381,16 +2333,8 @@ public abstract class FS { } /** - * @return the time (milliseconds since 1970-01-01) when this object was - * last modified - * @deprecated use getLastModifiedInstant instead - */ - @Deprecated - public long getLastModifiedTime() { - return lastModifiedInstant.toEpochMilli(); - } - - /** + * Get the time when this object was last modified + * * @return the time when this object was last modified * @since 5.1.9 */ @@ -2441,14 +2385,18 @@ public abstract class FS { * Constructor when there are issues with reading. All attributes except * given will be set to the default values. * - * @param fs * @param path + * file path + * @param fs + * FS to use */ public Attributes(File path, FS fs) { this(fs, path, false, false, false, false, false, 0L, EPOCH, 0L); } /** + * Get the length of this file + * * @return length of this file object */ public long getLength() { @@ -2458,6 +2406,8 @@ public abstract class FS { } /** + * Get the filename + * * @return the filename */ public String getName() { @@ -2465,6 +2415,8 @@ public abstract class FS { } /** + * Get the file the attributes apply to + * * @return the file the attributes apply to */ public File getFile() { @@ -2522,6 +2474,33 @@ public abstract class FS { } /** + * Get common dir path. + * + * @param dir + * the .git folder + * @return common dir path + * @throws IOException + * if commondir file can't be read + * + * @since 7.0 + */ + public File getCommonDir(File dir) throws IOException { + // first the GIT_COMMON_DIR is same as GIT_DIR + File commonDir = dir; + // now check if commondir file exists (e.g. worktree repository) + File commonDirFile = new File(dir, Constants.COMMONDIR_FILE); + if (commonDirFile.isFile()) { + String commonDirPath = new String(IO.readFully(commonDirFile)) + .trim(); + commonDir = new File(commonDirPath); + if (!commonDir.isAbsolute()) { + commonDir = new File(dir, commonDirPath).getCanonicalFile(); + } + } + return commonDir; + } + + /** * This runnable will consume an input stream's content into an output * stream as soon as it gets available. * <p> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java index 1c113617f8..db2b5b4f71 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, Robin Rosenberg and others + * Copyright (C) 2010, 2024, Robin Rosenberg and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -203,7 +203,16 @@ public class FS_POSIX extends FS { /** {@inheritDoc} */ @Override public boolean canExecute(File f) { - return FileUtils.canExecute(f); + if (!isFile(f)) { + return false; + } + try { + Path path = FileUtils.toPath(f); + Set<PosixFilePermission> pset = Files.getPosixFilePermissions(path); + return pset.contains(PosixFilePermission.OWNER_EXECUTE); + } catch (IOException ex) { + return false; + } } /** {@inheritDoc} */ @@ -250,8 +259,12 @@ public class FS_POSIX extends FS { /** {@inheritDoc} */ @Override public ProcessBuilder runInShell(String cmd, String[] args) { - List<String> argv = new ArrayList<>(4 + args.length); + List<String> argv = new ArrayList<>(5 + args.length); argv.add("sh"); //$NON-NLS-1$ + if (SystemReader.getInstance().isMacOS()) { + // Use a login shell to get the full normal $PATH + argv.add("-l"); //$NON-NLS-1$ + } argv.add("-c"); //$NON-NLS-1$ argv.add(cmd + " \"$@\""); //$NON-NLS-1$ argv.add(cmd); @@ -328,73 +341,6 @@ public class FS_POSIX extends FS { return supportsAtomicFileCreation == AtomicFileCreation.SUPPORTED; } - @Override - @SuppressWarnings("boxing") - /** - * {@inheritDoc} - * <p> - * An implementation of the File#createNewFile() semantics which works also - * on NFS. If the config option - * {@code core.supportsAtomicCreateNewFile = true} (which is the default) - * then simply File#createNewFile() is called. - * - * But if {@code core.supportsAtomicCreateNewFile = false} then after - * successful creation of the lock file a hard link to that lock file is - * created and the attribute nlink of the lock file is checked to be 2. If - * multiple clients manage to create the same lock file nlink would be - * greater than 2 showing the error. - * - * @see "https://www.time-travellers.org/shane/papers/NFS_considered_harmful.html" - * - * @deprecated use {@link FS_POSIX#createNewFileAtomic(File)} instead - * @since 4.5 - */ - @Deprecated - public boolean createNewFile(File lock) throws IOException { - if (!lock.createNewFile()) { - return false; - } - if (supportsAtomicCreateNewFile()) { - return true; - } - Path lockPath = lock.toPath(); - Path link = null; - FileStore store = null; - try { - store = Files.getFileStore(lockPath); - } catch (SecurityException e) { - return true; - } - try { - Boolean canLink = CAN_HARD_LINK.computeIfAbsent(store, - s -> Boolean.TRUE); - if (Boolean.FALSE.equals(canLink)) { - return true; - } - link = Files.createLink( - Paths.get(lock.getAbsolutePath() + ".lnk"), //$NON-NLS-1$ - lockPath); - Integer nlink = (Integer) (Files.getAttribute(lockPath, - "unix:nlink")); //$NON-NLS-1$ - if (nlink > 2) { - LOG.warn(MessageFormat.format( - JGitText.get().failedAtomicFileCreation, lockPath, - nlink)); - return false; - } else if (nlink < 2) { - CAN_HARD_LINK.put(store, Boolean.FALSE); - } - return true; - } catch (UnsupportedOperationException | IllegalArgumentException e) { - CAN_HARD_LINK.put(store, Boolean.FALSE); - return true; - } finally { - if (link != null) { - Files.delete(link); - } - } - } - /** * {@inheritDoc} * <p> @@ -418,6 +364,7 @@ public class FS_POSIX extends FS { * @return LockToken this lock token must be held until the file is no * longer needed * @throws IOException + * if an IO error occurred * @since 5.0 */ @Override @@ -446,8 +393,7 @@ public class FS_POSIX extends FS { return token(true, null); } link = Files.createLink(Paths.get(uniqueLinkPath(file)), path); - Integer nlink = (Integer) (Files.getAttribute(path, - "unix:nlink")); //$NON-NLS-1$ + Integer nlink = (Integer) Files.getAttribute(path, "unix:nlink"); //$NON-NLS-1$ if (nlink.intValue() > 2) { LOG.warn(MessageFormat.format( JGitText.get().failedAtomicFileCreation, path, nlink)); 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 index ae73d3feb8..5926655b7b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java @@ -58,43 +58,36 @@ public class FS_Win32 extends FS { super(src); } - /** {@inheritDoc} */ @Override public FS newInstance() { return new FS_Win32(this); } - /** {@inheritDoc} */ @Override public boolean supportsExecute() { return false; } - /** {@inheritDoc} */ @Override public boolean canExecute(File f) { return false; } - /** {@inheritDoc} */ @Override public boolean setExecute(File f, boolean canExec) { return false; } - /** {@inheritDoc} */ @Override public boolean isCaseSensitive() { return false; } - /** {@inheritDoc} */ @Override public boolean retryFailedLockFileCommit() { return true; } - /** {@inheritDoc} */ @Override public Entry[] list(File directory, FileModeStrategy fileModeStrategy) { if (!Files.isDirectory(directory.toPath(), LinkOption.NOFOLLOW_LINKS)) { @@ -140,7 +133,6 @@ public class FS_Win32 extends FS { return result.toArray(new Entry[0]); } - /** {@inheritDoc} */ @Override protected File discoverGitExe() { String path = SystemReader.getInstance().getenv("PATH"); //$NON-NLS-1$ @@ -171,7 +163,6 @@ public class FS_Win32 extends FS { return gitExe; } - /** {@inheritDoc} */ @Override protected File userHomeImpl() { String home = SystemReader.getInstance().getenv("HOME"); //$NON-NLS-1$ @@ -194,7 +185,6 @@ public class FS_Win32 extends FS { return super.userHomeImpl(); } - /** {@inheritDoc} */ @Override public ProcessBuilder runInShell(String cmd, String[] args) { List<String> argv = new ArrayList<>(3 + args.length); @@ -207,7 +197,6 @@ public class FS_Win32 extends FS { return proc; } - /** {@inheritDoc} */ @Override public Attributes getAttributes(File path) { return FileUtils.getFileAttributesBasic(this, path); 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 index add5498175..237879110a 100644 --- 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 @@ -14,8 +14,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; import java.io.OutputStream; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -43,10 +41,7 @@ public class FS_Win32_Cygwin extends FS_Win32 { * @return true if cygwin is found */ public static boolean isCygwin() { - final String path = AccessController - .doPrivileged((PrivilegedAction<String>) () -> System - .getProperty("java.library.path") //$NON-NLS-1$ - ); + final String path = System.getProperty("java.library.path"); //$NON-NLS-1$ if (path == null) return false; File found = FS.searchPath(path, "cygpath.exe"); //$NON-NLS-1$ @@ -72,13 +67,11 @@ public class FS_Win32_Cygwin extends FS_Win32 { super(src); } - /** {@inheritDoc} */ @Override public FS newInstance() { return new FS_Win32_Cygwin(this); } - /** {@inheritDoc} */ @Override public File resolve(File dir, String pn) { String useCygPath = System.getProperty("jgit.usecygpath"); //$NON-NLS-1$ @@ -99,18 +92,14 @@ public class FS_Win32_Cygwin extends FS_Win32 { return super.resolve(dir, pn); } - /** {@inheritDoc} */ @Override protected File userHomeImpl() { - final String home = AccessController.doPrivileged( - (PrivilegedAction<String>) () -> System.getenv("HOME") //$NON-NLS-1$ - ); + final String home = System.getenv("HOME"); //$NON-NLS-1$ if (home == null || home.length() == 0) return super.userHomeImpl(); return resolve(new File("."), home); //$NON-NLS-1$ } - /** {@inheritDoc} */ @Override public ProcessBuilder runInShell(String cmd, String[] args) { List<String> argv = new ArrayList<>(4 + args.length); @@ -129,14 +118,12 @@ public class FS_Win32_Cygwin extends FS_Win32 { return QuotedString.BOURNE.quote(cmd.replace(File.separatorChar, '/')); } - /** {@inheritDoc} */ @Override public String relativize(String base, String other) { final String relativized = super.relativize(base, other); return relativized.replace(File.separatorChar, '/'); } - /** {@inheritDoc} */ @Override public ProcessResult runHookIfPresent(Repository repository, String hookName, String[] args, OutputStream outRedirect, OutputStream errRedirect, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java index f013e7e095..39c67f1b86 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java @@ -288,12 +288,14 @@ public class FileUtils { * @throws java.nio.file.AtomicMoveNotSupportedException * if file cannot be moved as an atomic file system operation * @throws java.io.IOException + * if an IO error occurred * @since 4.1 */ public static void rename(final File src, final File dst, CopyOption... options) throws AtomicMoveNotSupportedException, IOException { int attempts = FS.DETECTED.retryFailedLockFileCommit() ? 10 : 1; + IOException finalError = null; while (--attempts >= 0) { try { Files.move(toPath(src), toPath(dst), options); @@ -301,29 +303,35 @@ public class FileUtils { } catch (AtomicMoveNotSupportedException e) { throw e; } catch (IOException e) { - try { - if (!dst.delete()) { - delete(dst, EMPTY_DIRECTORIES_ONLY | RECURSIVE); + if (attempts == 0) { + // Only delete on the last attempt. + try { + if (!dst.delete()) { + delete(dst, EMPTY_DIRECTORIES_ONLY | RECURSIVE); + } + // On *nix there is no try, you do or do not + Files.move(toPath(src), toPath(dst), options); + return; + } catch (IOException e2) { + e2.addSuppressed(e); + finalError = e2; } - // On *nix there is no try, you do or do not - Files.move(toPath(src), toPath(dst), options); - return; - } catch (IOException e2) { - // ignore and continue retry } } - try { - Thread.sleep(100); - } catch (InterruptedException e) { - throw new IOException( - MessageFormat.format(JGitText.get().renameFileFailed, - src.getAbsolutePath(), dst.getAbsolutePath()), - e); + if (attempts > 0) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new IOException(MessageFormat.format( + JGitText.get().renameFileFailed, + src.getAbsolutePath(), dst.getAbsolutePath()), e); + } } } throw new IOException( MessageFormat.format(JGitText.get().renameFileFailed, - src.getAbsolutePath(), dst.getAbsolutePath())); + src.getAbsolutePath(), dst.getAbsolutePath()), + finalError); } /** @@ -446,6 +454,7 @@ public class FileUtils { * the target of the symbolic link * @return the path to the symbolic link * @throws java.io.IOException + * if an IO error occurred * @since 4.2 */ public static Path createSymLink(File path, String target) @@ -474,6 +483,7 @@ public class FileUtils { * a {@link java.io.File} object. * @return target path of the symlink, or null if it is not a symbolic link * @throws java.io.IOException + * if an IO error occurred * @since 3.0 */ public static String readSymLink(File path) throws IOException { @@ -499,6 +509,7 @@ public class FileUtils { * The parent dir, can be null to use system default temp dir. * @return the temp dir created. * @throws java.io.IOException + * if an IO error occurred * @since 3.4 */ public static File createTempDir(String prefix, String suffix, File dir) @@ -620,11 +631,11 @@ public class FileUtils { } /** - * Determine if an IOException is a Stale NFS File Handle + * Determine if an IOException is a stale NFS file handle * * @param ioe * an {@link java.io.IOException} object. - * @return a boolean true if the IOException is a Stale NFS FIle Handle + * @return a boolean true if the IOException is a stale NFS file handle * @since 4.1 */ public static boolean isStaleFileHandle(IOException ioe) { @@ -635,13 +646,13 @@ public class FileUtils { } /** - * Determine if a throwable or a cause in its causal chain is a Stale NFS - * File Handle + * Determine if a throwable or a cause in its causal chain is a stale NFS + * file handle * * @param throwable * a {@link java.lang.Throwable} object. * @return a boolean true if the throwable or a cause in its causal chain is - * a Stale NFS File Handle + * a stale NFS file handle * @since 4.7 */ public static boolean isStaleFileHandleInCausalChain(Throwable throwable) { @@ -749,7 +760,10 @@ public class FileUtils { } /** + * Check if file is a symlink + * * @param file + * the file to be checked if it is a symbolic link * @return {@code true} if the passed file is a symbolic link */ static boolean isSymlink(File file) { @@ -757,21 +771,10 @@ public class FileUtils { } /** - * @param file - * @return lastModified attribute for given file, not following symbolic - * links - * @throws IOException - * @deprecated use {@link #lastModifiedInstant(Path)} instead which returns - * FileTime - */ - @Deprecated - static long lastModified(File file) throws IOException { - return Files.getLastModifiedTime(toPath(file), LinkOption.NOFOLLOW_LINKS) - .toMillis(); - } - - /** + * Get last modified timestamp of a file + * * @param path + * file path * @return lastModified attribute for given file, not following symbolic * links */ @@ -795,8 +798,10 @@ public class FileUtils { * Return all the attributes of a file, without following symbolic links. * * @param file + * the file * @return {@link BasicFileAttributes} of the file - * @throws IOException in case of any I/O errors accessing the file + * @throws IOException + * in case of any I/O errors accessing the file * * @since 4.5.6 */ @@ -807,21 +812,12 @@ public class FileUtils { /** * Set the last modified time of a file system object. * - * @param file - * @param time - * @throws IOException - */ - @Deprecated - static void setLastModified(File file, long time) throws IOException { - Files.setLastModifiedTime(toPath(file), FileTime.fromMillis(time)); - } - - /** - * Set the last modified time of a file system object. - * * @param path + * file path * @param time + * last modified timestamp of the file * @throws IOException + * if an IO error occurred */ static void setLastModified(Path path, Instant time) throws IOException { @@ -829,7 +825,10 @@ public class FileUtils { } /** + * Whether the file exists + * * @param file + * the file * @return {@code true} if the given file exists, not following symbolic * links */ @@ -838,9 +837,13 @@ public class FileUtils { } /** + * Check if file is hidden (on Windows) + * * @param file + * the file * @return {@code true} if the given file is hidden * @throws IOException + * if an IO error occurred */ static boolean isHidden(File file) throws IOException { return Files.isHidden(toPath(file)); @@ -854,6 +857,7 @@ public class FileUtils { * @param hidden * a boolean. * @throws java.io.IOException + * if an IO error occurred * @since 4.1 */ public static void setHidden(File file, boolean hidden) throws IOException { @@ -868,6 +872,7 @@ public class FileUtils { * a {@link java.io.File}. * @return length of the given file * @throws java.io.IOException + * if an IO error occurred * @since 4.1 */ public static long getLength(File file) throws IOException { @@ -879,7 +884,10 @@ public class FileUtils { } /** + * Check if file is directory + * * @param file + * the file * @return {@code true} if the given file is a directory, not following * symbolic links */ @@ -888,7 +896,10 @@ public class FileUtils { } /** + * Check if File is a file + * * @param file + * the file * @return {@code true} if the given file is a file, not following symbolic * links */ @@ -929,8 +940,12 @@ public class FileUtils { } /** + * Get basic file attributes + * * @param fs + * a {@link org.eclipse.jgit.util.FS} object. * @param file + * the file * @return non null attributes object */ static Attributes getFileAttributesBasic(FS fs, File file) { @@ -1079,6 +1094,7 @@ public class FileUtils { * @param f * the file to touch * @throws IOException + * if an IO error occurred * @since 5.1.8 */ public static void touch(Path f) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GSSManagerFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GSSManagerFactory.java index be56e5ecf5..ba0df932ce 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/GSSManagerFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GSSManagerFactory.java @@ -28,7 +28,7 @@ public abstract class GSSManagerFactory { * @return detected GSSManager factory */ public static GSSManagerFactory detect() { - return (SunGSSManagerFactory.isSupported()) ? new SunGSSManagerFactory() + return SunGSSManagerFactory.isSupported() ? new SunGSSManagerFactory() : new DefaultGSSManagerFactory(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java index e6bf497ac4..332e65985e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java @@ -10,10 +10,10 @@ package org.eclipse.jgit.util; -import java.text.DateFormat; -import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; import java.util.Locale; -import java.util.TimeZone; import org.eclipse.jgit.lib.PersonIdent; @@ -26,9 +26,9 @@ import org.eclipse.jgit.lib.PersonIdent; */ public class GitDateFormatter { - private DateFormat dateTimeInstance; + private DateTimeFormatter dateTimeFormat; - private DateFormat dateTimeInstance2; + private DateTimeFormatter dateTimeFormat2; private final Format format; @@ -96,30 +96,34 @@ public class GitDateFormatter { default: break; case DEFAULT: // Not default: - dateTimeInstance = new SimpleDateFormat( + dateTimeFormat = DateTimeFormatter.ofPattern( "EEE MMM dd HH:mm:ss yyyy Z", Locale.US); //$NON-NLS-1$ break; case ISO: - dateTimeInstance = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", //$NON-NLS-1$ + dateTimeFormat = DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss Z", //$NON-NLS-1$ Locale.US); break; case LOCAL: - dateTimeInstance = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", //$NON-NLS-1$ + dateTimeFormat = DateTimeFormatter.ofPattern( + "EEE MMM dd HH:mm:ss yyyy", //$NON-NLS-1$ Locale.US); break; case RFC: - dateTimeInstance = new SimpleDateFormat( + dateTimeFormat = DateTimeFormatter.ofPattern( "EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); //$NON-NLS-1$ break; case SHORT: - dateTimeInstance = new SimpleDateFormat("yyyy-MM-dd", Locale.US); //$NON-NLS-1$ + dateTimeFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd", //$NON-NLS-1$ + Locale.US); break; case LOCALE: case LOCALELOCAL: - SystemReader systemReader = SystemReader.getInstance(); - dateTimeInstance = systemReader.getDateTimeInstance( - DateFormat.DEFAULT, DateFormat.DEFAULT); - dateTimeInstance2 = systemReader.getSimpleDateFormat("Z"); //$NON-NLS-1$ + dateTimeFormat = DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.MEDIUM) + .withLocale(Locale.US); + dateTimeFormat2 = DateTimeFormatter.ofPattern("Z", //$NON-NLS-1$ + Locale.US); break; } } @@ -135,39 +139,45 @@ public class GitDateFormatter { @SuppressWarnings("boxing") public String formatDate(PersonIdent ident) { switch (format) { - case RAW: - int offset = ident.getTimeZoneOffset(); + case RAW: { + int offset = ident.getZoneOffset().getTotalSeconds(); String sign = offset < 0 ? "-" : "+"; //$NON-NLS-1$ //$NON-NLS-2$ int offset2; - if (offset < 0) + if (offset < 0) { offset2 = -offset; - else + } else { offset2 = offset; - int hours = offset2 / 60; - int minutes = offset2 % 60; + } + int minutes = (offset2 / 60) % 60; + int hours = offset2 / 60 / 60; return String.format("%d %s%02d%02d", //$NON-NLS-1$ - ident.getWhen().getTime() / 1000, sign, hours, minutes); + ident.getWhenAsInstant().getEpochSecond(), sign, hours, + minutes); + } case RELATIVE: - return RelativeDateFormatter.format(ident.getWhen()); + return RelativeDateFormatter.format(ident.getWhenAsInstant()); case LOCALELOCAL: case LOCAL: - dateTimeInstance.setTimeZone(SystemReader.getInstance() - .getTimeZone()); - return dateTimeInstance.format(ident.getWhen()); - case LOCALE: - TimeZone tz = ident.getTimeZone(); - if (tz == null) - tz = SystemReader.getInstance().getTimeZone(); - dateTimeInstance.setTimeZone(tz); - dateTimeInstance2.setTimeZone(tz); - return dateTimeInstance.format(ident.getWhen()) + " " //$NON-NLS-1$ - + dateTimeInstance2.format(ident.getWhen()); - default: - tz = ident.getTimeZone(); - if (tz == null) - tz = SystemReader.getInstance().getTimeZone(); - dateTimeInstance.setTimeZone(ident.getTimeZone()); - return dateTimeInstance.format(ident.getWhen()); + return dateTimeFormat + .withZone(SystemReader.getInstance().getTimeZoneId()) + .format(ident.getWhenAsInstant()); + case LOCALE: { + ZoneId tz = ident.getZoneId(); + if (tz == null) { + tz = SystemReader.getInstance().getTimeZoneId(); + } + return dateTimeFormat.withZone(tz).format(ident.getWhenAsInstant()) + + " " //$NON-NLS-1$ + + dateTimeFormat2.withZone(tz) + .format(ident.getWhenAsInstant()); + } + default: { + ZoneId tz = ident.getZoneId(); + if (tz == null) { + tz = SystemReader.getInstance().getTimeZoneId(); + } + return dateTimeFormat.withZone(tz).format(ident.getWhenAsInstant()); + } } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java index 6a4b39652a..f080056546 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java @@ -28,7 +28,10 @@ import org.eclipse.jgit.internal.JGitText; * used. One example is the parsing of the config parameter gc.pruneexpire. The * parser can handle only subset of what native gits approxidate parser * understands. + * + * @deprecated Use {@link GitTimeParser} instead. */ +@Deprecated(since = "7.1") public class GitDateParser { /** * The Date representing never. Though this is a concrete value, most diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitTimeParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitTimeParser.java new file mode 100644 index 0000000000..acaa1ce563 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitTimeParser.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2024 Christian Halstrick and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.util; + +import java.text.MessageFormat; +import java.text.ParseException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.EnumMap; +import java.util.Map; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.JGitText; + +/** + * Parses strings with time and date specifications into + * {@link java.time.Instant}. + * + * When git needs to parse strings specified by the user this parser can be + * used. One example is the parsing of the config parameter gc.pruneexpire. The + * parser can handle only subset of what native gits approxidate parser + * understands. + * + * @since 7.1 + */ +public class GitTimeParser { + + private static final Map<ParseableSimpleDateFormat, DateTimeFormatter> formatCache = new EnumMap<>( + ParseableSimpleDateFormat.class); + + // An enum of all those formats which this parser can parse with the help of + // a DateTimeFormatter. There are other formats (e.g. the relative formats + // like "yesterday" or "1 week ago") which this parser can parse but which + // are not listed here because they are parsed without the help of a + // DateTimeFormatter. + enum ParseableSimpleDateFormat { + ISO("yyyy-MM-dd HH:mm:ss Z"), // //$NON-NLS-1$ + RFC("EEE, dd MMM yyyy HH:mm:ss Z"), // //$NON-NLS-1$ + SHORT("yyyy-MM-dd"), // //$NON-NLS-1$ + SHORT_WITH_DOTS_REVERSE("dd.MM.yyyy"), // //$NON-NLS-1$ + SHORT_WITH_DOTS("yyyy.MM.dd"), // //$NON-NLS-1$ + SHORT_WITH_SLASH("MM/dd/yyyy"), // //$NON-NLS-1$ + DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), // //$NON-NLS-1$ + LOCAL("EEE MMM dd HH:mm:ss yyyy"); //$NON-NLS-1$ + + private final String formatStr; + + ParseableSimpleDateFormat(String formatStr) { + this.formatStr = formatStr; + } + } + + private GitTimeParser() { + // This class is not supposed to be instantiated + } + + /** + * Parses a string into a {@link java.time.LocalDateTime} using the default + * locale. Since this parser also supports relative formats (e.g. + * "yesterday") the caller can specify the reference date. These types of + * strings can be parsed: + * <ul> + * <li>"never"</li> + * <li>"now"</li> + * <li>"yesterday"</li> + * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br> + * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of ' + * ' one can use '.' to separate the words</li> + * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li> + * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li> + * <li>"yyyy-MM-dd"</li> + * <li>"yyyy.MM.dd"</li> + * <li>"MM/dd/yyyy",</li> + * <li>"dd.MM.yyyy"</li> + * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li> + * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li> + * </ul> + * + * @param dateStr + * the string to be parsed + * @return the parsed {@link java.time.LocalDateTime} + * @throws java.text.ParseException + * if the given dateStr was not recognized + */ + public static LocalDateTime parse(String dateStr) throws ParseException { + return parse(dateStr, SystemReader.getInstance().civilNow()); + } + + /** + * Parses a string into a {@link java.time.Instant} using the default + * locale. Since this parser also supports relative formats (e.g. + * "yesterday") the caller can specify the reference date. These types of + * strings can be parsed: + * <ul> + * <li>"never"</li> + * <li>"now"</li> + * <li>"yesterday"</li> + * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br> + * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of ' + * ' one can use '.' to separate the words</li> + * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li> + * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li> + * <li>"yyyy-MM-dd"</li> + * <li>"yyyy.MM.dd"</li> + * <li>"MM/dd/yyyy",</li> + * <li>"dd.MM.yyyy"</li> + * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li> + * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li> + * </ul> + * + * @param dateStr + * the string to be parsed + * @return the parsed {@link java.time.Instant} + * @throws java.text.ParseException + * if the given dateStr was not recognized + * @since 7.2 + */ + public static Instant parseInstant(String dateStr) throws ParseException { + return parse(dateStr).atZone(SystemReader.getInstance().getTimeZoneId()) + .toInstant(); + } + + // Only tests seem to use this method + static LocalDateTime parse(String dateStr, LocalDateTime now) + throws ParseException { + dateStr = dateStr.trim(); + + if (dateStr.equalsIgnoreCase("never")) { //$NON-NLS-1$ + return LocalDateTime.MAX; + } + LocalDateTime ret = parseRelative(dateStr, now); + if (ret != null) { + return ret; + } + for (ParseableSimpleDateFormat f : ParseableSimpleDateFormat.values()) { + try { + return parseSimple(dateStr, f); + } catch (DateTimeParseException e) { + // simply proceed with the next parser + } + } + ParseableSimpleDateFormat[] values = ParseableSimpleDateFormat.values(); + StringBuilder allFormats = new StringBuilder("\"") //$NON-NLS-1$ + .append(values[0].formatStr); + for (int i = 1; i < values.length; i++) { + allFormats.append("\", \"").append(values[i].formatStr); //$NON-NLS-1$ + } + allFormats.append("\""); //$NON-NLS-1$ + throw new ParseException( + MessageFormat.format(JGitText.get().cannotParseDate, dateStr, + allFormats.toString()), + 0); + } + + // tries to parse a string with the formats supported by DateTimeFormatter + private static LocalDateTime parseSimple(String dateStr, + ParseableSimpleDateFormat f) throws DateTimeParseException { + DateTimeFormatter dateFormat = formatCache.computeIfAbsent(f, + format -> DateTimeFormatter + .ofPattern(f.formatStr) + .withLocale(SystemReader.getInstance().getLocale())); + TemporalAccessor parsed = dateFormat.parse(dateStr); + return parsed.isSupported(ChronoField.HOUR_OF_DAY) + ? LocalDateTime.from(parsed) + : LocalDate.from(parsed).atStartOfDay(); + } + + // tries to parse a string with a relative time specification + @SuppressWarnings("nls") + @Nullable + private static LocalDateTime parseRelative(String dateStr, + LocalDateTime now) { + // check for the static words "yesterday" or "now" + if (dateStr.equals("now")) { + return now; + } + + if (dateStr.equals("yesterday")) { + return now.minusDays(1); + } + + // parse constructs like "3 days ago", "5.week.2.day.ago" + String[] parts = dateStr.split("\\.| ", -1); + int partsLength = parts.length; + // check we have an odd number of parts (at least 3) and that the last + // part is "ago" + if (partsLength < 3 || (partsLength & 1) == 0 + || !parts[parts.length - 1].equals("ago")) { + return null; + } + int number; + for (int i = 0; i < parts.length - 2; i += 2) { + try { + number = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + return null; + } + if (parts[i + 1] == null) { + return null; + } + switch (parts[i + 1]) { + case "year": + case "years": + now = now.minusYears(number); + break; + case "month": + case "months": + now = now.minusMonths(number); + break; + case "week": + case "weeks": + now = now.minusWeeks(number); + break; + case "day": + case "days": + now = now.minusDays(number); + break; + case "hour": + case "hours": + now = now.minusHours(number); + break; + case "minute": + case "minutes": + now = now.minusMinutes(number); + break; + case "second": + case "seconds": + now = now.minusSeconds(number); + break; + default: + return null; + } + } + return now; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java index e3ba606346..1942342c45 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java @@ -321,6 +321,7 @@ public class HttpSupport { * a {@link org.eclipse.jgit.transport.http.HttpConnection} * object. * @throws java.io.IOException + * if an IO error occurred * @since 4.3 */ public static void disableSslVerify(HttpConnection conn) @@ -346,7 +347,9 @@ public class HttpSupport { * that have all available protocols enabled already, up to the one * specified. * <p> + * <br> * <table> + * <caption>TLS versions</caption> * <tr> * <td>SSLContext.getInstance()</td> * <td>OpenJDK</td> @@ -354,16 +357,16 @@ public class HttpSupport { * </tr> * <tr> * <td>"TLS"</td> - * <td>Supported: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)<br /> + * <td>Supported: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)<br> * Enabled: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)</td> - * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br /> + * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br> * Enabled: TLSv1</td> * </tr> * <tr> * <td>"TLSv1.2"</td> - * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br /> + * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br> * Enabled: TLSv1, TLSV1.1, TLSv1.2</td> - * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br /> + * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br> * Enabled: TLSv1.2</td> * </tr> * </table> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java index 80877bbdc6..8cc5316271 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java @@ -66,19 +66,7 @@ public class IO { public static final byte[] readSome(File path, int limit) throws FileNotFoundException, IOException { try (SilentFileInputStream in = new SilentFileInputStream(path)) { - byte[] buf = new byte[limit]; - int cnt = 0; - for (;;) { - int n = in.read(buf, cnt, buf.length - cnt); - if (n <= 0) - break; - cnt += n; - } - if (cnt == buf.length) - return buf; - byte[] res = new byte[cnt]; - System.arraycopy(buf, 0, res, 0, cnt); - return res; + return in.readNBytes(limit); } } @@ -99,37 +87,10 @@ public class IO { public static final byte[] readFully(File path, int max) throws FileNotFoundException, IOException { try (SilentFileInputStream in = new SilentFileInputStream(path)) { - long sz = Math.max(path.length(), 1); - if (sz > max) + byte[] buf = in.readNBytes(max); + if (in.read() != -1) { throw new IOException(MessageFormat.format( JGitText.get().fileIsTooLarge, path)); - - byte[] buf = new byte[(int) sz]; - int valid = 0; - for (;;) { - if (buf.length == valid) { - if (buf.length == max) { - int next = in.read(); - if (next < 0) - break; - - throw new IOException(MessageFormat.format( - JGitText.get().fileIsTooLarge, path)); - } - - byte[] nb = new byte[Math.min(buf.length * 2, max)]; - System.arraycopy(buf, 0, nb, 0, valid); - buf = nb; - } - int n = in.read(buf, valid, buf.length - valid); - if (n < 0) - break; - valid += n; - } - if (valid < buf.length) { - byte[] nb = new byte[valid]; - System.arraycopy(buf, 0, nb, 0, valid); - buf = nb; } return buf; } @@ -157,26 +118,7 @@ public class IO { */ public static ByteBuffer readWholeStream(InputStream in, int sizeHint) throws IOException { - byte[] out = new byte[sizeHint]; - int pos = 0; - while (pos < out.length) { - int read = in.read(out, pos, out.length - pos); - if (read < 0) - return ByteBuffer.wrap(out, 0, pos); - pos += read; - } - - int last = in.read(); - if (last < 0) - return ByteBuffer.wrap(out, 0, pos); - - try (TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap( - Integer.MAX_VALUE)) { - tmp.write(out); - tmp.write(last); - tmp.copy(in); - return ByteBuffer.wrap(tmp.toByteArray()); - } + return ByteBuffer.wrap(in.readAllBytes()); } /** @@ -197,13 +139,9 @@ public class IO { */ 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(JGitText.get().shortReadOfBlock); - off += r; - len -= r; - } + int read = fd.readNBytes(dst, off, len); + if (read != len) + throw new EOFException(JGitText.get().shortReadOfBlock); } /** @@ -271,14 +209,7 @@ public class IO { */ public static int readFully(InputStream fd, byte[] dst, int off) throws IOException { - int r; - int len = 0; - while (off < dst.length - && (r = fd.read(dst, off, dst.length - off)) >= 0) { - off += r; - len += r; - } - return len; + return fd.readNBytes(dst, off, dst.length - off); } /** @@ -300,6 +231,7 @@ public class IO { */ public static void skipFully(InputStream fd, long toSkip) throws IOException { + // same as fd.skipNBytes(toSkip) of JDK 12; while (toSkip > 0) { final long r = fd.skip(toSkip); if (r <= 0) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java index cc4f0a46fe..6a5190c6a2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java @@ -204,7 +204,6 @@ public class IntList { entries = n; } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); @@ -220,6 +219,8 @@ public class IntList { /** * A comparator of primitive ints. + * + * @since 6.6 */ public interface IntComparator { @@ -230,8 +231,8 @@ public class IntList { * the first int to compare * @param second * the second int to compare - * @return a negative number if first < second, 0 if first == second, or - * a positive number if first > second + * @return a negative number if first < second, 0 if first == second, or + * a positive number if first > second */ int compare(int first, int second); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Iterators.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Iterators.java new file mode 100644 index 0000000000..74b728bdf7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Iterators.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025, NVIDIA Corporation. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.util; + +import java.util.Iterator; + +/** + * Utility class for Iterators + * + * @since 6.10.2 + */ +public class Iterators { + /** + * Create an iterator which traverses an array in reverse. + * + * @param array T[] + * @return Iterator<T> + */ + public static <T> Iterator<T> reverseIterator(T[] array) { + return new Iterator<>() { + int index = array.length; + + @Override + public boolean hasNext() { + return index > 0; + } + + @Override + public T next() { + return array[--index]; + } + }; + } + + /** + * Make an iterable for easy use in modern for loops. + * + * @param iterator Iterator<T> + * @return Iterable<T> + */ + public static <T> Iterable<T> iterable(Iterator<T> iterator) { + return new Iterable<>() { + @Override + public Iterator<T> iterator() { + return iterator; + } + }; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java index 7456c71f5f..7b7c1d0886 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java @@ -43,6 +43,8 @@ public class LfsFactory { } /** + * Get the LFS factory instance + * * @return the current LFS implementation */ public static LfsFactory getInstance() { @@ -50,6 +52,8 @@ public class LfsFactory { } /** + * Set the LFS factory instance + * * @param instance * register a {@link LfsFactory} instance as the * {@link LfsFactory} implementation to use. @@ -59,6 +63,8 @@ public class LfsFactory { } /** + * Whether LFS support is available + * * @return whether LFS support is available */ public boolean isAvailable() { @@ -105,6 +111,7 @@ public class LfsFactory { * @return a loader for the actual data of a blob, or the original loader in * case LFS is not applicable. * @throws IOException + * if an IO error occurred */ public ObjectLoader applySmudgeFilter(Repository db, ObjectLoader loader, Attribute attribute) throws IOException { @@ -117,6 +124,7 @@ public class LfsFactory { * @param repo * the {@link Repository} the hook is applied to. * @param outputStream + * output stream * @return a {@link PrePushHook} implementation or <code>null</code> */ @Nullable @@ -131,7 +139,9 @@ public class LfsFactory { * @param repo * the {@link Repository} the hook is applied to. * @param outputStream + * output stream * @param errorStream + * error stream * @return a {@link PrePushHook} implementation or <code>null</code> * @since 5.6 */ @@ -153,6 +163,8 @@ public class LfsFactory { } /** + * Whether LFS is enabled + * * @param db * the repository to check * @return whether LFS is enabled for the given repository locally or @@ -163,6 +175,8 @@ public class LfsFactory { } /** + * Get git attributes for given path + * * @param db * the repository * @param path @@ -285,6 +299,8 @@ public class LfsFactory { } /** + * Get stream length + * * @return the length of the stream */ public long getLength() { @@ -298,6 +314,8 @@ public class LfsFactory { */ public interface LfsInstallCommand extends Callable<Void> { /** + * Set the repository to enable LFS for + * * @param repo * the repository to enable support for. * @return The {@link LfsInstallCommand} for chaining. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/LongList.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/LongList.java index b2bdfc1fd7..47f38f4627 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/LongList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/LongList.java @@ -140,7 +140,6 @@ public class LongList { entries = n; } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java index 7b4ff7ff16..fea7172d84 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java @@ -133,7 +133,7 @@ public final class NB { * @since 3.0 */ public static long decodeInt64(final byte[] intbuf, final int offset) { - long r = intbuf[offset] << 8; + long r = (long) intbuf[offset] << 8; r |= intbuf[offset + 1] & 0xff; r <<= 8; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawCharSequence.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawCharSequence.java index 3de7a1587c..be4bd9e356 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawCharSequence.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawCharSequence.java @@ -41,25 +41,21 @@ public final class RawCharSequence implements CharSequence { endPtr = end; } - /** {@inheritDoc} */ @Override public char charAt(int index) { return (char) (buffer[startPtr + index] & 0xff); } - /** {@inheritDoc} */ @Override public int length() { return endPtr - startPtr; } - /** {@inheritDoc} */ @Override public CharSequence subSequence(int start, int end) { return new RawCharSequence(buffer, startPtr + start, startPtr + end); } - /** {@inheritDoc} */ @Override public String toString() { final int n = length(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java index 0e8e9b3d84..3ed72516c7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java @@ -13,10 +13,17 @@ package org.eclipse.jgit.util; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.Instant.EPOCH; +import static java.time.ZoneOffset.UTC; 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.object; +import static org.eclipse.jgit.lib.ObjectChecker.parent; +import static org.eclipse.jgit.lib.ObjectChecker.tag; import static org.eclipse.jgit.lib.ObjectChecker.tagger; +import static org.eclipse.jgit.lib.ObjectChecker.tree; +import static org.eclipse.jgit.lib.ObjectChecker.type; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; @@ -25,6 +32,10 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; +import java.time.DateTimeException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -39,14 +50,6 @@ import org.eclipse.jgit.lib.PersonIdent; * Handy utility functions to parse raw object contents. */ public final class RawParseUtils { - /** - * UTF-8 charset constant. - * - * @since 2.2 - * @deprecated use {@link java.nio.charset.StandardCharsets#UTF_8} instead - */ - @Deprecated - public static final Charset UTF8_CHARSET = UTF_8; private static final byte[] digits10; @@ -354,6 +357,7 @@ public final class RawParseUtils { * if the string is not hex formatted. * @since 4.3 */ + @SuppressWarnings("IntLongMath") public static final long parseHexInt64(final byte[] bs, final int p) { long r = digits16[bs[p]] << 4; @@ -461,6 +465,29 @@ public final class RawParseUtils { } /** + * Parse a Git style timezone string in [+-]hhmm format + * + * @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 ZoneOffset represention of the timezone offset string. + * Invalid offsets default to UTC. + */ + private static ZoneId parseZoneOffset(final byte[] b, int ptr, + MutableInteger ptrResult) { + int hhmm = parseBase10(b, ptr, ptrResult); + try { + return ZoneOffset.ofHoursMinutes(hhmm / 100, hhmm % 100); + } catch (DateTimeException e) { + return UTC; + } + } + + /** * Locate the first position after a given character. * * @param b @@ -519,17 +546,24 @@ public final class RawParseUtils { } /** - * Locate the end of the header. Note that headers may be - * more than one line long. + * Locate the first end of line after the given position, while treating + * following lines which are starting with spaces as part of the current + * line. + * <p> + * For example, {@code nextLfSkippingSplitLines( + * "row \n with space at beginning of a following line\nThe actual next line", + * 0)} will return the position of {@code "\nThe actual next line"}. + * * @param b * buffer to scan. * @param ptr - * position within buffer to start looking for the end-of-header. - * @return new position just after the header. This is either - * b.length, or the index of the header's terminating newline. - * @since 5.1 + * position within buffer to start looking for the next line. + * @return new position just after the line end of the last line-split. This + * is either b.length, or the index of the current split-line's + * terminating newline. + * @since 6.9 */ - public static final int headerEnd(final byte[] b, int ptr) { + public static final int nextLfSkippingSplitLines(final byte[] b, int ptr) { final int sz = b.length; while (ptr < sz) { final byte c = b[ptr++]; @@ -537,7 +571,62 @@ public final class RawParseUtils { return ptr - 1; } } - return ptr - 1; + return ptr; + } + + /** + * Extract a part of a buffer as a header value, removing the single blanks + * at the front of continuation lines. + * + * @param b + * buffer to extract the header from + * @param start + * of the header value, see + * {@link #headerStart(byte[], byte[], int)} + * @param end + * of the header; see + * {@link #nextLfSkippingSplitLines(byte[], int)} + * @return the header value, with blanks indicating continuation lines + * stripped + * @since 6.9 + */ + public static final byte[] headerValue(final byte[] b, int start, int end) { + byte[] data = new byte[end - start]; + int out = 0; + byte last = '\0'; + for (int in = start; in < end; in++) { + byte ch = b[in]; + if (ch != ' ' || last != '\n') { + data[out++] = ch; + } + last = ch; + } + if (out == data.length) { + return data; + } + return Arrays.copyOf(data, out); + } + + /** + * Locate the first end of header after the given position. Note that + * headers may be more than one line long. + * <p> + * Also note that there might be multiple headers. If you wish to find the + * last header's end - call this in a loop. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start looking for the header + * (normally a new-line). + * @return new position just after the line end. This is either b.length, or + * the index of the header's terminating newline. + * @since 5.1 + * @deprecated use {{@link #nextLfSkippingSplitLines}} directly instead + */ + @Deprecated + public static final int headerEnd(final byte[] b, int ptr) { + return nextLfSkippingSplitLines(b, ptr); } /** @@ -575,6 +664,22 @@ public final class RawParseUtils { } /** + * Returns whether the message starts with any known headers. + * + * @param b + * buffer to scan. + * @return whether the message starts with any known headers + * @since 6.9 + */ + public static final boolean hasAnyKnownHeaders(byte[] b) { + return match(b, 0, tree) != -1 || match(b, 0, parent) != -1 + || match(b, 0, author) != -1 || match(b, 0, committer) != -1 + || match(b, 0, encoding) != -1 || match(b, 0, object) != -1 + || match(b, 0, type) != -1 || match(b, 0, tag) != -1 + || match(b, 0, tagger) != -1; + } + + /** * Locate the first position before a given character. * * @param b @@ -868,6 +973,26 @@ public final class RawParseUtils { } /** + * Parse the "encoding " header into a character set reference. + * <p> + * If unsuccessful, return UTF-8. + * + * @param buffer + * buffer to scan. + * @return the Java character set representation. Never null. Default to + * UTF-8. + * @see #parseEncoding(byte[]) + * @since 6.7 + */ + public static Charset guessEncoding(byte[] buffer) { + try { + return parseEncoding(buffer); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { + return UTF_8; + } + } + + /** * Parse a name string (e.g. author, committer, tagger) into a PersonIdent. * <p> * Leading spaces won't be trimmed from the string, i.e. will show up in the @@ -931,17 +1056,19 @@ public final class RawParseUtils { // character if there is no trailing LF. final int tzBegin = lastIndexOfTrim(raw, ' ', nextLF(raw, emailE - 1) - 2) + 1; - if (tzBegin <= emailE) // No time/zone, still valid - return new PersonIdent(name, email, 0, 0); + if (tzBegin <= emailE) { // No time/zone, still valid + return new PersonIdent(name, email, EPOCH, UTC); + } final int whenBegin = Math.max(emailE, lastIndexOfTrim(raw, ' ', tzBegin - 1) + 1); - if (whenBegin >= tzBegin - 1) // No time/zone, still valid - return new PersonIdent(name, email, 0, 0); + if (whenBegin >= tzBegin - 1) { // No time/zone, still valid + return new PersonIdent(name, email, EPOCH, UTC); + } - final long when = parseLongBase10(raw, whenBegin, null); - final int tz = parseTimeZoneOffset(raw, tzBegin); - return new PersonIdent(name, email, when * 1000L, tz); + long when = parseLongBase10(raw, whenBegin, null); + return new PersonIdent(name, email, Instant.ofEpochSecond(when), + parseZoneOffset(raw, tzBegin, null)); } /** @@ -979,16 +1106,16 @@ public final class RawParseUtils { name = decode(raw, nameB, stop); final MutableInteger ptrout = new MutableInteger(); - long when; - int tz; + Instant when; + ZoneId tz; if (emailE < stop) { - when = parseLongBase10(raw, emailE + 1, ptrout); - tz = parseTimeZoneOffset(raw, ptrout.value); + when = Instant.ofEpochSecond(parseLongBase10(raw, emailE + 1, ptrout)); + tz = parseZoneOffset(raw, ptrout.value, null); } else { - when = 0; - tz = 0; + when = EPOCH; + tz = UTC; } - return new PersonIdent(name, email, when * 1000L, tz); + return new PersonIdent(name, email, when, tz); } /** @@ -1237,6 +1364,7 @@ public final class RawParseUtils { final int sz = b.length; if (ptr == 0) ptr += 48; // skip the "object ..." line. + // Assume the rest of the current paragraph is all headers. while (ptr < sz && b[ptr] != '\n') ptr = nextLF(b, ptr); if (ptr < sz && b[ptr] == '\n') diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawSubStringPattern.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawSubStringPattern.java index 57464f3c41..04fdcd0fa4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawSubStringPattern.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawSubStringPattern.java @@ -98,7 +98,6 @@ public class RawSubStringPattern { return needleString; } - /** {@inheritDoc} */ @Override public String toString() { return pattern(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java index 462bab081a..350ec76cf9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java @@ -43,6 +43,8 @@ public class RefList<T extends Ref> implements Iterable<Ref> { /** * Create an empty unmodifiable reference list. * + * @param <T> + * type of reference being stored. * @return an empty unmodifiable reference list. */ @SuppressWarnings("unchecked") @@ -70,7 +72,6 @@ public class RefList<T extends Ref> implements Iterable<Ref> { this.cnt = src.cnt; } - /** {@inheritDoc} */ @Override public Iterator<Ref> iterator() { return new Iterator<>() { @@ -286,7 +287,6 @@ public class RefList<T extends Ref> implements Iterable<Ref> { return add(idx, ref); } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder r = new StringBuilder(); @@ -305,6 +305,8 @@ public class RefList<T extends Ref> implements Iterable<Ref> { /** * Create a {@link Collector} for {@link Ref}. * + * @param <T> + * type of reference being stored. * @param mergeFunction * if specified the result will be sorted and deduped. * @return {@link Collector} for {@link Ref} @@ -355,7 +357,11 @@ public class RefList<T extends Ref> implements Iterable<Ref> { list = new Ref[Math.max(capacity, 16)]; } - /** @return number of items in this builder's internal collection. */ + /** + * Get size + * + * @return number of items in this builder's internal collection. + */ public int size() { return size; } @@ -390,6 +396,7 @@ public class RefList<T extends Ref> implements Iterable<Ref> { * after additions are complete using {@link #sort()}. * * @param ref + * reference to add */ public void add(T ref) { if (list.length == size) { @@ -404,6 +411,7 @@ public class RefList<T extends Ref> implements Iterable<Ref> { * Add all items from another builder. * * @param other + * another builder * @since 5.4 */ public void addAll(Builder other) { @@ -454,6 +462,7 @@ public class RefList<T extends Ref> implements Iterable<Ref> { * Dedupe the refs in place. Must be called after {@link #sort}. * * @param mergeFunction + * function used for de-duplication */ @SuppressWarnings("unchecked") void dedupe(BinaryOperator<T> mergeFunction) { @@ -475,7 +484,11 @@ public class RefList<T extends Ref> implements Iterable<Ref> { Arrays.fill(list, size, list.length, null); } - /** @return an unmodifiable list using this collection's backing array. */ + /** + * Get unmodifiable list based on this list + * + * @return an unmodifiable list using this collection's backing array. + */ public RefList<T> toRefList() { return new RefList<>(list, size); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java index c68a76cef4..a4d1fd5b70 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java @@ -119,13 +119,11 @@ public class RefMap extends AbstractMap<String, Ref> { this.resolved = (RefList<Ref>) resolved; } - /** {@inheritDoc} */ @Override public boolean containsKey(Object name) { return get(name) != null; } - /** {@inheritDoc} */ @Override public Ref get(Object key) { String name = toRefName((String) key); @@ -137,7 +135,6 @@ public class RefMap extends AbstractMap<String, Ref> { return ref; } - /** {@inheritDoc} */ @Override public Ref put(String keyName, Ref value) { String name = toRefName(keyName); @@ -165,7 +162,6 @@ public class RefMap extends AbstractMap<String, Ref> { return prior; } - /** {@inheritDoc} */ @Override public Ref remove(Object key) { String name = toRefName((String) key); @@ -189,13 +185,11 @@ public class RefMap extends AbstractMap<String, Ref> { return res; } - /** {@inheritDoc} */ @Override public boolean isEmpty() { return entrySet().isEmpty(); } - /** {@inheritDoc} */ @Override public Set<Entry<String, Ref>> entrySet() { if (entrySet == null) { @@ -238,7 +232,6 @@ public class RefMap extends AbstractMap<String, Ref> { return entrySet; } - /** {@inheritDoc} */ @Override public String toString() { StringBuilder r = new StringBuilder(); @@ -259,6 +252,7 @@ public class RefMap extends AbstractMap<String, Ref> { * Create a {@link Collector} for {@link Ref}. * * @param mergeFunction + * merge function * @return {@link Collector} for {@link Ref} * @since 5.4 */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java index 5611b1e78e..b6b19e0e7b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.util; import java.text.MessageFormat; +import java.time.Duration; +import java.time.Instant; import java.util.Date; import org.eclipse.jgit.internal.JGitText; @@ -42,12 +44,29 @@ public class RelativeDateFormatter { * @return age of given {@link java.util.Date} compared to now formatted in * the same relative format as returned by * {@code git log --relative-date} + * @deprecated Use {@link #format(Instant)} instead. */ + @Deprecated(since = "7.2") @SuppressWarnings("boxing") public static String format(Date when) { + return format(when.toInstant()); + } - long ageMillis = SystemReader.getInstance().getCurrentTime() - - when.getTime(); + /** + * Get age of given {@link java.time.Instant} compared to now formatted in the + * same relative format as returned by {@code git log --relative-date} + * + * @param when + * an instant to format + * @return age of given instant compared to now formatted in + * the same relative format as returned by + * {@code git log --relative-date} + * @since 7.2 + */ + @SuppressWarnings("boxing") + public static String format(Instant when) { + long ageMillis = Duration + .between(when, SystemReader.getInstance().now()).toMillis(); // shouldn't happen in a perfect world if (ageMillis < 0) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java index cf06172c17..e3e3e04fd9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java @@ -13,8 +13,8 @@ import java.text.MessageFormat; import java.util.Locale; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification; -import org.eclipse.jgit.lib.GpgSignatureVerifier.TrustLevel; +import org.eclipse.jgit.lib.SignatureVerifier.SignatureVerification; +import org.eclipse.jgit.lib.SignatureVerifier.TrustLevel; import org.eclipse.jgit.lib.PersonIdent; /** @@ -39,29 +39,34 @@ public final class SignatureUtils { * to use for dates * @return a textual representation of the {@link SignatureVerification}, * using LF as line separator + * + * @since 7.0 */ public static String toString(SignatureVerification verification, PersonIdent creator, GitDateFormatter formatter) { StringBuilder result = new StringBuilder(); - // Use the creator's timezone for the signature date - PersonIdent dateId = new PersonIdent(creator, - verification.getCreationDate()); - result.append(MessageFormat.format(JGitText.get().verifySignatureMade, - formatter.formatDate(dateId))); - result.append('\n'); + if (verification.creationDate() != null) { + // Use the creator's timezone for the signature date + PersonIdent dateId = new PersonIdent(creator, + verification.creationDate().toInstant()); + result.append( + MessageFormat.format(JGitText.get().verifySignatureMade, + formatter.formatDate(dateId))); + result.append('\n'); + } result.append(MessageFormat.format( JGitText.get().verifySignatureKey, - verification.getKeyFingerprint().toUpperCase(Locale.ROOT))); + verification.keyFingerprint().toUpperCase(Locale.ROOT))); result.append('\n'); - if (!StringUtils.isEmptyOrNull(verification.getSigner())) { + if (!StringUtils.isEmptyOrNull(verification.signer())) { result.append( MessageFormat.format(JGitText.get().verifySignatureIssuer, - verification.getSigner())); + verification.signer())); result.append('\n'); } String msg; - if (verification.getVerified()) { - if (verification.isExpired()) { + if (verification.verified()) { + if (verification.expired()) { msg = JGitText.get().verifySignatureExpired; } else { msg = JGitText.get().verifySignatureGood; @@ -69,14 +74,14 @@ public final class SignatureUtils { } else { msg = JGitText.get().verifySignatureBad; } - result.append(MessageFormat.format(msg, verification.getKeyUser())); - if (!TrustLevel.UNKNOWN.equals(verification.getTrustLevel())) { + result.append(MessageFormat.format(msg, verification.keyUser())); + if (!TrustLevel.UNKNOWN.equals(verification.trustLevel())) { result.append(' ' + MessageFormat .format(JGitText.get().verifySignatureTrust, verification - .getTrustLevel().name().toLowerCase(Locale.ROOT))); + .trustLevel().name().toLowerCase(Locale.ROOT))); } result.append('\n'); - msg = verification.getMessage(); + msg = verification.message(); if (!StringUtils.isEmptyOrNull(msg)) { result.append(msg); result.append('\n'); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SshSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SshSupport.java index e29704158d..42a76b5b16 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SshSupport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SshSupport.java @@ -48,6 +48,7 @@ public class SshSupport { * cases. * @return The entire output read from stdout. * @throws IOException + * if an IO error occurred * @throws CommandFailedException * if the ssh command execution failed, error message contains * the content of stderr. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java index d957deb34c..efa6e7ddc3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java @@ -43,14 +43,18 @@ public class Stats { } /** - * @return number of the added values + * Returns the number of added values + * + * @return the number of added values */ public int count() { return n; } /** - * @return minimum of the added values + * Returns the smallest value added + * + * @return the smallest value added */ public double min() { if (n < 1) { @@ -60,7 +64,9 @@ public class Stats { } /** - * @return maximum of the added values + * Returns the biggest value added + * + * @return the biggest value added */ public double max() { if (n < 1) { @@ -70,9 +76,10 @@ public class Stats { } /** - * @return average of the added values + * Returns the average of the added values + * + * @return the average of the added values */ - public double avg() { if (n < 1) { return Double.NaN; @@ -81,7 +88,9 @@ public class Stats { } /** - * @return variance of the added values + * Returns the variance of the added values + * + * @return the variance of the added values */ public double var() { if (n < 2) { @@ -91,7 +100,9 @@ public class Stats { } /** - * @return standard deviation of the added values + * Returns the standard deviation of the added values + * + * @return the standard deviation of the added values */ public double stddev() { return Math.sqrt(this.var()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java index 917add3609..e381a3bcc9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java @@ -14,6 +14,7 @@ import java.text.MessageFormat; import java.util.Collection; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; @@ -22,6 +23,8 @@ import org.eclipse.jgit.lib.Constants; */ public final class StringUtils { + private static final String EMPTY = ""; //$NON-NLS-1$ + private static final long KiB = 1024; private static final long MiB = 1024 * KiB; @@ -181,9 +184,9 @@ public final class StringUtils { * * @param stringValue * the string to parse. - * @return the boolean interpretation of {@code value}. + * @return the boolean interpretation of {@code stringValue}. * @throws java.lang.IllegalArgumentException - * if {@code value} is not recognized as one of the standard + * if {@code stringValue} is not recognized as one of the standard * boolean names. */ public static boolean toBoolean(String stringValue) { @@ -275,6 +278,44 @@ public final class StringUtils { } /** + * Remove the specified character from beginning and end of a string + * <p> + * If the character repeats, all copies + * + * @param str input string + * @param c character to remove + * @return the input string with c + * @since 7.2 + */ + public static String trim(String str, char c) { + if (str == null || str.length() == 0) { + return str; + } + + int endPos = str.length()-1; + while (endPos >= 0 && str.charAt(endPos) == c) { + endPos--; + } + + // Whole string is c + if (endPos == -1) { + return EMPTY; + } + + int startPos = 0; + while (startPos < endPos && str.charAt(startPos) == c) { + startPos++; + } + + if (startPos == 0 && endPos == str.length()-1) { + // No need to copy + return str; + } + + return str.substring(startPos, endPos+1); + } + + /** * Appends {@link Constants#DOT_GIT_EXT} unless the given name already ends * with that suffix. * @@ -346,7 +387,7 @@ public final class StringUtils { * allow negative numbers, too * @return the value parsed * @throws NumberFormatException - * if the {@value} is not parseable, or beyond the range of + * if the {@code value} is not parseable, or beyond the range of * {@link Long} * @throws StringIndexOutOfBoundsException * if the string is empty or contains only whitespace, or @@ -420,7 +461,7 @@ public final class StringUtils { * allow negative numbers, too * @return the value parsed * @throws NumberFormatException - * if the {@value} is not parseable or beyond the range of + * if the {@code value} is not parseable or beyond the range of * {@link Integer} * @throws StringIndexOutOfBoundsException * if the string is empty or contains only whitespace, or @@ -463,4 +504,39 @@ public final class StringUtils { } return String.valueOf(value); } + + /** + * Compares Strings and returns the initial sequence of characters that is + * common to all of them. + * + * @param strings + * Strings to consider + * @return common prefix of all Strings + * @since 6.8 + */ + public static @NonNull String commonPrefix(@Nullable String... strings) { + if (strings == null || strings.length == 0) { + return EMPTY; + } + String first = strings[0]; + if (first == null) { + return EMPTY; + } + if (strings.length == 1) { + return first; + } + for (int i = 0; i < first.length(); i++) { + char currentChar = first.charAt(i); + for (int j = 1; j < strings.length; j++) { + String str = strings[j]; + if (str == null) { + return EMPTY; + } + if (str.length() == i || currentChar != str.charAt(i)) { + return str.substring(0, i); + } + } + } + return first; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java index a8a77904a2..0b7c6204f2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java @@ -23,10 +23,12 @@ import java.nio.charset.UnsupportedCharsetException; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.Locale; import java.util.TimeZone; import java.util.concurrent.atomic.AtomicReference; @@ -39,6 +41,7 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectChecker; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.storage.file.UserConfigFile; import org.eclipse.jgit.util.time.MonotonicClock; import org.eclipse.jgit.util.time.MonotonicSystemClock; import org.slf4j.Logger; @@ -124,28 +127,20 @@ public abstract class SystemReader { @Override public FileBasedConfig openUserConfig(Config parent, FS fs) { - return new FileBasedConfig(parent, new File(fs.userHome(), ".gitconfig"), //$NON-NLS-1$ - fs); - } - - private Path getXDGConfigHome(FS fs) { - String configHomePath = getenv(Constants.XDG_CONFIG_HOME); - if (StringUtils.isEmptyOrNull(configHomePath)) { - configHomePath = new File(fs.userHome(), ".config") //$NON-NLS-1$ - .getAbsolutePath(); - } - try { - return Paths.get(configHomePath); - } catch (InvalidPathException e) { - LOG.error(JGitText.get().logXDGConfigHomeInvalid, - configHomePath, e); + File homeFile = new File(fs.userHome(), ".gitconfig"); //$NON-NLS-1$ + Path xdgPath = getXdgConfigDirectory(fs); + if (xdgPath != null) { + Path configPath = xdgPath.resolve("git") //$NON-NLS-1$ + .resolve(Constants.CONFIG); + return new UserConfigFile(parent, homeFile, configPath.toFile(), + fs); } - return null; + return new FileBasedConfig(parent, homeFile, fs); } @Override public FileBasedConfig openJGitConfig(Config parent, FS fs) { - Path xdgPath = getXDGConfigHome(fs); + Path xdgPath = getXdgConfigDirectory(fs); if (xdgPath != null) { Path configPath = xdgPath.resolve("jgit") //$NON-NLS-1$ .resolve(Constants.CONFIG); @@ -176,11 +171,87 @@ public abstract class SystemReader { } @Override + public Instant now() { + return Instant.now(); + } + + @Override public int getTimezone(long when) { return getTimeZone().getOffset(when) / (60 * 1000); } } + /** + * Delegating SystemReader. Reduces boiler-plate code applications need to + * implement when overriding only a few of the SystemReader's methods. + * + * @since 6.9 + */ + public static class Delegate extends SystemReader { + + private final SystemReader delegate; + + /** + * Create a delegating system reader + * + * @param delegate + * the system reader to delegate to + */ + public Delegate(SystemReader delegate) { + this.delegate = delegate; + } + + @Override + public String getHostname() { + return delegate.getHostname(); + } + + @Override + public String getenv(String variable) { + return delegate.getenv(variable); + } + + @Override + public String getProperty(String key) { + return delegate.getProperty(key); + } + + @Override + public FileBasedConfig openUserConfig(Config parent, FS fs) { + return delegate.openUserConfig(parent, fs); + } + + @Override + public FileBasedConfig openSystemConfig(Config parent, FS fs) { + return delegate.openSystemConfig(parent, fs); + } + + @Override + public FileBasedConfig openJGitConfig(Config parent, FS fs) { + return delegate.openJGitConfig(parent, fs); + } + + @Override + public long getCurrentTime() { + return delegate.getCurrentTime(); + } + + @Override + public Instant now() { + return delegate.now(); + } + + @Override + public int getTimezone(long when) { + return delegate.getTimezone(when); + } + + @Override + public ZoneOffset getTimeZoneAt(Instant when) { + return delegate.getTimeZoneAt(when); + } + } + private static volatile SystemReader INSTANCE = DEFAULT; /** @@ -391,6 +462,66 @@ public abstract class SystemReader { } /** + * Gets the directory denoted by environment variable XDG_CONFIG_HOME. If + * the variable is not set or empty, return a path for + * {@code $HOME/.config}. + * + * @param fileSystem + * {@link FS} to get the user's home directory + * @return a {@link Path} denoting the directory, which may exist or not, or + * {@code null} if the environment variable is not set and there is + * no home directory, or the path is invalid. + * @since 6.7 + */ + public Path getXdgConfigDirectory(FS fileSystem) { + String configHomePath = getenv(Constants.XDG_CONFIG_HOME); + if (StringUtils.isEmptyOrNull(configHomePath)) { + File home = fileSystem.userHome(); + if (home == null) { + return null; + } + configHomePath = new File(home, ".config").getAbsolutePath(); //$NON-NLS-1$ + } + try { + return Paths.get(configHomePath); + } catch (InvalidPathException e) { + LOG.error(JGitText.get().logXDGConfigHomeInvalid, configHomePath, + e); + } + return null; + } + + /** + * Gets the directory denoted by environment variable XDG_CACHE_HOME. If + * the variable is not set or empty, return a path for + * {@code $HOME/.cache}. + * + * @param fileSystem + * {@link FS} to get the user's home directory + * @return a {@link Path} denoting the directory, which may exist or not, or + * {@code null} if the environment variable is not set and there is + * no home directory, or the path is invalid. + * @since 7.3 + */ + public Path getXdgCacheDirectory(FS fileSystem) { + String cacheHomePath = getenv(Constants.XDG_CACHE_HOME); + if (StringUtils.isEmptyOrNull(cacheHomePath)) { + File home = fileSystem.userHome(); + if (home == null) { + return null; + } + cacheHomePath = new File(home, ".cache").getAbsolutePath(); //$NON-NLS-1$ + } + try { + return Paths.get(cacheHomePath); + } catch (InvalidPathException e) { + LOG.error(JGitText.get().logXDGCacheHomeInvalid, cacheHomePath, + e); + } + return null; + } + + /** * Update config and its parents if they seem modified * * @param config @@ -419,10 +550,37 @@ public abstract class SystemReader { * Get the current system time * * @return the current system time + * + * @deprecated Use {@link #now()} */ + @Deprecated(since = "7.1") public abstract long getCurrentTime(); /** + * Get the current system time + * + * @return the current system time + * + * @since 7.1 + */ + public Instant now() { + // Subclasses overriding getCurrentTime should keep working + // TODO(ifrade): Once we remove getCurrentTime, use Instant.now() + return Instant.ofEpochMilli(getCurrentTime()); + } + + /** + * Get "now" as civil time, in the System timezone + * + * @return the current system time + * + * @since 7.1 + */ + public LocalDateTime civilNow() { + return LocalDateTime.ofInstant(now(), getTimeZoneId()); + } + + /** * Get clock instance preferred by this system. * * @return clock instance preferred by this system. @@ -438,20 +596,48 @@ public abstract class SystemReader { * @param when * a system timestamp * @return the local time zone + * + * @deprecated Use {@link #getTimeZoneAt(Instant)} instead. */ + @Deprecated(since = "7.1") public abstract int getTimezone(long when); /** + * Get the local time zone offset at "when" time + * + * @param when + * a system timestamp + * @return the local time zone + * @since 7.1 + */ + public ZoneOffset getTimeZoneAt(Instant when) { + return getTimeZoneId().getRules().getOffset(when); + } + + /** * Get system time zone, possibly mocked for testing * * @return system time zone, possibly mocked for testing * @since 1.2 + * + * @deprecated Use {@link #getTimeZoneId()} */ + @Deprecated(since = "7.1") public TimeZone getTimeZone() { return TimeZone.getDefault(); } /** + * Get system time zone, possibly mocked for testing + * + * @return system time zone, possibly mocked for testing + * @since 7.1 + */ + public ZoneId getTimeZoneId() { + return ZoneId.systemDefault(); + } + + /** * Get the locale to use * * @return the locale to use @@ -586,9 +772,7 @@ public abstract class SystemReader { } private String getOsName() { - return AccessController.doPrivileged( - (PrivilegedAction<String>) () -> getProperty("os.name") //$NON-NLS-1$ - ); + return getProperty("os.name"); //$NON-NLS-1$ } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoCRLFInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoCRLFInputStream.java index cedb159827..ccc19691b1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoCRLFInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoCRLFInputStream.java @@ -57,14 +57,12 @@ public class AutoCRLFInputStream extends InputStream { this.detectBinary = detectBinary; } - /** {@inheritDoc} */ @Override public int read() throws IOException { final int read = read(single, 0, 1); return read == 1 ? single[0] & 0xff : -1; } - /** {@inheritDoc} */ @Override public int read(byte[] bs, int off, int len) throws IOException { if (len == 0) @@ -103,7 +101,6 @@ public class AutoCRLFInputStream extends InputStream { return n; } - /** {@inheritDoc} */ @Override public void close() throws IOException { in.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoCRLFOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoCRLFOutputStream.java index 305ccbd7e6..9fb316f28d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoCRLFOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoCRLFOutputStream.java @@ -65,14 +65,12 @@ public class AutoCRLFOutputStream extends OutputStream { this.detectBinary = detectBinary; } - /** {@inheritDoc} */ @Override public void write(int b) throws IOException { onebytebuf[0] = (byte) b; write(onebytebuf, 0, 1); } - /** {@inheritDoc} */ @Override public void write(byte[] b) throws IOException { int overflow = buffer(b, 0, b.length); @@ -80,7 +78,6 @@ public class AutoCRLFOutputStream extends OutputStream { write(b, b.length - overflow, overflow); } - /** {@inheritDoc} */ @Override public void write(byte[] b, int startOff, int startLen) throws IOException { @@ -151,7 +148,6 @@ public class AutoCRLFOutputStream extends OutputStream { write(binbuf, 0, cachedLen); } - /** {@inheritDoc} */ @Override public void flush() throws IOException { if (binbufcnt <= binbuf.length) { @@ -161,7 +157,6 @@ public class AutoCRLFOutputStream extends OutputStream { out.flush(); } - /** {@inheritDoc} */ @Override public void close() throws IOException { flush(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFInputStream.java index 7db882c074..4b9706a3ce 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFInputStream.java @@ -147,52 +147,12 @@ public class AutoLFInputStream extends InputStream { && flags.contains(StreamFlag.FOR_CHECKOUT); } - /** - * Creates a new InputStream, wrapping the specified stream. - * - * @param in - * raw input stream - * @param detectBinary - * whether binaries should be detected - * @since 2.0 - * @deprecated since 5.9, use {@link #create(InputStream, StreamFlag...)} - * instead - */ - @Deprecated - public AutoLFInputStream(InputStream in, boolean detectBinary) { - this(in, detectBinary, false); - } - - /** - * Creates a new InputStream, wrapping the specified stream. - * - * @param in - * raw input stream - * @param detectBinary - * whether binaries should be detected - * @param abortIfBinary - * throw an IOException if the file is binary - * @since 3.3 - * @deprecated since 5.9, use {@link #create(InputStream, StreamFlag...)} - * instead - */ - @Deprecated - public AutoLFInputStream(InputStream in, boolean detectBinary, - boolean abortIfBinary) { - this.in = in; - this.detectBinary = detectBinary; - this.abortIfBinary = abortIfBinary; - this.forCheckout = false; - } - - /** {@inheritDoc} */ @Override public int read() throws IOException { final int read = read(single, 0, 1); return read == 1 ? single[0] & 0xff : -1; } - /** {@inheritDoc} */ @Override public int read(byte[] bs, int off, int len) throws IOException { @@ -242,7 +202,6 @@ public class AutoLFInputStream extends InputStream { return isBinary; } - /** {@inheritDoc} */ @Override public void close() throws IOException { in.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFOutputStream.java index a0e9fb68c5..e56991d43a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFOutputStream.java @@ -69,14 +69,12 @@ public class AutoLFOutputStream extends OutputStream { this.detectBinary = detectBinary; } - /** {@inheritDoc} */ @Override public void write(int b) throws IOException { onebytebuf[0] = (byte) b; write(onebytebuf, 0, 1); } - /** {@inheritDoc} */ @Override public void write(byte[] b) throws IOException { int overflow = buffer(b, 0, b.length); @@ -85,7 +83,6 @@ public class AutoLFOutputStream extends OutputStream { } } - /** {@inheritDoc} */ @Override public void write(byte[] b, int startOff, int startLen) throws IOException { @@ -164,7 +161,6 @@ public class AutoLFOutputStream extends OutputStream { write(binbuf, 0, cachedLen); } - /** {@inheritDoc} */ @Override public void flush() throws IOException { if (binbufcnt <= binbuf.length) { @@ -173,7 +169,6 @@ public class AutoLFOutputStream extends OutputStream { out.flush(); } - /** {@inheritDoc} */ @Override public void close() throws IOException { flush(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ByteBufferInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ByteBufferInputStream.java new file mode 100644 index 0000000000..804f7f860a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ByteBufferInputStream.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2023, SAP SE and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.util.io; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.InvalidMarkException; +import java.util.Objects; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.internal.JGitText; + +/** + * An {@link InputStream} backed by a {@link ByteBuffer}. + * + * @since 6.8 + */ +public class ByteBufferInputStream extends InputStream { + + private ByteBuffer buf; + + /** + * Creates a {@link ByteBufferInputStream} + * + * @param buf + * the ByteBuffer backing the stream + */ + public ByteBufferInputStream(@NonNull ByteBuffer buf) { + this.buf = buf; + } + + @Override + public int read() throws IOException { + nullCheck(); + if (buf.hasRemaining()) { + return buf.get() & 0xFF; + } + return -1; + } + + @Override + public int read(byte[] b) throws IOException { + nullCheck(); + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + nullCheck(); + Objects.checkFromIndexSize(off, len, b.length); + if (len == 0) { + return 0; + } + int length = Math.min(buf.remaining(), len); + if (length == 0) { + return -1; + } + buf.get(b, off, length); + return length; + } + + @Override + public byte[] readAllBytes() throws IOException { + return readNBytes(buf.remaining()); + } + + @Override + public byte[] readNBytes(int len) throws IOException { + int l = Math.min(len, buf.remaining()); + byte[] b = new byte[l]; + read(b); + return b; + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + return read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + nullCheck(); + if (n <= 0) { + return 0; + } + // ByteBuffer index has type int + int delta = n > Integer.MAX_VALUE ? buf.remaining() + : Math.min((int) n, buf.remaining()); + buf.position(buf.position() + delta); + return delta; + } + + @Override + public int available() throws IOException { + nullCheck(); + return buf.remaining(); + } + + @Override + public void close() { + buf = null; + } + + @Override + public synchronized void mark(int readlimit) { + buf.mark(); + } + + @Override + public synchronized void reset() throws IOException { + try { + buf.reset(); + } catch (InvalidMarkException e) { + throw new IOException(e); + } + } + + @Override + public boolean markSupported() { + return true; + } + + private void nullCheck() throws IOException { + if (buf == null) { + throw new IOException(JGitText.get().inputStreamClosed); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/CountingOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/CountingOutputStream.java index 782f3f4ca6..d0049d29de 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/CountingOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/CountingOutputStream.java @@ -39,27 +39,23 @@ public class CountingOutputStream extends OutputStream { return cnt; } - /** {@inheritDoc} */ @Override public void write(int val) throws IOException { out.write(val); cnt++; } - /** {@inheritDoc} */ @Override public void write(byte[] buf, int off, int len) throws IOException { out.write(buf, off, len); cnt += len; } - /** {@inheritDoc} */ @Override public void flush() throws IOException { out.flush(); } - /** {@inheritDoc} */ @Override public void close() throws IOException { out.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/DisabledOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/DisabledOutputStream.java index f1bfbe29a5..03c25bb702 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/DisabledOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/DisabledOutputStream.java @@ -27,7 +27,6 @@ public final class DisabledOutputStream extends OutputStream { // more than one instance from being created. } - /** {@inheritDoc} */ @Override public void write(int b) throws IOException { // We shouldn't be writing output at this stage, there 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 index 7e46afbc4b..888b8fbb09 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/InterruptTimer.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/InterruptTimer.java @@ -104,10 +104,20 @@ public final class InterruptTimer { */ public void terminate() { state.terminate(); + boolean interrupted = false; try { - thread.join(); - } catch (InterruptedException e) { - // + while (true) { + try { + thread.join(); + return; + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/IsolatedOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/IsolatedOutputStream.java index 2bbdbefd38..1faf6ea9aa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/IsolatedOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/IsolatedOutputStream.java @@ -58,13 +58,11 @@ public class IsolatedOutputStream extends OutputStream { new ArrayBlockingQueue<>(1), new NamedThreadFactory()); } - /** {@inheritDoc} */ @Override public void write(int ch) throws IOException { write(new byte[] { (byte) ch }, 0, 1); } - /** {@inheritDoc} */ @Override public void write(byte[] buf, int pos, int cnt) throws IOException { @@ -75,7 +73,6 @@ public class IsolatedOutputStream extends OutputStream { }); } - /** {@inheritDoc} */ @Override public void flush() throws IOException { checkClosed(); @@ -85,7 +82,6 @@ public class IsolatedOutputStream extends OutputStream { }); } - /** {@inheritDoc} */ @Override public void close() throws IOException { if (!copier.isShutdown()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/LimitedInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/LimitedInputStream.java index 88006242d9..681a52988e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/LimitedInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/LimitedInputStream.java @@ -47,21 +47,18 @@ public abstract class LimitedInputStream extends FilterInputStream { this.limit = limit; } - /** {@inheritDoc} */ @Override public int available() throws IOException { return (int) Math.min(in.available(), left); } // it's okay to mark even if mark isn't supported, as reset won't work - /** {@inheritDoc} */ @Override public synchronized void mark(int readLimit) { in.mark(readLimit); mark = left; } - /** {@inheritDoc} */ @Override public int read() throws IOException { if (left == 0) { @@ -78,7 +75,6 @@ public abstract class LimitedInputStream extends FilterInputStream { return result; } - /** {@inheritDoc} */ @Override public int read(byte[] b, int off, int len) throws IOException { if (left == 0) { @@ -96,7 +92,6 @@ public abstract class LimitedInputStream extends FilterInputStream { return result; } - /** {@inheritDoc} */ @Override public synchronized void reset() throws IOException { if (!in.markSupported()) @@ -109,7 +104,6 @@ public abstract class LimitedInputStream extends FilterInputStream { left = mark; } - /** {@inheritDoc} */ @Override public long skip(long n) throws IOException { n = Math.min(n, left); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/MessageWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/MessageWriter.java index 2637766153..8d5b8fdcba 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/MessageWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/MessageWriter.java @@ -51,7 +51,6 @@ public class MessageWriter extends Writer { enc = new OutputStreamWriter(getRawStream(), UTF_8); } - /** {@inheritDoc} */ @Override public void write(char[] cbuf, int off, int len) throws IOException { synchronized (buf) { @@ -71,20 +70,17 @@ public class MessageWriter extends Writer { return buf; } - /** {@inheritDoc} */ @Override public void close() throws IOException { // Do nothing, we are buffered with no resources. } - /** {@inheritDoc} */ @Override public void flush() throws IOException { // Do nothing, we are buffered with no resources. } /** @return string version of all buffered data. */ - /** {@inheritDoc} */ @Override public String toString() { return RawParseUtils.decode(buf.toByteArray()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/SilentInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/SilentInputStream.java new file mode 100644 index 0000000000..8c2c61a434 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/SilentInputStream.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.util.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An {@link InputStream} that swallows exceptions on {@link #close()}. + * + * @since 7.4 + */ +public class SilentInputStream extends FilterInputStream { + + private static final Logger LOG = LoggerFactory + .getLogger(SilentInputStream.class); + + /** + * Wraps an existing {@link InputStream}. + * + * @param in + * {@link InputStream} to wrap + */ + public SilentInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + try { + super.close(); + } catch (IOException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Exception ignored while closing input stream", e); //$NON-NLS-1$ + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java index c0724e43f2..ed412fa6f5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java @@ -67,7 +67,6 @@ public class StreamCopyThread extends Thread { } } - /** {@inheritDoc} */ @Override public void run() { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TeeInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TeeInputStream.java index 56d0169f7d..96376bfc6a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TeeInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TeeInputStream.java @@ -46,7 +46,6 @@ public class TeeInputStream extends InputStream { this.dst = dst; } - /** {@inheritDoc} */ @Override public int read() throws IOException { byte[] b = skipBuffer(); @@ -54,7 +53,6 @@ public class TeeInputStream extends InputStream { return n == 1 ? b[0] & 0xff : -1; } - /** {@inheritDoc} */ @Override public long skip(long count) throws IOException { long skipped = 0; @@ -71,7 +69,6 @@ public class TeeInputStream extends InputStream { return skipped; } - /** {@inheritDoc} */ @Override public int read(byte[] b, int off, int len) throws IOException { if (len == 0) @@ -83,7 +80,6 @@ public class TeeInputStream extends InputStream { return n; } - /** {@inheritDoc} */ @Override public void close() throws IOException { byte[] b = skipBuffer(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TeeOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TeeOutputStream.java index e6fdd709b2..ab084a66f2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TeeOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TeeOutputStream.java @@ -34,35 +34,30 @@ public class TeeOutputStream extends OutputStream { this.stream2 = stream2; } - /** {@inheritDoc} */ @Override public void write(byte[] buf) throws IOException { this.stream1.write(buf); this.stream2.write(buf); } - /** {@inheritDoc} */ @Override public void write(byte[] buf, int off, int len) throws IOException { this.stream1.write(buf, off, len); this.stream2.write(buf, off, len); } - /** {@inheritDoc} */ @Override public void write(int b) throws IOException { this.stream1.write(b); this.stream2.write(b); } - /** {@inheritDoc} */ @Override public void flush() throws IOException { this.stream1.flush(); this.stream2.flush(); } - /** {@inheritDoc} */ @Override public void close() throws IOException { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java index 3bc92d5bcd..13982b133c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java @@ -11,8 +11,6 @@ package org.eclipse.jgit.util.io; import java.io.IOException; import java.io.Writer; -import java.security.AccessController; -import java.security.PrivilegedAction; import org.eclipse.jgit.util.SystemReader; @@ -35,25 +33,19 @@ public class ThrowingPrintWriter extends Writer { */ public ThrowingPrintWriter(Writer out) { this.out = out; - LF = AccessController - .doPrivileged((PrivilegedAction<String>) () -> SystemReader - .getInstance().getProperty("line.separator") //$NON-NLS-1$ - ); + LF = SystemReader.getInstance().getProperty("line.separator"); //$NON-NLS-1$ } - /** {@inheritDoc} */ @Override public void write(char[] cbuf, int off, int len) throws IOException { out.write(cbuf, off, len); } - /** {@inheritDoc} */ @Override public void flush() throws IOException { out.flush(); } - /** {@inheritDoc} */ @Override public void close() throws IOException { out.close(); @@ -62,8 +54,10 @@ public class ThrowingPrintWriter extends Writer { /** * Print a string and terminate with a line feed. * - * @param s a {@link java.lang.String} object. + * @param s + * a {@link java.lang.String} object. * @throws java.io.IOException + * if an IO error occurred */ public void println(String s) throws IOException { print(s + LF); @@ -73,6 +67,7 @@ public class ThrowingPrintWriter extends Writer { * Print a platform dependent new line * * @throws java.io.IOException + * if an IO error occurred */ public void println() throws IOException { print(LF); @@ -81,8 +76,10 @@ public class ThrowingPrintWriter extends Writer { /** * Print a char * - * @param value a char. + * @param value + * a char. * @throws java.io.IOException + * if an IO error occurred */ public void print(char value) throws IOException { print(String.valueOf(value)); @@ -94,6 +91,7 @@ public class ThrowingPrintWriter extends Writer { * @param value * an int. * @throws java.io.IOException + * if an IO error occurred */ public void print(int value) throws IOException { print(String.valueOf(value)); @@ -102,8 +100,10 @@ public class ThrowingPrintWriter extends Writer { /** * Print a long as string * - * @param value a long. + * @param value + * a long. * @throws java.io.IOException + * if an IO error occurred */ public void print(long value) throws IOException { print(String.valueOf(value)); @@ -112,8 +112,10 @@ public class ThrowingPrintWriter extends Writer { /** * Print a short as string * - * @param value a short. + * @param value + * a short. * @throws java.io.IOException + * if an IO error occurred */ public void print(short value) throws IOException { print(String.valueOf(value)); @@ -128,6 +130,7 @@ public class ThrowingPrintWriter extends Writer { * @param args * objects. * @throws java.io.IOException + * if an IO error occurred */ public void format(String fmt, Object... args) throws IOException { print(String.format(fmt, args)); @@ -139,6 +142,7 @@ public class ThrowingPrintWriter extends Writer { * @param any * an object. * @throws java.io.IOException + * if an IO error occurred */ public void print(Object any) throws IOException { out.write(String.valueOf(any)); 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 index 1947b3bf04..4d9f83d233 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutInputStream.java @@ -63,7 +63,6 @@ public class TimeoutInputStream extends FilterInputStream { timeout = millis; } - /** {@inheritDoc} */ @Override public int read() throws IOException { try { @@ -76,13 +75,11 @@ public class TimeoutInputStream extends FilterInputStream { } } - /** {@inheritDoc} */ @Override public int read(byte[] buf) throws IOException { return read(buf, 0, buf.length); } - /** {@inheritDoc} */ @Override public int read(byte[] buf, int off, int cnt) throws IOException { try { @@ -95,7 +92,6 @@ public class TimeoutInputStream extends FilterInputStream { } } - /** {@inheritDoc} */ @Override public long skip(long cnt) throws IOException { try { 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 index 3fbf6ffdcb..afd798a1a2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutOutputStream.java @@ -65,7 +65,6 @@ public class TimeoutOutputStream extends OutputStream { timeout = millis; } - /** {@inheritDoc} */ @Override public void write(int b) throws IOException { try { @@ -78,13 +77,11 @@ public class TimeoutOutputStream extends OutputStream { } } - /** {@inheritDoc} */ @Override public void write(byte[] buf) throws IOException { write(buf, 0, buf.length); } - /** {@inheritDoc} */ @Override public void write(byte[] buf, int off, int len) throws IOException { try { @@ -97,7 +94,6 @@ public class TimeoutOutputStream extends OutputStream { } } - /** {@inheritDoc} */ @Override public void flush() throws IOException { try { @@ -110,7 +106,6 @@ public class TimeoutOutputStream extends OutputStream { } } - /** {@inheritDoc} */ @Override public void close() throws IOException { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/UnionInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/UnionInputStream.java index 459888190f..7e950f6529 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/UnionInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/UnionInputStream.java @@ -12,8 +12,8 @@ package org.eclipse.jgit.util.io; import java.io.IOException; import java.io.InputStream; -import java.util.Iterator; -import java.util.LinkedList; +import java.util.ArrayDeque; +import java.util.Deque; /** * An InputStream which reads from one or more InputStreams. @@ -34,7 +34,7 @@ public class UnionInputStream extends InputStream { } }; - private final LinkedList<InputStream> streams = new LinkedList<>(); + private final Deque<InputStream> streams = new ArrayDeque<>(); /** * Create an empty InputStream that is currently at EOF state. @@ -91,7 +91,6 @@ public class UnionInputStream extends InputStream { return streams.isEmpty(); } - /** {@inheritDoc} */ @Override public int read() throws IOException { for (;;) { @@ -106,7 +105,6 @@ public class UnionInputStream extends InputStream { } } - /** {@inheritDoc} */ @Override public int read(byte[] b, int off, int len) throws IOException { if (len == 0) @@ -123,13 +121,11 @@ public class UnionInputStream extends InputStream { } } - /** {@inheritDoc} */ @Override public int available() throws IOException { return head().available(); } - /** {@inheritDoc} */ @Override public long skip(long count) throws IOException { long skipped = 0; @@ -163,19 +159,18 @@ public class UnionInputStream extends InputStream { return skipped; } - /** {@inheritDoc} */ @Override public void close() throws IOException { IOException err = null; - for (Iterator<InputStream> i = streams.iterator(); i.hasNext();) { + for (InputStream stream : streams) { try { - i.next().close(); + stream.close(); } catch (IOException closeError) { err = closeError; } - i.remove(); } + streams.clear(); if (err != null) throw err; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1.java index 56e90d0636..0a56c83040 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1.java @@ -193,7 +193,6 @@ public abstract class SHA1 { * <p> * Implementations not supporting collision detection always return * {@code false}. - * <p> * * @return {@code true} if a likely collision was detected. */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1Java.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1Java.java index 213ee97531..33e6875883 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1Java.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1Java.java @@ -407,7 +407,7 @@ class SHA1Java extends SHA1 { private static int s1(int a, int b, int c, int d, int w_t) { return rotateLeft(a, 5) // f: 0 <= t <= 19 - + ((b & c) | ((~b) & d)) + + ((b & c) | (~b & d)) + 0x5A827999 + w_t; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/UbcCheck.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/UbcCheck.java index cebdbee27a..91ee3cc9be 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/UbcCheck.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/UbcCheck.java @@ -89,6 +89,7 @@ final class UbcCheck { private static final int DV_II_55_0_bit = 1 << 30; private static final int DV_II_56_0_bit = 1 << 31; + @SuppressWarnings("UnnecessaryParentheses") static int check(int[] w) { int mask = ~0; mask &= (((((w[44] ^ w[45]) >>> 29) & 1) - 1) | ~(DV_I_48_0_bit diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicSystemClock.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicSystemClock.java index 66857b5bf8..4e079f08b5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicSystemClock.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicSystemClock.java @@ -37,7 +37,6 @@ public class MonotonicSystemClock implements MonotonicClock { } } - /** {@inheritDoc} */ @Override public ProposedTimestamp propose() { final long u = nowMicros(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java index 8c20423bc6..a20eaaf908 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java @@ -138,7 +138,10 @@ public abstract class ProposedTimestamp implements AutoCloseable { * Get time since epoch, with up to microsecond resolution. * * @return time since epoch, with up to microsecond resolution. + * + * @deprecated Use instant() instead */ + @Deprecated(since = "7.2") public Timestamp timestamp() { return Timestamp.from(instant()); } @@ -147,7 +150,10 @@ public abstract class ProposedTimestamp implements AutoCloseable { * Get time since epoch, with up to millisecond resolution. * * @return time since epoch, with up to millisecond resolution. + * + * @deprecated Use instant() instead */ + @Deprecated(since = "7.2") public Date date() { return new Date(millis()); } @@ -162,7 +168,6 @@ public abstract class ProposedTimestamp implements AutoCloseable { // Do nothing by default. } - /** {@inheritDoc} */ @Override public String toString() { return instant().toString(); |