diff options
Diffstat (limited to 'org.eclipse.jgit')
76 files changed, 4005 insertions, 835 deletions
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 159736e7b8..24998cc8fe 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -39,6 +39,28 @@ <message_argument value="SHA1_IMPLEMENTATION"/> </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$TrustPackedRefsStat"> + <filter id="1142947843"> + <message_arguments> + <message_argument value="6.1.1"/> + <message_argument value="TrustPackedRefsStat"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/lib/ObjectDatabase.java" type="org.eclipse.jgit.lib.ObjectDatabase"> + <filter id="336695337"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.ObjectDatabase"/> + <message_argument value="getApproximateObjectCount()"/> + </message_arguments> + </filter> </resource> <resource path="src/org/eclipse/jgit/lib/Repository.java" type="org.eclipse.jgit.lib.Repository"> <filter id="1142947843"> @@ -48,7 +70,47 @@ </message_arguments> </filter> </resource> + <resource path="src/org/eclipse/jgit/lib/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter"> + <filter id="403767336"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/> + <message_argument value="UNSET_INT"/> + </message_arguments> + </filter> + <filter id="403804204"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/> + <message_argument value="getIntInRange(Config, String, String, String, int, int, int)"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/merge/ResolveMerger.java" type="org.eclipse.jgit.merge.ResolveMerger"> + <filter id="338792546"> + <message_arguments> + <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/> + <message_argument value="addCheckoutMetadata(String, Attributes)"/> + </message_arguments> + </filter> + <filter id="338792546"> + <message_arguments> + <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/> + <message_argument value="addToCheckout(String, DirCacheEntry, Attributes)"/> + </message_arguments> + </filter> + <filter id="338792546"> + <message_arguments> + <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/> + <message_argument value="processEntry(CanonicalTreeParser, CanonicalTreeParser, CanonicalTreeParser, DirCacheBuildIterator, WorkingTreeIterator, boolean, Attributes)"/> + </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="1142947843"> <message_arguments> <message_argument value="5.13.2"/> @@ -76,6 +138,22 @@ </message_arguments> </filter> </resource> + <resource path="src/org/eclipse/jgit/transport/BasePackPushConnection.java" type="org.eclipse.jgit.transport.BasePackPushConnection"> + <filter id="338792546"> + <message_arguments> + <message_argument value="org.eclipse.jgit.transport.BasePackPushConnection"/> + <message_argument value="noRepository()"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/transport/PushConfig.java" type="org.eclipse.jgit.transport.PushConfig"> + <filter id="338722907"> + <message_arguments> + <message_argument value="org.eclipse.jgit.transport.PushConfig"/> + <message_argument value="PushConfig()"/> + </message_arguments> + </filter> + </resource> <resource path="src/org/eclipse/jgit/util/HttpSupport.java" type="org.eclipse.jgit.util.HttpSupport"> <filter id="1142947843"> <message_arguments> @@ -84,6 +162,67 @@ </message_arguments> </filter> </resource> + <resource path="src/org/eclipse/jgit/util/sha1/SHA1.java" type="org.eclipse.jgit.util.sha1.SHA1"> + <filter id="337764418"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + </message_arguments> + </filter> + <filter id="421650549"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + <message_argument value="digest()"/> + </message_arguments> + </filter> + <filter id="421650549"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + <message_argument value="digest(MutableObjectId)"/> + </message_arguments> + </filter> + <filter id="421650549"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + <message_argument value="hasCollision()"/> + </message_arguments> + </filter> + <filter id="421650549"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + <message_argument value="reset()"/> + </message_arguments> + </filter> + <filter id="421650549"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + <message_argument value="setDetectCollision(boolean)"/> + </message_arguments> + </filter> + <filter id="421650549"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + <message_argument value="toObjectId()"/> + </message_arguments> + </filter> + <filter id="421650549"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + <message_argument value="update(byte)"/> + </message_arguments> + </filter> + <filter id="421650549"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + <message_argument value="update(byte[])"/> + </message_arguments> + </filter> + <filter id="421650549"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.sha1.SHA1"/> + <message_argument value="update(byte[], int, int)"/> + </message_arguments> + </filter> + </resource> <resource path="src/org/eclipse/jgit/util/sha1/SHA1.java" type="org.eclipse.jgit.util.sha1.SHA1$Sha1Implementation"> <filter id="1142947843"> <message_arguments> diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 87b58e1fc5..808d5d37b3 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -3,12 +3,12 @@ Bundle-ManifestVersion: 2 Bundle-Name: %Bundle-Name Automatic-Module-Name: org.eclipse.jgit Bundle-SymbolicName: org.eclipse.jgit -Bundle-Version: 6.0.1.qualifier +Bundle-Version: 6.1.1.qualifier Bundle-Localization: plugin Bundle-Vendor: %Bundle-Vendor Eclipse-ExtensibleAPI: true -Export-Package: org.eclipse.jgit.annotations;version="6.0.1", - org.eclipse.jgit.api;version="6.0.1"; +Export-Package: org.eclipse.jgit.annotations;version="6.1.1", + org.eclipse.jgit.api;version="6.1.1"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.notes, org.eclipse.jgit.dircache, @@ -23,18 +23,18 @@ Export-Package: org.eclipse.jgit.annotations;version="6.0.1", org.eclipse.jgit.revwalk.filter, org.eclipse.jgit.blame, org.eclipse.jgit.merge", - org.eclipse.jgit.api.errors;version="6.0.1"; + org.eclipse.jgit.api.errors;version="6.1.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.errors", - org.eclipse.jgit.attributes;version="6.0.1"; + org.eclipse.jgit.attributes;version="6.1.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.treewalk", - org.eclipse.jgit.blame;version="6.0.1"; + org.eclipse.jgit.blame;version="6.1.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.diff", - org.eclipse.jgit.diff;version="6.0.1"; + org.eclipse.jgit.diff;version="6.1.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.attributes, org.eclipse.jgit.revwalk, @@ -42,44 +42,48 @@ Export-Package: org.eclipse.jgit.annotations;version="6.0.1", org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.treewalk, org.eclipse.jgit.util", - org.eclipse.jgit.dircache;version="6.0.1"; + org.eclipse.jgit.dircache;version="6.1.1"; 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.0.1"; + org.eclipse.jgit.errors;version="6.1.1"; 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.0.1"; + org.eclipse.jgit.events;version="6.1.1"; uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.fnmatch;version="6.0.1", - org.eclipse.jgit.gitrepo;version="6.0.1"; + org.eclipse.jgit.fnmatch;version="6.1.1", + org.eclipse.jgit.gitrepo;version="6.1.1"; uses:="org.xml.sax.helpers, org.eclipse.jgit.api, org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, org.xml.sax", - org.eclipse.jgit.gitrepo.internal;version="6.0.1";x-internal:=true, - org.eclipse.jgit.hooks;version="6.0.1";uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.ignore;version="6.0.1", - org.eclipse.jgit.ignore.internal;version="6.0.1"; + org.eclipse.jgit.gitrepo.internal;version="6.1.1";x-internal:=true, + org.eclipse.jgit.hooks;version="6.1.1";uses:="org.eclipse.jgit.lib", + org.eclipse.jgit.ignore;version="6.1.1", + org.eclipse.jgit.ignore.internal;version="6.1.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal;version="6.0.1"; + org.eclipse.jgit.internal;version="6.1.1"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.http.test", - org.eclipse.jgit.internal.fsck;version="6.0.1"; + org.eclipse.jgit.internal.diffmergetool;version="6.1.1"; + x-friends:="org.eclipse.jgit.test, + org.eclipse.jgit.pgm.test, + org.eclipse.jgit.pgm", + org.eclipse.jgit.internal.fsck;version="6.1.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.revwalk;version="6.0.1"; + org.eclipse.jgit.internal.revwalk;version="6.1.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.storage.dfs;version="6.0.1"; + org.eclipse.jgit.internal.storage.dfs;version="6.1.1"; 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.0.1"; + org.eclipse.jgit.internal.storage.file;version="6.1.1"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.junit, org.eclipse.jgit.junit.http, @@ -88,32 +92,32 @@ Export-Package: org.eclipse.jgit.annotations;version="6.0.1", org.eclipse.jgit.pgm, org.eclipse.jgit.pgm.test, org.eclipse.jgit.ssh.apache", - org.eclipse.jgit.internal.storage.io;version="6.0.1"; + org.eclipse.jgit.internal.storage.io;version="6.1.1"; x-friends:="org.eclipse.jgit.junit, org.eclipse.jgit.test, org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.pack;version="6.0.1"; + org.eclipse.jgit.internal.storage.pack;version="6.1.1"; x-friends:="org.eclipse.jgit.junit, org.eclipse.jgit.test, org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.reftable;version="6.0.1"; + org.eclipse.jgit.internal.storage.reftable;version="6.1.1"; 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.0.1";x-internal:=true, - org.eclipse.jgit.internal.transport.connectivity;version="6.0.1"; + org.eclipse.jgit.internal.submodule;version="6.1.1";x-internal:=true, + org.eclipse.jgit.internal.transport.connectivity;version="6.1.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.transport.http;version="6.0.1"; + org.eclipse.jgit.internal.transport.http;version="6.1.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.transport.parser;version="6.0.1"; + org.eclipse.jgit.internal.transport.parser;version="6.1.1"; x-friends:="org.eclipse.jgit.http.server, org.eclipse.jgit.test", - org.eclipse.jgit.internal.transport.ssh;version="6.0.1"; + org.eclipse.jgit.internal.transport.ssh;version="6.1.1"; x-friends:="org.eclipse.jgit.ssh.apache, org.eclipse.jgit.ssh.jsch, org.eclipse.jgit.test", - org.eclipse.jgit.lib;version="6.0.1"; + org.eclipse.jgit.lib;version="6.1.1"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.util.sha1, org.eclipse.jgit.dircache, @@ -127,10 +131,11 @@ Export-Package: org.eclipse.jgit.annotations;version="6.0.1", org.eclipse.jgit.util, org.eclipse.jgit.submodule, org.eclipse.jgit.util.time", - org.eclipse.jgit.lib.internal;version="6.0.1"; - x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.logging;version="6.0.1", - org.eclipse.jgit.merge;version="6.0.1"; + org.eclipse.jgit.lib.internal;version="6.1.1"; + x-friends:="org.eclipse.jgit.test, + org.eclipse.jgit.pgm", + org.eclipse.jgit.logging;version="6.1.1", + org.eclipse.jgit.merge;version="6.1.1"; uses:="org.eclipse.jgit.dircache, org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, @@ -139,40 +144,40 @@ Export-Package: org.eclipse.jgit.annotations;version="6.0.1", org.eclipse.jgit.util, org.eclipse.jgit.api, org.eclipse.jgit.attributes", - org.eclipse.jgit.nls;version="6.0.1", - org.eclipse.jgit.notes;version="6.0.1"; + org.eclipse.jgit.nls;version="6.1.1", + org.eclipse.jgit.notes;version="6.1.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk, org.eclipse.jgit.merge", - org.eclipse.jgit.patch;version="6.0.1"; + org.eclipse.jgit.patch;version="6.1.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.diff", - org.eclipse.jgit.revplot;version="6.0.1"; + org.eclipse.jgit.revplot;version="6.1.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.revwalk", - org.eclipse.jgit.revwalk;version="6.0.1"; + org.eclipse.jgit.revwalk;version="6.1.1"; uses:="org.eclipse.jgit.lib, 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.0.1"; + org.eclipse.jgit.revwalk.filter;version="6.1.1"; uses:="org.eclipse.jgit.revwalk, org.eclipse.jgit.lib, org.eclipse.jgit.util", - org.eclipse.jgit.storage.file;version="6.0.1"; + org.eclipse.jgit.storage.file;version="6.1.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.util", - org.eclipse.jgit.storage.pack;version="6.0.1"; + org.eclipse.jgit.storage.pack;version="6.1.1"; uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.submodule;version="6.0.1"; + org.eclipse.jgit.submodule;version="6.1.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.diff, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.treewalk, org.eclipse.jgit.util", - org.eclipse.jgit.transport;version="6.0.1"; + org.eclipse.jgit.transport;version="6.1.1"; uses:="javax.crypto, org.eclipse.jgit.util.io, org.eclipse.jgit.lib, @@ -185,21 +190,21 @@ Export-Package: org.eclipse.jgit.annotations;version="6.0.1", org.eclipse.jgit.transport.resolver, org.eclipse.jgit.storage.pack, org.eclipse.jgit.errors", - org.eclipse.jgit.transport.http;version="6.0.1"; + org.eclipse.jgit.transport.http;version="6.1.1"; uses:="javax.net.ssl", - org.eclipse.jgit.transport.resolver;version="6.0.1"; + org.eclipse.jgit.transport.resolver;version="6.1.1"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.lib", - org.eclipse.jgit.treewalk;version="6.0.1"; + org.eclipse.jgit.treewalk;version="6.1.1"; 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.0.1"; + org.eclipse.jgit.treewalk.filter;version="6.1.1"; uses:="org.eclipse.jgit.treewalk", - org.eclipse.jgit.util;version="6.0.1"; + org.eclipse.jgit.util;version="6.1.1"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.hooks, org.eclipse.jgit.revwalk, @@ -212,12 +217,12 @@ Export-Package: org.eclipse.jgit.annotations;version="6.0.1", org.eclipse.jgit.treewalk, javax.net.ssl, org.eclipse.jgit.util.time", - org.eclipse.jgit.util.io;version="6.0.1"; + org.eclipse.jgit.util.io;version="6.1.1"; uses:="org.eclipse.jgit.attributes, org.eclipse.jgit.lib, org.eclipse.jgit.treewalk", - org.eclipse.jgit.util.sha1;version="6.0.1", - org.eclipse.jgit.util.time;version="6.0.1" + org.eclipse.jgit.util.sha1;version="6.1.1", + org.eclipse.jgit.util.time;version="6.1.1" Bundle-RequiredExecutionEnvironment: JavaSE-11 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", javax.crypto, diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF index 302c4548a3..33e6156773 100644 --- a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF @@ -3,5 +3,5 @@ Bundle-ManifestVersion: 2 Bundle-Name: org.eclipse.jgit - Sources Bundle-SymbolicName: org.eclipse.jgit.source Bundle-Vendor: Eclipse.org - JGit -Bundle-Version: 6.0.1.qualifier -Eclipse-SourceBundle: org.eclipse.jgit;version="6.0.1.qualifier";roots="." +Bundle-Version: 6.1.1.qualifier +Eclipse-SourceBundle: org.eclipse.jgit;version="6.1.1.qualifier";roots="." diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml index e78227250f..f0e92b3894 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.0.1-SNAPSHOT</version> + <version>6.1.1-SNAPSHOT</version> </parent> <artifactId>org.eclipse.jgit</artifactId> 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 c887c6dfbf..5534a0a44c 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -359,6 +359,8 @@ initFailedNonBareRepoSameDirs=When initializing a non-bare repo with directory { inMemoryBufferLimitExceeded=In-memory buffer limit exceeded inputDidntMatchLength=Input did not match supplied length. {0} bytes are missing. 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} integerValueOutOfRange=Integer value {0}.{1} out of range internalRevisionError=internal revision error internalServerError=internal server error @@ -370,6 +372,7 @@ invalidAwsApiSignatureVersion=Invalid aws.api.signature.version: {0} invalidBooleanValue=Invalid boolean value: {0}.{1}={2} invalidChannel=Invalid channel {0} invalidCommitParentNumber=Invalid commit parent number +invalidCoreAbbrev=Invalid value {0} of option core.abbrev invalidDepth=Invalid depth: {0} invalidEncoding=Invalid encoding from git config i18n.commitEncoding: {0} invalidEncryption=Invalid encryption @@ -571,6 +574,11 @@ pushCertificateInvalidField=Push certificate has missing or invalid value for {0 pushCertificateInvalidFieldValue=Push certificate has missing or invalid value for {0}: {1} pushCertificateInvalidHeader=Push certificate has invalid header format pushCertificateInvalidSignature=Push certificate has invalid signature format +pushDefaultNothing=No refspec given and push.default=nothing; no upstream branch can be determined +pushDefaultNoUpstream=No upstream branch found for local branch ''{0}'' +pushDefaultSimple=push.default=simple requires local branch name ''{0}'' to be equal to upstream tracked branch name ''{1}'' +pushDefaultTriangularUpstream=push.default=upstream cannot be used when the push remote ''{0}'' is different from the fetch remote ''{1}'' +pushDefaultUnknown=Unknown push.default={0}; cannot push pushIsNotSupportedForBundleTransport=Push is not supported for bundle transport pushNotPermitted=push not permitted pushOptionsNotSupported=Push options not supported; received {0} 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 7922f9e729..f88179ac1a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.IOException; import java.text.MessageFormat; import java.util.LinkedList; @@ -124,7 +126,7 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { final RevCommit srcParent = getParentCommit(srcCommit, revWalk); String ourName = calculateOurName(headRef); - String cherryPickName = srcCommit.getId().abbreviate(7).name() + String cherryPickName = srcCommit.getId().abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name() + " " + srcCommit.getShortMessage(); //$NON-NLS-1$ Merger merger = strategy.newMerger(repo); @@ -183,7 +185,7 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { if (unmergedPaths != null) { message = new MergeMessageFormatter() .formatWithConflicts(srcCommit.getFullMessage(), - unmergedPaths); + unmergedPaths, '#'); } else { message = srcCommit.getFullMessage(); } 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 37f1d482aa..7a591aa3b5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; @@ -46,6 +47,8 @@ import org.eclipse.jgit.hooks.PostCommitHook; import org.eclipse.jgit.hooks.PreCommitHook; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.CommitConfig; +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; @@ -133,6 +136,12 @@ public class CommitCommand extends GitCommand<RevCommit> { private CredentialsProvider credentialsProvider; + private @NonNull CleanupMode cleanupMode = CleanupMode.VERBATIM; + + private boolean cleanDefaultIsStrip = true; + + private Character commentChar; + /** * Constructor for CommitCommand * @@ -200,7 +209,7 @@ public class CommitCommand extends GitCommand<RevCommit> { throw new WrongRepositoryStateException( JGitText.get().commitAmendOnInitialNotPossible); - if (headId != null) + if (headId != null) { if (amend) { RevCommit previousCommit = rw.parseCommit(headId); for (RevCommit p : previousCommit.getParents()) @@ -210,7 +219,7 @@ public class CommitCommand extends GitCommand<RevCommit> { } else { parents.add(0, headId); } - + } if (!noVerify) { message = Hooks .commitMsg(repo, @@ -219,6 +228,19 @@ public class CommitCommand extends GitCommand<RevCommit> { .setCommitMessage(message).call(); } + CommitConfig config = null; + if (CleanupMode.DEFAULT.equals(cleanupMode)) { + config = repo.getConfig().get(CommitConfig.KEY); + cleanupMode = config.resolve(cleanupMode, cleanDefaultIsStrip); + } + char comments; + if (commentChar == null) { + comments = '#'; // TODO use git config core.commentChar + } else { + comments = commentChar.charValue(); + } + message = CommitConfig.cleanText(message, cleanupMode, comments); + RevCommit revCommit; DirCache index = repo.lockDirCache(); try (ObjectInserter odi = repo.newObjectInserter()) { @@ -658,6 +680,57 @@ public class CommitCommand extends GitCommand<RevCommit> { } /** + * Sets the {@link CleanupMode} to apply to the commit message. If not + * called, {@link CommitCommand} applies {@link CleanupMode#VERBATIM}. + * + * @param mode + * {@link CleanupMode} to set + * @return {@code this} + * @since 6.1 + */ + public CommitCommand setCleanupMode(@NonNull CleanupMode mode) { + checkCallable(); + this.cleanupMode = mode; + return this; + } + + /** + * Sets the default clean mode if {@link #setCleanupMode(CleanupMode) + * setCleanupMode(CleanupMode.DEFAULT)} is set and git config + * {@code commit.cleanup = default} or is not set. + * + * @param strip + * if {@code true}, default to {@link CleanupMode#STRIP}; + * otherwise default to {@link CleanupMode#WHITESPACE} + * @return {@code this} + * @since 6.1 + */ + public CommitCommand setDefaultClean(boolean strip) { + checkCallable(); + this.cleanDefaultIsStrip = strip; + return this; + } + + /** + * Sets the comment character to apply when cleaning a commit message. If + * {@code null} (the default) and the {@link #setCleanupMode(CleanupMode) + * clean-up mode} is {@link CleanupMode#STRIP} or + * {@link CleanupMode#SCISSORS}, the value of git config + * {@code core.commentChar} will be used. + * + * @param commentChar + * the comment character, or {@code null} to use the value from + * the git config + * @return {@code this} + * @since 6.1 + */ + public CommitCommand setCommentCharacter(Character commentChar) { + checkCallable(); + this.commentChar = commentChar; + return this; + } + + /** * Set whether to allow to create an empty commit * * @param allowEmpty @@ -806,7 +879,7 @@ public class CommitCommand extends GitCommand<RevCommit> { * command line. * * @param amend - * whether to ammend the tip of the current branch + * whether to amend the tip of the current branch * @return {@code this} */ public CommitCommand setAmend(boolean amend) { 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 1e524fadab..805a886392 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java @@ -11,6 +11,7 @@ package org.eclipse.jgit.api; import static org.eclipse.jgit.lib.Constants.R_REFS; import static org.eclipse.jgit.lib.Constants.R_TAGS; +import static org.eclipse.jgit.lib.TypedConfigGetter.UNSET_INT; import java.io.IOException; import java.text.MessageFormat; @@ -33,6 +34,7 @@ import org.eclipse.jgit.errors.InvalidPatternException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.fnmatch.FileNameMatcher; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.AbbrevConfig; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -89,6 +91,11 @@ public class DescribeCommand extends GitCommand<String> { private boolean always; /** + * The prefix length to use when abbreviating a commit hash. + */ + private int abbrev = UNSET_INT; + + /** * Constructor for DescribeCommand. * * @param repo @@ -205,12 +212,33 @@ public class DescribeCommand extends GitCommand<String> { return this; } + /** + * Sets the prefix length to use when abbreviating an object SHA-1. + * + * @param abbrev + * minimum length of the abbreviated string. Must be in the range + * [{@value AbbrevConfig#MIN_ABBREV}, + * {@value Constants#OBJECT_ID_STRING_LENGTH}]. + * @return {@code this} + * @since 6.1 + */ + public DescribeCommand setAbbrev(int abbrev) { + if (abbrev == 0) { + this.abbrev = 0; + } else { + this.abbrev = AbbrevConfig.capAbbrev(abbrev); + } + return this; + } + private String longDescription(Ref tag, int depth, ObjectId tip) throws IOException { - return String.format( - "%s-%d-g%s", formatRefName(tag.getName()), //$NON-NLS-1$ - Integer.valueOf(depth), w.getObjectReader().abbreviate(tip) - .name()); + if (abbrev == 0) { + return formatRefName(tag.getName()); + } + return String.format("%s-%d-g%s", formatRefName(tag.getName()), //$NON-NLS-1$ + Integer.valueOf(depth), + w.getObjectReader().abbreviate(tip, abbrev).name()); } /** @@ -302,6 +330,9 @@ public class DescribeCommand extends GitCommand<String> { if (target == null) { setTarget(Constants.HEAD); } + if (abbrev == UNSET_INT) { + abbrev = AbbrevConfig.parseFromConfig(repo).get(); + } Collection<Ref> tagList = repo.getRefDatabase() .getRefsByPrefix(useAll ? R_REFS : R_TAGS); @@ -413,7 +444,12 @@ public class DescribeCommand extends GitCommand<String> { // if all the nodes are dominated by all the tags, the walk stops if (candidates.isEmpty()) { - return always ? w.getObjectReader().abbreviate(target).name() : null; + return always + ? w.getObjectReader() + .abbreviate(target, + AbbrevConfig.capAbbrev(abbrev)) + .name() + : null; } Candidate best = Collections.min(candidates, 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 ef56d802c8..ce068b6306 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java @@ -405,7 +405,7 @@ public class MergeCommand extends GitCommand<MergeResult> { failingPaths, null); } String mergeMessageWithConflicts = new MergeMessageFormatter() - .formatWithConflicts(mergeMessage, unmergedPaths); + .formatWithConflicts(mergeMessage, unmergedPaths, '#'); repo.writeMergeCommitMsg(mergeMessageWithConflicts); return new MergeResult(null, merger.getBaseCommitId(), new ObjectId[] { headCommit.getId(), 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 aa5a63499c..08353dfdfa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> and others + * Copyright (C) 2010, 2022 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 @@ -21,7 +21,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.eclipse.jgit.api.errors.DetachedHeadException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.NotSupportedException; @@ -29,11 +31,16 @@ import org.eclipse.jgit.errors.TooLargeObjectInPackException; import org.eclipse.jgit.errors.TooLargePackException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.BranchConfig; +import org.eclipse.jgit.lib.Config; +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.Repository; +import org.eclipse.jgit.transport.PushConfig; +import org.eclipse.jgit.transport.PushConfig.PushDefault; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RefLeaseSpec; import org.eclipse.jgit.transport.RefSpec; @@ -52,7 +59,7 @@ import org.eclipse.jgit.transport.Transport; public class PushCommand extends TransportCommand<PushCommand, Iterable<PushResult>> { - private String remote = Constants.DEFAULT_REMOTE_NAME; + private String remote; private final List<RefSpec> refSpecs; @@ -71,6 +78,10 @@ public class PushCommand extends private List<String> pushOptions; + // Legacy behavior as default. Use setPushDefault(null) to determine the + // value from the git config. + private PushDefault pushDefault = PushDefault.CURRENT; + /** * <p> * Constructor for PushCommand. @@ -98,19 +109,20 @@ public class PushCommand extends InvalidRemoteException, org.eclipse.jgit.api.errors.TransportException { checkCallable(); + setCallable(false); ArrayList<PushResult> pushResults = new ArrayList<>(3); try { + Config config = repo.getConfig(); + remote = determineRemote(config, remote); if (refSpecs.isEmpty()) { - RemoteConfig config = new RemoteConfig(repo.getConfig(), + RemoteConfig rc = new RemoteConfig(config, getRemote()); - refSpecs.addAll(config.getPushRefSpecs()); - } - if (refSpecs.isEmpty()) { - Ref head = repo.exactRef(Constants.HEAD); - if (head != null && head.isSymbolic()) - refSpecs.add(new RefSpec(head.getLeaf().getName())); + refSpecs.addAll(rc.getPushRefSpecs()); + if (refSpecs.isEmpty()) { + determineDefaultRefSpecs(config); + } } if (force) { @@ -118,8 +130,8 @@ public class PushCommand extends refSpecs.set(i, refSpecs.get(i).setForceUpdate(true)); } - final List<Transport> transports; - transports = Transport.openAll(repo, remote, Transport.Operation.PUSH); + List<Transport> transports = Transport.openAll(repo, remote, + Transport.Operation.PUSH); for (@SuppressWarnings("resource") // Explicitly closed in finally final Transport transport : transports) { transport.setPushThin(thin); @@ -171,6 +183,102 @@ public class PushCommand extends return pushResults; } + private String determineRemote(Config config, String remoteName) + throws IOException { + if (remoteName != null) { + return remoteName; + } + Ref head = repo.exactRef(Constants.HEAD); + String effectiveRemote = null; + BranchConfig branchCfg = null; + if (head != null && head.isSymbolic()) { + String currentBranch = head.getLeaf().getName(); + branchCfg = new BranchConfig(config, + Repository.shortenRefName(currentBranch)); + effectiveRemote = branchCfg.getPushRemote(); + } + if (effectiveRemote == null) { + effectiveRemote = config.getString( + ConfigConstants.CONFIG_REMOTE_SECTION, null, + ConfigConstants.CONFIG_KEY_PUSH_DEFAULT); + if (effectiveRemote == null && branchCfg != null) { + effectiveRemote = branchCfg.getRemote(); + } + } + if (effectiveRemote == null) { + effectiveRemote = Constants.DEFAULT_REMOTE_NAME; + } + return effectiveRemote; + } + + private String getCurrentBranch() + throws IOException, DetachedHeadException { + Ref head = repo.exactRef(Constants.HEAD); + if (head != null && head.isSymbolic()) { + return head.getLeaf().getName(); + } + throw new DetachedHeadException(); + } + + private void determineDefaultRefSpecs(Config config) + throws IOException, GitAPIException { + if (pushDefault == null) { + pushDefault = config.get(PushConfig::new).getPushDefault(); + } + switch (pushDefault) { + case CURRENT: + refSpecs.add(new RefSpec(getCurrentBranch())); + break; + case MATCHING: + refSpecs.add(new RefSpec(":")); //$NON-NLS-1$ + break; + case NOTHING: + throw new InvalidRefNameException( + JGitText.get().pushDefaultNothing); + case SIMPLE: + case UPSTREAM: + String currentBranch = getCurrentBranch(); + BranchConfig branchCfg = new BranchConfig(config, + Repository.shortenRefName(currentBranch)); + String fetchRemote = branchCfg.getRemote(); + if (fetchRemote == null) { + fetchRemote = Constants.DEFAULT_REMOTE_NAME; + } + boolean isTriangular = !fetchRemote.equals(remote); + if (isTriangular) { + if (PushDefault.UPSTREAM.equals(pushDefault)) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().pushDefaultTriangularUpstream, + remote, fetchRemote)); + } + // Strange, but consistent with C git: "simple" doesn't even + // check whether there is a configured upstream, and if so, that + // it is equal to the local branch name. It just becomes + // "current". + refSpecs.add(new RefSpec(currentBranch)); + } else { + String trackedBranch = branchCfg.getMerge(); + if (branchCfg.isRemoteLocal() || trackedBranch == null + || !trackedBranch.startsWith(Constants.R_HEADS)) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().pushDefaultNoUpstream, + currentBranch)); + } + if (PushDefault.SIMPLE.equals(pushDefault) + && !trackedBranch.equals(currentBranch)) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().pushDefaultSimple, currentBranch, + trackedBranch)); + } + refSpecs.add(new RefSpec(currentBranch + ':' + trackedBranch)); + } + break; + default: + throw new InvalidRefNameException(MessageFormat + .format(JGitText.get().pushDefaultUnknown, pushDefault)); + } + } + /** * The remote (uri or name) used for the push operation. If no remote is * set, the default value of <code>Constants.DEFAULT_REMOTE_NAME</code> will @@ -336,9 +444,37 @@ public class PushCommand extends } /** + * Retrieves the {@link PushDefault} currently set. + * + * @return the {@link PushDefault}, or {@code null} if not set + * @since 6.1 + */ + public PushDefault getPushDefault() { + return pushDefault; + } + + /** + * Sets an explicit {@link PushDefault}. The default used if this is not + * called is {@link PushDefault#CURRENT} for compatibility reasons with + * earlier JGit versions. + * + * @param pushDefault + * {@link PushDefault} to set; if {@code null} the value defined + * in the git config will be used. + * + * @return {@code this} + * @since 6.1 + */ + public PushCommand setPushDefault(PushDefault pushDefault) { + checkCallable(); + this.pushDefault = pushDefault; + return this; + } + + /** * Push all branches under refs/heads/*. * - * @return {code this} + * @return {@code this} */ public PushCommand setPushAll() { refSpecs.add(Transport.REFSPEC_PUSH_ALL); @@ -348,7 +484,7 @@ public class PushCommand extends /** * Push all tags under refs/tags/*. * - * @return {code this} + * @return {@code this} */ public PushCommand setPushTags() { refSpecs.add(Transport.REFSPEC_TAGS); 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 a26ffc2e66..2b0d8ce1c9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.api.RebaseResult.Status; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.CheckoutConflictException; @@ -52,6 +53,8 @@ import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.CommitConfig; +import org.eclipse.jgit.lib.CommitConfig.CleanupMode; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; @@ -205,6 +208,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> { private InteractiveHandler interactiveHandler; + private CommitConfig commitConfig; + private boolean stopAfterInitialization = false; private RevCommit newHead; @@ -246,6 +251,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { lastStepWasForward = false; checkCallable(); checkParameters(); + commitConfig = repo.getConfig().get(CommitConfig.KEY); try { switch (operation) { case ABORT: @@ -441,11 +447,16 @@ public class RebaseCommand extends GitCommand<RebaseResult> { return null; // continue rebase process on pick command case REWORD: String oldMessage = commitToPick.getFullMessage(); - String newMessage = interactiveHandler - .modifyCommitMessage(oldMessage); + CleanupMode mode = commitConfig.resolve(CleanupMode.DEFAULT, true); + boolean[] doChangeId = { false }; + String newMessage = editCommitMessage(doChangeId, oldMessage, mode); try (Git git = new Git(repo)) { - newHead = git.commit().setMessage(newMessage).setAmend(true) - .setNoVerify(true).call(); + newHead = git.commit() + .setMessage(newMessage) + .setAmend(true) + .setNoVerify(true) + .setInsertChangeId(doChangeId[0]) + .call(); } return null; case EDIT: @@ -460,17 +471,49 @@ public class RebaseCommand extends GitCommand<RebaseResult> { resetSoftToParent(); List<RebaseTodoLine> steps = repo.readRebaseTodo( rebaseState.getPath(GIT_REBASE_TODO), false); - RebaseTodoLine nextStep = steps.isEmpty() ? null : steps.get(0); + boolean isLast = steps.isEmpty(); + if (!isLast) { + switch (steps.get(0).getAction()) { + case FIXUP: + case SQUASH: + break; + default: + isLast = true; + break; + } + } File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP); File messageSquashFile = rebaseState.getFile(MESSAGE_SQUASH); - if (isSquash && messageFixupFile.exists()) + if (isSquash && messageFixupFile.exists()) { messageFixupFile.delete(); - newHead = doSquashFixup(isSquash, commitToPick, nextStep, + } + newHead = doSquashFixup(isSquash, commitToPick, isLast, messageFixupFile, messageSquashFile); } return null; } + private String editCommitMessage(boolean[] doChangeId, String message, + @NonNull CleanupMode mode) { + String newMessage; + CommitConfig.CleanupMode cleanup; + if (interactiveHandler instanceof InteractiveHandler2) { + InteractiveHandler2.ModifyResult modification = ((InteractiveHandler2) interactiveHandler) + .editCommitMessage(message, mode, '#'); + newMessage = modification.getMessage(); + cleanup = modification.getCleanupMode(); + if (CleanupMode.DEFAULT.equals(cleanup)) { + cleanup = mode; + } + doChangeId[0] = modification.shouldAddChangeId(); + } else { + newMessage = interactiveHandler.modifyCommitMessage(message); + cleanup = CommitConfig.CleanupMode.STRIP; + doChangeId[0] = false; + } + return CommitConfig.cleanText(newMessage, cleanup, '#'); + } + private RebaseResult cherryPickCommit(RevCommit commitToPick) throws IOException, GitAPIException, NoMessageException, UnmergedPathsException, ConcurrentRefUpdateException, @@ -707,7 +750,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } private RevCommit doSquashFixup(boolean isSquash, RevCommit commitToPick, - RebaseTodoLine nextStep, File messageFixup, File messageSquash) + boolean isLast, File messageFixup, File messageSquash) throws IOException, GitAPIException { if (!messageSquash.exists()) { @@ -717,24 +760,20 @@ public class RebaseCommand extends GitCommand<RebaseResult> { initializeSquashFixupFile(MESSAGE_SQUASH, previousCommit.getFullMessage()); - if (!isSquash) - initializeSquashFixupFile(MESSAGE_FIXUP, - previousCommit.getFullMessage()); + if (!isSquash) { + rebaseState.createFile(MESSAGE_FIXUP, + previousCommit.getFullMessage()); + } } - String currSquashMessage = rebaseState - .readFile(MESSAGE_SQUASH); + String currSquashMessage = rebaseState.readFile(MESSAGE_SQUASH); int count = parseSquashFixupSequenceCount(currSquashMessage) + 1; String content = composeSquashMessage(isSquash, commitToPick, currSquashMessage, count); rebaseState.createFile(MESSAGE_SQUASH, content); - if (messageFixup.exists()) - rebaseState.createFile(MESSAGE_FIXUP, content); - return squashIntoPrevious( - !messageFixup.exists(), - nextStep); + return squashIntoPrevious(!messageFixup.exists(), isLast); } private void resetSoftToParent() throws IOException, @@ -756,26 +795,30 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } private RevCommit squashIntoPrevious(boolean sequenceContainsSquash, - RebaseTodoLine nextStep) + boolean isLast) throws IOException, GitAPIException { RevCommit retNewHead; - String commitMessage = rebaseState - .readFile(MESSAGE_SQUASH); - + String commitMessage; + if (!isLast || sequenceContainsSquash) { + commitMessage = rebaseState.readFile(MESSAGE_SQUASH); + } else { + commitMessage = rebaseState.readFile(MESSAGE_FIXUP); + } try (Git git = new Git(repo)) { - if (nextStep == null || ((nextStep.getAction() != Action.FIXUP) - && (nextStep.getAction() != Action.SQUASH))) { - // this is the last step in this sequence + if (isLast) { + boolean[] doChangeId = { false }; if (sequenceContainsSquash) { - commitMessage = interactiveHandler - .modifyCommitMessage(commitMessage); + commitMessage = editCommitMessage(doChangeId, commitMessage, + CleanupMode.STRIP); } retNewHead = git.commit() - .setMessage(stripCommentLines(commitMessage)) - .setAmend(true).setNoVerify(true).call(); + .setMessage(commitMessage) + .setAmend(true) + .setNoVerify(true) + .setInsertChangeId(doChangeId[0]) + .call(); rebaseState.getFile(MESSAGE_SQUASH).delete(); rebaseState.getFile(MESSAGE_FIXUP).delete(); - } else { // Next step is either Squash or Fixup retNewHead = git.commit().setMessage(commitMessage) @@ -785,21 +828,6 @@ public class RebaseCommand extends GitCommand<RebaseResult> { return retNewHead; } - private static String stripCommentLines(String commitMessage) { - StringBuilder result = new StringBuilder(); - for (String line : commitMessage.split("\n")) { //$NON-NLS-1$ - if (!line.trim().startsWith("#")) //$NON-NLS-1$ - result.append(line).append("\n"); //$NON-NLS-1$ - } - if (!commitMessage.endsWith("\n")) { //$NON-NLS-1$ - int bufferSize = result.length(); - if (bufferSize > 0 && result.charAt(bufferSize - 1) == '\n') { - result.deleteCharAt(bufferSize - 1); - } - } - return result.toString(); - } - @SuppressWarnings("nls") private static String composeSquashMessage(boolean isSquash, RevCommit commitToPick, String currSquashMessage, int count) { @@ -1625,26 +1653,106 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } /** - * Allows configure rebase interactive process and modify commit message + * Allows to configure the interactive rebase process steps and to modify + * commit messages. */ public interface InteractiveHandler { + /** - * Given list of {@code steps} should be modified according to user - * rebase configuration + * Callback API to modify the initial list of interactive rebase steps. + * * @param steps - * initial configuration of rebase interactive + * initial configuration of interactive rebase */ void prepareSteps(List<RebaseTodoLine> steps); /** - * Used for editing commit message on REWORD + * Used for editing commit message on REWORD or SQUASH. * - * @param commit + * @param message + * existing commit message * @return new commit message */ - String modifyCommitMessage(String commit); + String modifyCommitMessage(String message); } + /** + * Extends {@link InteractiveHandler} with an enhanced callback for editing + * commit messages. + * + * @since 6.1 + */ + public interface InteractiveHandler2 extends InteractiveHandler { + + /** + * Callback API for editing a commit message on REWORD or SQUASH. + * <p> + * The callback gets the comment character currently set, and the + * clean-up mode. It can use this information when presenting the + * message to the user, and it also has the possibility to clean the + * message itself (in which case the returned {@link ModifyResult} + * should have {@link CleanupMode#VERBATIM} set lest JGit cleans the + * message again). It can also override the initial clean-up mode by + * returning clean-up mode other than {@link CleanupMode#DEFAULT}. If it + * does return {@code DEFAULT}, the passed-in {@code mode} will be + * applied. + * </p> + * + * @param message + * existing commit message + * @param mode + * {@link CleanupMode} currently set + * @param commentChar + * comment character used + * @return a {@link ModifyResult} + */ + @NonNull + ModifyResult editCommitMessage(@NonNull String message, + @NonNull CleanupMode mode, char commentChar); + + @Override + default String modifyCommitMessage(String message) { + // Should actually not be called; but do something reasonable anyway + ModifyResult result = editCommitMessage( + message == null ? "" : message, CleanupMode.STRIP, //$NON-NLS-1$ + '#'); + return result.getMessage(); + } + + /** + * Describes the result of editing a commit message: the new message, + * and how it should be cleaned. + */ + interface ModifyResult { + + /** + * Retrieves the new commit message. + * + * @return the message + */ + @NonNull + String getMessage(); + + /** + * Tells how the message returned by {@link #getMessage()} should be + * cleaned. + * + * @return the {@link CleanupMode} + */ + @NonNull + CleanupMode getCleanupMode(); + + /** + * Tells whether a Gerrit Change-Id should be computed and added to + * the commit message, as with + * {@link CommitCommand#setInsertChangeId(boolean)}. + * + * @return {@code true} if a Change-Id should be handled, + * {@code false} otherwise + */ + boolean shouldAddChangeId(); + } + } PersonIdent parseAuthor(byte[] raw) { if (raw.length == 0) 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 22ef4d0a32..db88ad8dc9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.IOException; import java.text.MessageFormat; import java.util.LinkedList; @@ -128,8 +130,9 @@ public class RevertCommand extends GitCommand<RevCommit> { revWalk.parseHeaders(srcParent); String ourName = calculateOurName(headRef); - String revertName = srcCommit.getId().abbreviate(7).name() - + " " + srcCommit.getShortMessage(); //$NON-NLS-1$ + String revertName = srcCommit.getId() + .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name() + " " //$NON-NLS-1$ + + srcCommit.getShortMessage(); ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo); merger.setWorkingTreeIterator(new FileTreeIterator(repo)); @@ -183,8 +186,8 @@ public class RevertCommand extends GitCommand<RevCommit> { merger.getMergeResults(), failingPaths, null); if (!merger.failed() && !unmergedPaths.isEmpty()) { String message = new MergeMessageFormatter() - .formatWithConflicts(newMessage, - merger.getUnmergedPaths()); + .formatWithConflicts(newMessage, + merger.getUnmergedPaths(), '#'); repo.writeRevertHead(srcCommit.getId()); repo.writeMergeCommitMsg(message); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java index 35fd8992b6..f7a1f4eff8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -302,7 +304,8 @@ public class StashCreateCommand extends GitCommand<RevCommit> { builder.setParentId(headCommit); builder.setTreeId(cache.writeTree(inserter)); builder.setMessage(MessageFormat.format(indexMessage, branch, - headCommit.abbreviate(7).name(), + headCommit.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name(), headCommit.getShortMessage())); ObjectId indexCommit = inserter.insert(builder); @@ -319,7 +322,10 @@ public class StashCreateCommand extends GitCommand<RevCommit> { builder.setParentIds(new ObjectId[0]); builder.setTreeId(untrackedDirCache.writeTree(inserter)); builder.setMessage(MessageFormat.format(MSG_UNTRACKED, - branch, headCommit.abbreviate(7).name(), + branch, + headCommit + .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name(), headCommit.getShortMessage())); untrackedCommit = inserter.insert(builder); } @@ -339,7 +345,8 @@ public class StashCreateCommand extends GitCommand<RevCommit> { builder.addParentId(untrackedCommit); builder.setMessage(MessageFormat.format( workingDirectoryMessage, branch, - headCommit.abbreviate(7).name(), + headCommit.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name(), headCommit.getShortMessage())); builder.setTreeId(cache.writeTree(inserter)); commitId = inserter.insert(builder); 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 638dd827ed..7ec78597fa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java @@ -1,43 +1,11 @@ /* - * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com> + * Copyright (C) 2015, 2022 Ivan Motsch <ivan.motsch@bsiag.com> and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * 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. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.attributes; @@ -46,6 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.function.Supplier; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.attributes.Attribute.State; @@ -84,6 +53,8 @@ public class AttributesHandler { private final TreeWalk treeWalk; + private final Supplier<CanonicalTreeParser> attributesTree; + private final AttributesNode globalNode; private final AttributesNode infoNode; @@ -98,22 +69,41 @@ public class AttributesHandler { * @param treeWalk * a {@link org.eclipse.jgit.treewalk.TreeWalk} * @throws java.io.IOException + * @deprecated since 6.1, use {@link #AttributesHandler(TreeWalk, Supplier)} + * instead */ + @Deprecated public AttributesHandler(TreeWalk treeWalk) throws IOException { + this(treeWalk, () -> treeWalk.getTree(CanonicalTreeParser.class)); + } + + /** + * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with + * default rules as well as merged rules from global, info and worktree root + * attributes + * + * @param treeWalk + * a {@link org.eclipse.jgit.treewalk.TreeWalk} + * @param attributesTree + * the tree to read .gitattributes from + * @throws java.io.IOException + * @since 6.1 + */ + public AttributesHandler(TreeWalk treeWalk, + Supplier<CanonicalTreeParser> attributesTree) throws IOException { this.treeWalk = treeWalk; - AttributesNodeProvider attributesNodeProvider =treeWalk.getAttributesNodeProvider(); + this.attributesTree = attributesTree; + AttributesNodeProvider attributesNodeProvider = treeWalk + .getAttributesNodeProvider(); this.globalNode = attributesNodeProvider != null ? attributesNodeProvider.getGlobalAttributesNode() : null; this.infoNode = attributesNodeProvider != null ? attributesNodeProvider.getInfoAttributesNode() : null; AttributesNode rootNode = attributesNode(treeWalk, - rootOf( - treeWalk.getTree(WorkingTreeIterator.class)), - rootOf( - treeWalk.getTree(DirCacheIterator.class)), - rootOf(treeWalk - .getTree(CanonicalTreeParser.class))); + rootOf(treeWalk.getTree(WorkingTreeIterator.class)), + rootOf(treeWalk.getTree(DirCacheIterator.class)), + rootOf(attributesTree.get())); expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES); for (AttributesNode node : new AttributesNode[] { globalNode, rootNode, @@ -152,7 +142,7 @@ public class AttributesHandler { isDirectory, treeWalk.getTree(WorkingTreeIterator.class), treeWalk.getTree(DirCacheIterator.class), - treeWalk.getTree(CanonicalTreeParser.class), + attributesTree.get(), attributes); // Gets the attributes located in the global attribute file 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 49da95c9ab..1a5f74f98a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java @@ -18,6 +18,7 @@ import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY; import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME; import static org.eclipse.jgit.diff.DiffEntry.Side.NEW; import static org.eclipse.jgit.diff.DiffEntry.Side.OLD; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; import static org.eclipse.jgit.lib.Constants.encode; import static org.eclipse.jgit.lib.Constants.encodeASCII; import static org.eclipse.jgit.lib.FileMode.GITLINK; @@ -90,7 +91,7 @@ public class DiffFormatter implements AutoCloseable { private int context = 3; - private int abbreviationLength = 7; + private int abbreviationLength = OBJECT_ID_ABBREV_STRING_LENGTH; private DiffAlgorithm diffAlgorithm; 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 c904a782db..3d50a82155 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -4,7 +4,8 @@ * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br> * 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) 2019, 2020, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2017, 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 @@ -299,7 +300,7 @@ public class DirCacheCheckout { walk = new NameConflictTreeWalk(repo); builder = dc.builder(); - addTree(walk, headCommitTree); + walk.setHead(addTree(walk, headCommitTree)); addTree(walk, mergeCommitTree); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); @@ -315,13 +316,6 @@ public class DirCacheCheckout { } } - private void addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, IncorrectObjectTypeException, IOException { - if (id == null) - tw.addTree(new EmptyTreeIterator()); - else - tw.addTree(id); - } - /** * Scan index and merge tree (no HEAD). Used e.g. for initial checkout when * there is no head yet. @@ -341,7 +335,7 @@ public class DirCacheCheckout { builder = dc.builder(); walk = new NameConflictTreeWalk(repo); - addTree(walk, mergeCommitTree); + walk.setHead(addTree(walk, mergeCommitTree)); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); workingTree.setDirCacheIterator(walk, dciPos); @@ -356,6 +350,14 @@ public class DirCacheCheckout { conflicts.removeAll(removed); } + private int addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if (id == null) { + return tw.addTree(new EmptyTreeIterator()); + } + return tw.addTree(id); + } + /** * Processing an entry in the context of {@link #prescanOneTree()} when only * one tree is given @@ -382,17 +384,14 @@ public class DirCacheCheckout { // failOnConflict is false. Putting something to conflicts // would mean we delete it. Instead we want the mergeCommit // content to be checked out. - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } } else - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } else if (f == null || !m.idEqual(i)) { // The working tree file is missing or the merge content differs // from index content - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } else if (i.getDirCacheEntry() != null) { // The index contains a file (and not a folder) if (f.isModified(i.getDirCacheEntry(), true, @@ -400,8 +399,7 @@ public class DirCacheCheckout { || i.getDirCacheEntry().getStage() != 0) // The working tree file is dirty or the index contains a // conflict - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); 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. @@ -802,7 +800,7 @@ public class DirCacheCheckout { if (f != null && isModifiedSubtree_IndexWorkingtree(name)) { conflict(name, dce, h, m); // 1 } else { - update(name, mId, mMode); // 2 + update(1, name, mId, mMode); // 2 } break; @@ -828,7 +826,7 @@ public class DirCacheCheckout { // are found later break; case 0xD0F: // 19 - update(name, mId, mMode); + update(1, name, mId, mMode); break; case 0xDF0: // conflict without a rule case 0x0FD: // 15 @@ -839,7 +837,7 @@ public class DirCacheCheckout { if (isModifiedSubtree_IndexWorkingtree(name)) conflict(name, dce, h, m); // 8 else - update(name, mId, mMode); // 7 + update(1, name, mId, mMode); // 7 } else conflict(name, dce, h, m); // 9 break; @@ -859,7 +857,7 @@ public class DirCacheCheckout { break; case 0x0DF: // 16 17 if (!isModifiedSubtree_IndexWorkingtree(name)) - update(name, mId, mMode); + update(1, name, mId, mMode); else conflict(name, dce, h, m); break; @@ -929,7 +927,7 @@ public class DirCacheCheckout { // At least one of Head, Index, Merge is not empty // -> only Merge contains something for this path. Use it! // Potentially update the file - update(name, mId, mMode); // 1 + update(1, name, mId, mMode); // 1 else if (m == null) // Nothing in Merge // Something in Head @@ -947,7 +945,7 @@ public class DirCacheCheckout { // find in Merge. Potentially updates the file. if (equalIdAndMode(hId, hMode, mId, mMode)) { if (initialCheckout || force) { - update(name, mId, mMode); + update(1, name, mId, mMode); } else { keep(name, dce, f); } @@ -1131,7 +1129,7 @@ public class DirCacheCheckout { // TODO check that we don't overwrite some unsaved // file content - update(name, mId, mMode); + update(1, name, mId, mMode); } else if (dce != null && (f != null && f.isModified(dce, true, this.walk.getObjectReader()))) { @@ -1150,7 +1148,7 @@ public class DirCacheCheckout { // -> Standard case when switching between branches: // Nothing new in index but something different in // Merge. Update index and file - update(name, mId, mMode); + update(1, name, mId, mMode); } } else { // Head differs from index or merge is same as index @@ -1237,12 +1235,17 @@ public class DirCacheCheckout { removed.add(path); } - private void update(String path, ObjectId mId, FileMode mode) - throws IOException { + private void update(CanonicalTreeParser tree) throws IOException { + update(0, tree.getEntryPathString(), tree.getEntryObjectId(), + tree.getEntryFileMode()); + } + + private void update(int index, String path, ObjectId mId, + FileMode mode) throws IOException { if (!FileMode.TREE.equals(mode)) { updated.put(path, new CheckoutMetadata( - walk.getEolStreamType(CHECKOUT_OP), - walk.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE))); + walk.getCheckoutEolStreamType(index), + walk.getSmudgeCommand(index))); DirCacheEntry entry = new DirCacheEntry(path, DirCacheEntry.STAGE_0); entry.setObjectId(mId); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java new file mode 100644 index 0000000000..e6626aece3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2021, 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 + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.gitrepo; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.jgit.lib.Constants.R_TAGS; + +import java.io.IOException; +import java.net.URI; +import java.text.MessageFormat; +import java.util.List; + +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteFile; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteUnavailableException; +import org.eclipse.jgit.gitrepo.RepoProject.CopyFile; +import org.eclipse.jgit.gitrepo.RepoProject.LinkFile; +import org.eclipse.jgit.gitrepo.internal.RepoText; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.FileUtils; + +/** + * Writes .gitmodules and gitlinks of parsed manifest projects into a bare + * repository. + * + * To write on a regular repository, see {@link RegularSuperprojectWriter}. + */ +class BareSuperprojectWriter { + private static final int LOCK_FAILURE_MAX_RETRIES = 5; + + // Retry exponentially with delays in this range + private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50; + + private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000; + + private final Repository repo; + + private final URI targetUri; + + private final String targetBranch; + + private final RemoteReader callback; + + private final BareWriterConfig config; + + private final PersonIdent author; + + private List<ExtraContent> extraContents; + + static class BareWriterConfig { + boolean ignoreRemoteFailures = false; + + boolean recordRemoteBranch = true; + + boolean recordSubmoduleLabels = true; + + boolean recordShallowSubmodules = true; + + static BareWriterConfig getDefault() { + return new BareWriterConfig(); + } + + private BareWriterConfig() { + } + } + + static class ExtraContent { + final String path; + + final String content; + + ExtraContent(String path, String content) { + this.path = path; + this.content = content; + } + } + + BareSuperprojectWriter(Repository repo, URI targetUri, + String targetBranch, + PersonIdent author, RemoteReader callback, + BareWriterConfig config, + List<ExtraContent> extraContents) { + assert (repo.isBare()); + this.repo = repo; + this.targetUri = targetUri; + this.targetBranch = targetBranch; + this.author = author; + this.callback = callback; + this.config = config; + this.extraContents = extraContents; + } + + RevCommit write(List<RepoProject> repoProjects) + throws GitAPIException { + DirCache index = DirCache.newInCore(); + ObjectInserter inserter = repo.newObjectInserter(); + + try (RevWalk rw = new RevWalk(repo)) { + prepareIndex(repoProjects, index, inserter); + ObjectId treeId = index.writeTree(inserter); + long prevDelay = 0; + for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) { + try { + return commitTreeOnCurrentTip(inserter, rw, treeId); + } catch (ConcurrentRefUpdateException e) { + prevDelay = FileUtils.delay(prevDelay, + LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS, + LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS); + Thread.sleep(prevDelay); + repo.getRefDatabase().refresh(); + } + } + // In the last try, just propagate the exceptions + return commitTreeOnCurrentTip(inserter, rw, treeId); + } catch (IOException | InterruptedException e) { + throw new ManifestErrorException(e); + } + } + + private void prepareIndex(List<RepoProject> projects, DirCache index, + ObjectInserter inserter) throws IOException, GitAPIException { + Config cfg = new Config(); + StringBuilder attributes = new StringBuilder(); + DirCacheBuilder builder = index.builder(); + for (RepoProject proj : projects) { + String name = proj.getName(); + String path = proj.getPath(); + String url = proj.getUrl(); + ObjectId objectId; + if (ObjectId.isId(proj.getRevision())) { + objectId = ObjectId.fromString(proj.getRevision()); + } else { + objectId = callback.sha1(url, proj.getRevision()); + if (objectId == null && !config.ignoreRemoteFailures) { + throw new RemoteUnavailableException(url); + } + if (config.recordRemoteBranch) { + // "branch" field is only for non-tag references. + // Keep tags in "ref" field as hint for other tools. + String field = proj.getRevision().startsWith(R_TAGS) ? "ref" //$NON-NLS-1$ + : "branch"; //$NON-NLS-1$ + cfg.setString("submodule", name, field, //$NON-NLS-1$ + proj.getRevision()); + } + + if (config.recordShallowSubmodules + && proj.getRecommendShallow() != null) { + // The shallow recommendation is losing information. + // As the repo manifests stores the recommended + // depth in the 'clone-depth' field, while + // git core only uses a binary 'shallow = true/false' + // hint, we'll map any depth to 'shallow = true' + cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$ + true); + } + } + if (config.recordSubmoduleLabels) { + StringBuilder rec = new StringBuilder(); + rec.append("/"); //$NON-NLS-1$ + rec.append(path); + for (String group : proj.getGroups()) { + rec.append(" "); //$NON-NLS-1$ + rec.append(group); + } + rec.append("\n"); //$NON-NLS-1$ + attributes.append(rec.toString()); + } + + URI submodUrl = URI.create(url); + if (targetUri != null) { + submodUrl = RepoCommand.relativize(targetUri, submodUrl); + } + cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$ + cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$ + submodUrl.toString()); + + // create gitlink + if (objectId != null) { + DirCacheEntry dcEntry = new DirCacheEntry(path); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.GITLINK); + builder.add(dcEntry); + + for (CopyFile copyfile : proj.getCopyFiles()) { + RemoteFile rf = callback.readFileWithMode(url, + proj.getRevision(), copyfile.src); + objectId = inserter.insert(Constants.OBJ_BLOB, + rf.getContents()); + dcEntry = new DirCacheEntry(copyfile.dest); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(rf.getFileMode()); + builder.add(dcEntry); + } + for (LinkFile linkfile : proj.getLinkFiles()) { + String link; + if (linkfile.dest.contains("/")) { //$NON-NLS-1$ + link = FileUtils.relativizeGitPath( + linkfile.dest.substring(0, + linkfile.dest.lastIndexOf('/')), + proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$ + } else { + link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$ + } + + objectId = inserter.insert(Constants.OBJ_BLOB, + link.getBytes(UTF_8)); + dcEntry = new DirCacheEntry(linkfile.dest); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.SYMLINK); + builder.add(dcEntry); + } + } + } + String content = cfg.toText(); + + // create a new DirCacheEntry for .gitmodules file. + DirCacheEntry dcEntry = new DirCacheEntry( + Constants.DOT_GIT_MODULES); + ObjectId objectId = inserter.insert(Constants.OBJ_BLOB, + content.getBytes(UTF_8)); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.REGULAR_FILE); + builder.add(dcEntry); + + if (config.recordSubmoduleLabels) { + // create a new DirCacheEntry for .gitattributes file. + DirCacheEntry dcEntryAttr = new DirCacheEntry( + Constants.DOT_GIT_ATTRIBUTES); + ObjectId attrId = inserter.insert(Constants.OBJ_BLOB, + attributes.toString().getBytes(UTF_8)); + dcEntryAttr.setObjectId(attrId); + dcEntryAttr.setFileMode(FileMode.REGULAR_FILE); + builder.add(dcEntryAttr); + } + + for (ExtraContent ec : extraContents) { + DirCacheEntry extraDcEntry = new DirCacheEntry(ec.path); + + ObjectId oid = inserter.insert(Constants.OBJ_BLOB, + ec.content.getBytes(UTF_8)); + extraDcEntry.setObjectId(oid); + extraDcEntry.setFileMode(FileMode.REGULAR_FILE); + builder.add(extraDcEntry); + } + + builder.finish(); + } + + private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter, + RevWalk rw, ObjectId treeId) + throws IOException, ConcurrentRefUpdateException { + ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$ + if (headId != null + && rw.parseCommit(headId).getTree().getId().equals(treeId)) { + // No change. Do nothing. + return rw.parseCommit(headId); + } + + CommitBuilder commit = new CommitBuilder(); + commit.setTreeId(treeId); + if (headId != null) { + commit.setParentIds(headId); + } + commit.setAuthor(author); + commit.setCommitter(author); + commit.setMessage(RepoText.get().repoCommitMessage); + + ObjectId commitId = inserter.insert(commit); + inserter.flush(); + + RefUpdate ru = repo.updateRef(targetBranch); + ru.setNewObjectId(commitId); + ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId()); + Result rc = ru.update(rw); + switch (rc) { + case NEW: + case FORCED: + case FAST_FORWARD: + // Successful. Do nothing. + break; + case REJECTED: + case LOCK_FAILURE: + throw new ConcurrentRefUpdateException(MessageFormat.format( + JGitText.get().cannotLock, targetBranch), ru.getRef(), rc); + default: + throw new JGitInternalException( + MessageFormat.format(JGitText.get().updatingRefFailed, + targetBranch, commitId.name(), rc)); + } + + return rw.parseCommit(commitId); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java new file mode 100644 index 0000000000..afab9943a7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021, 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 + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.gitrepo; + +import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME; +import static org.eclipse.jgit.lib.Constants.R_REMOTES; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.SubmoduleAddCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException; +import org.eclipse.jgit.gitrepo.RepoProject.CopyFile; +import org.eclipse.jgit.gitrepo.RepoProject.LinkFile; +import org.eclipse.jgit.gitrepo.internal.RepoText; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +/** + * Writes .gitmodules and gitlinks of parsed manifest projects into a regular + * repository (using git submodule commands) + * + * To write on a bare repository, use {@link BareSuperprojectWriter} + */ +class RegularSuperprojectWriter { + + private Repository repo; + + private ProgressMonitor monitor; + + RegularSuperprojectWriter(Repository repo, ProgressMonitor monitor) { + this.repo = repo; + this.monitor = monitor; + } + + RevCommit write(List<RepoProject> repoProjects) + throws GitAPIException { + try (Git git = new Git(repo)) { + for (RepoProject proj : repoProjects) { + addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(), + proj.getRevision(), proj.getCopyFiles(), + proj.getLinkFiles(), git); + } + return git.commit().setMessage(RepoText.get().repoCommitMessage) + .call(); + } catch (IOException e) { + throw new ManifestErrorException(e); + } + } + + 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); + if (!linkfiles.isEmpty()) { + throw new UnsupportedOperationException( + JGitText.get().nonBareLinkFilesNotSupported); + } + + SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path) + .setURI(url); + if (monitor != null) { + add.setProgressMonitor(monitor); + } + + Repository subRepo = add.call(); + if (revision != null) { + try (Git sub = new Git(subRepo)) { + sub.checkout().setName(findRef(revision, subRepo)).call(); + } + subRepo.close(); + git.add().addFilepattern(path).call(); + } + for (CopyFile copyfile : copyfiles) { + copyfile.copy(); + git.add().addFilepattern(copyfile.dest).call(); + } + } + + private static String findRef(String ref, Repository repo) + throws IOException { + if (!ObjectId.isId(ref)) { + Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$ + if (r != null) { + return r.getName(); + } + } + return ref; + } +} 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 e0a822479f..6e943e5d36 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java @@ -9,11 +9,6 @@ */ package org.eclipse.jgit.gitrepo; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME; -import static org.eclipse.jgit.lib.Constants.R_REMOTES; -import static org.eclipse.jgit.lib.Constants.R_TAGS; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -31,34 +26,21 @@ import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.GitCommand; -import org.eclipse.jgit.api.SubmoduleAddCommand; -import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRefNameException; -import org.eclipse.jgit.api.errors.JGitInternalException; -import org.eclipse.jgit.dircache.DirCache; -import org.eclipse.jgit.dircache.DirCacheBuilder; -import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.gitrepo.BareSuperprojectWriter.ExtraContent; import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader; -import org.eclipse.jgit.gitrepo.RepoProject.CopyFile; -import org.eclipse.jgit.gitrepo.RepoProject.LinkFile; import org.eclipse.jgit.gitrepo.internal.RepoText; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.CommitBuilder; -import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectInserter; 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.RefUpdate; -import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.FileUtils; @@ -80,12 +62,7 @@ import org.eclipse.jgit.util.FileUtils; * @since 3.4 */ public class RepoCommand extends GitCommand<RevCommit> { - private static final int LOCK_FAILURE_MAX_RETRIES = 5; - - // Retry exponentially with delays in this range - private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50; - private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000; private String manifestPath; private String baseUri; @@ -93,17 +70,18 @@ public class RepoCommand extends GitCommand<RevCommit> { private String groupsParam; private String branch; private String targetBranch = Constants.HEAD; - private boolean recordRemoteBranch = true; - private boolean recordSubmoduleLabels = true; - private boolean recordShallowSubmodules = true; private PersonIdent author; private RemoteReader callback; private InputStream inputStream; private IncludedFileReader includedReader; - private boolean ignoreRemoteFailures = false; + + private BareSuperprojectWriter.BareWriterConfig bareWriterConfig = BareSuperprojectWriter.BareWriterConfig + .getDefault(); private ProgressMonitor monitor; + private final List<ExtraContent> extraContents = new ArrayList<>(); + /** * A callback to get ref sha1 of a repository from its uri. * @@ -269,14 +247,14 @@ public class RepoCommand extends GitCommand<RevCommit> { } @SuppressWarnings("serial") - private static class ManifestErrorException extends GitAPIException { + static class ManifestErrorException extends GitAPIException { ManifestErrorException(Throwable cause) { super(RepoText.get().invalidManifest, cause); } } @SuppressWarnings("serial") - private static class RemoteUnavailableException extends GitAPIException { + static class RemoteUnavailableException extends GitAPIException { RemoteUnavailableException(String uri) { super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri)); } @@ -421,7 +399,7 @@ public class RepoCommand extends GitCommand<RevCommit> { * @since 4.2 */ public RepoCommand setRecordRemoteBranch(boolean enable) { - this.recordRemoteBranch = enable; + this.bareWriterConfig.recordRemoteBranch = enable; return this; } @@ -436,7 +414,7 @@ public class RepoCommand extends GitCommand<RevCommit> { * @since 4.4 */ public RepoCommand setRecordSubmoduleLabels(boolean enable) { - this.recordSubmoduleLabels = enable; + this.bareWriterConfig.recordSubmoduleLabels = enable; return this; } @@ -451,7 +429,7 @@ public class RepoCommand extends GitCommand<RevCommit> { * @since 4.4 */ public RepoCommand setRecommendShallow(boolean enable) { - this.recordShallowSubmodules = enable; + this.bareWriterConfig.recordShallowSubmodules = enable; return this; } @@ -485,7 +463,7 @@ public class RepoCommand extends GitCommand<RevCommit> { * @since 4.3 */ public RepoCommand setIgnoreRemoteFailures(boolean ignore) { - this.ignoreRemoteFailures = ignore; + this.bareWriterConfig.ignoreRemoteFailures = ignore; return this; } @@ -534,6 +512,22 @@ public class RepoCommand extends GitCommand<RevCommit> { return this; } + /** + * Create a file with the given content in the destination repository + * + * @param path + * where to create the file in the destination repository + * @param contents + * content for the create file + * @return this command + * + * @since 6.1 + */ + public RepoCommand addToDestination(String path, String contents) { + this.extraContents.add(new ExtraContent(path, contents)); + return this; + } + /** {@inheritDoc} */ @Override public RevCommit call() throws GitAPIException { @@ -570,240 +564,18 @@ public class RepoCommand extends GitCommand<RevCommit> { } if (repo.isBare()) { - if (author == null) - author = new PersonIdent(repo); - if (callback == null) - callback = new DefaultRemoteReader(); List<RepoProject> renamedProjects = renameProjects(filteredProjects); - - DirCache index = DirCache.newInCore(); - ObjectInserter inserter = repo.newObjectInserter(); - - try (RevWalk rw = new RevWalk(repo)) { - prepareIndex(renamedProjects, index, inserter); - ObjectId treeId = index.writeTree(inserter); - long prevDelay = 0; - for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) { - try { - return commitTreeOnCurrentTip( - inserter, rw, treeId); - } catch (ConcurrentRefUpdateException e) { - prevDelay = FileUtils.delay(prevDelay, - LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS, - LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS); - Thread.sleep(prevDelay); - repo.getRefDatabase().refresh(); - } - } - // In the last try, just propagate the exceptions - return commitTreeOnCurrentTip(inserter, rw, treeId); - } catch (IOException | InterruptedException e) { - throw new ManifestErrorException(e); - } - } - try (Git git = new Git(repo)) { - for (RepoProject proj : filteredProjects) { - addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(), - proj.getRevision(), proj.getCopyFiles(), - proj.getLinkFiles(), git); - } - return git.commit().setMessage(RepoText.get().repoCommitMessage) - .call(); - } catch (IOException e) { - throw new ManifestErrorException(e); - } - } - - private void prepareIndex(List<RepoProject> projects, DirCache index, - ObjectInserter inserter) throws IOException, GitAPIException { - Config cfg = new Config(); - StringBuilder attributes = new StringBuilder(); - DirCacheBuilder builder = index.builder(); - for (RepoProject proj : projects) { - String name = proj.getName(); - String path = proj.getPath(); - String url = proj.getUrl(); - ObjectId objectId; - if (ObjectId.isId(proj.getRevision())) { - objectId = ObjectId.fromString(proj.getRevision()); - } else { - objectId = callback.sha1(url, proj.getRevision()); - if (objectId == null && !ignoreRemoteFailures) { - throw new RemoteUnavailableException(url); - } - if (recordRemoteBranch) { - // "branch" field is only for non-tag references. - // Keep tags in "ref" field as hint for other tools. - String field = proj.getRevision().startsWith(R_TAGS) ? "ref" //$NON-NLS-1$ - : "branch"; //$NON-NLS-1$ - cfg.setString("submodule", name, field, //$NON-NLS-1$ - proj.getRevision()); - } - - if (recordShallowSubmodules - && proj.getRecommendShallow() != null) { - // The shallow recommendation is losing information. - // As the repo manifests stores the recommended - // depth in the 'clone-depth' field, while - // git core only uses a binary 'shallow = true/false' - // hint, we'll map any depth to 'shallow = true' - cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$ - true); - } - } - if (recordSubmoduleLabels) { - StringBuilder rec = new StringBuilder(); - rec.append("/"); //$NON-NLS-1$ - rec.append(path); - for (String group : proj.getGroups()) { - rec.append(" "); //$NON-NLS-1$ - rec.append(group); - } - rec.append("\n"); //$NON-NLS-1$ - attributes.append(rec.toString()); - } - - URI submodUrl = URI.create(url); - if (targetUri != null) { - submodUrl = relativize(targetUri, submodUrl); - } - cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$ - cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$ - submodUrl.toString()); - - // create gitlink - if (objectId != null) { - DirCacheEntry dcEntry = new DirCacheEntry(path); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.GITLINK); - builder.add(dcEntry); - - for (CopyFile copyfile : proj.getCopyFiles()) { - RemoteFile rf = callback.readFileWithMode(url, - proj.getRevision(), copyfile.src); - objectId = inserter.insert(Constants.OBJ_BLOB, - rf.getContents()); - dcEntry = new DirCacheEntry(copyfile.dest); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(rf.getFileMode()); - builder.add(dcEntry); - } - for (LinkFile linkfile : proj.getLinkFiles()) { - String link; - if (linkfile.dest.contains("/")) { //$NON-NLS-1$ - link = FileUtils.relativizeGitPath( - linkfile.dest.substring(0, - linkfile.dest.lastIndexOf('/')), - proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$ - } else { - link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$ - } - - objectId = inserter.insert(Constants.OBJ_BLOB, - link.getBytes(UTF_8)); - dcEntry = new DirCacheEntry(linkfile.dest); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.SYMLINK); - builder.add(dcEntry); - } - } - } - String content = cfg.toText(); - - // create a new DirCacheEntry for .gitmodules file. - DirCacheEntry dcEntry = new DirCacheEntry( - Constants.DOT_GIT_MODULES); - ObjectId objectId = inserter.insert(Constants.OBJ_BLOB, - content.getBytes(UTF_8)); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.REGULAR_FILE); - builder.add(dcEntry); - - if (recordSubmoduleLabels) { - // create a new DirCacheEntry for .gitattributes file. - DirCacheEntry dcEntryAttr = new DirCacheEntry( - Constants.DOT_GIT_ATTRIBUTES); - ObjectId attrId = inserter.insert(Constants.OBJ_BLOB, - attributes.toString().getBytes(UTF_8)); - dcEntryAttr.setObjectId(attrId); - dcEntryAttr.setFileMode(FileMode.REGULAR_FILE); - builder.add(dcEntryAttr); - } - - builder.finish(); - } - - private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter, - RevWalk rw, ObjectId treeId) - throws IOException, ConcurrentRefUpdateException { - ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$ - if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) { - // No change. Do nothing. - return rw.parseCommit(headId); + BareSuperprojectWriter writer = new BareSuperprojectWriter(repo, targetUri, + targetBranch, + author == null ? new PersonIdent(repo) : author, + callback == null ? new DefaultRemoteReader() : callback, + bareWriterConfig, extraContents); + return writer.write(renamedProjects); } - CommitBuilder commit = new CommitBuilder(); - commit.setTreeId(treeId); - if (headId != null) - commit.setParentIds(headId); - commit.setAuthor(author); - commit.setCommitter(author); - commit.setMessage(RepoText.get().repoCommitMessage); - - ObjectId commitId = inserter.insert(commit); - inserter.flush(); - - RefUpdate ru = repo.updateRef(targetBranch); - ru.setNewObjectId(commitId); - ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId()); - Result rc = ru.update(rw); - switch (rc) { - case NEW: - case FORCED: - case FAST_FORWARD: - // Successful. Do nothing. - break; - case REJECTED: - case LOCK_FAILURE: - throw new ConcurrentRefUpdateException(MessageFormat - .format(JGitText.get().cannotLock, targetBranch), - ru.getRef(), rc); - default: - throw new JGitInternalException(MessageFormat.format( - JGitText.get().updatingRefFailed, - targetBranch, commitId.name(), rc)); - } - return rw.parseCommit(commitId); - } - - 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); - if (!linkfiles.isEmpty()) { - throw new UnsupportedOperationException( - JGitText.get().nonBareLinkFilesNotSupported); - } - - SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path) - .setURI(url); - if (monitor != null) - add.setProgressMonitor(monitor); - - Repository subRepo = add.call(); - if (revision != null) { - try (Git sub = new Git(subRepo)) { - sub.checkout().setName(findRef(revision, subRepo)).call(); - } - subRepo.close(); - git.add().addFilepattern(path).call(); - } - for (CopyFile copyfile : copyfiles) { - copyfile.copy(); - git.add().addFilepattern(copyfile.dest).call(); - } + RegularSuperprojectWriter writer = new RegularSuperprojectWriter(repo, monitor); + return writer.write(filteredProjects); } /** @@ -910,13 +682,4 @@ public class RepoCommand extends GitCommand<RevCommit> { return URI.create(j.toString()); } - private static String findRef(String ref, Repository repo) - throws IOException { - if (!ObjectId.isId(ref)) { - Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$ - if (r != null) - return r.getName(); - } - return ref; - } } 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 61a2e87671..2adde0a5d0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -387,6 +387,8 @@ public class JGitText extends TranslationBundle { /***/ public String inMemoryBufferLimitExceeded; /***/ public String inputDidntMatchLength; /***/ public String inputStreamMustSupportMark; + /***/ public String integerValueNotInRange; + /***/ public String integerValueNotInRangeSubSection; /***/ public String integerValueOutOfRange; /***/ public String internalRevisionError; /***/ public String internalServerError; @@ -398,6 +400,7 @@ public class JGitText extends TranslationBundle { /***/ public String invalidBooleanValue; /***/ public String invalidChannel; /***/ public String invalidCommitParentNumber; + /***/ public String invalidCoreAbbrev; /***/ public String invalidDepth; /***/ public String invalidEncoding; /***/ public String invalidEncryption; @@ -599,6 +602,11 @@ public class JGitText extends TranslationBundle { /***/ public String pushCertificateInvalidFieldValue; /***/ public String pushCertificateInvalidHeader; /***/ public String pushCertificateInvalidSignature; + /***/ public String pushDefaultNothing; + /***/ public String pushDefaultNoUpstream; + /***/ public String pushDefaultSimple; + /***/ public String pushDefaultTriangularUpstream; + /***/ public String pushDefaultUnknown; /***/ public String pushIsNotSupportedForBundleTransport; /***/ public String pushNotPermitted; /***/ public String pushOptionsNotSupported; 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 new file mode 100644 index 0000000000..509515c37a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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.diffmergetool; + +/** + * Pre-defined command line diff tools. + * + * Adds same diff tools as also pre-defined in C-Git + * <p> + * see "git-core\mergetools\" + * </p> + * <p> + * see links to command line parameter description for the tools + * </p> + * + * <pre> + * araxis + * bc + * bc3 + * codecompare + * deltawalker + * diffmerge + * diffuse + * ecmerge + * emerge + * examdiff + * guiffy + * gvimdiff + * gvimdiff2 + * gvimdiff3 + * kdiff3 + * kompare + * meld + * opendiff + * p4merge + * tkdiff + * vimdiff + * vimdiff2 + * vimdiff3 + * winmerge + * xxdiff + * </pre> + * + */ +@SuppressWarnings("nls") +public enum CommandLineDiffTool { + /** + * See: <a href= + * "https://www.araxis.com/merge/documentation-windows/command-line.en">https://www.araxis.com/merge/documentation-windows/command-line.en</a> + */ + araxis("compare", "-wait -2 \"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a> + */ + bc("bcomp", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a> + */ + bc3("bcompare", bc), + /** + * See: <a href= + * "https://www.devart.com/codecompare/docs/index.html?comparing_via_command_line.htm">https://www.devart.com/codecompare/docs/index.html?comparing_via_command_line.htm</a> + */ + codecompare("CodeCompare", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "https://www.deltawalker.com/integrate/command-line">https://www.deltawalker.com/integrate/command-line</a> + */ + deltawalker("DeltaWalker", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html">https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html</a> + */ + diffmerge("diffmerge", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "http://diffuse.sourceforge.net/manual.html#introduction-usage">http://diffuse.sourceforge.net/manual.html#introduction-usage</a> + */ + diffuse("diffuse", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp">http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp</a> + */ + ecmerge("ecmerge", "--default --mode=diff2 \"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html">https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html</a> + */ + emerge("emacs", "-f emerge-files-command \"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options">https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options</a> + */ + examdiff("ExamDiff", "\"$LOCAL\" \"$REMOTE\" -nh"), + /** + * See: <a href= + * "https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html">https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html</a> + */ + guiffy("guiffy", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + gvimdiff("gviewdiff", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + gvimdiff2(gvimdiff), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + gvimdiff3(gvimdiff), + /** + * See: <a href= + * "http://kdiff3.sourceforge.net/doc/documentation.html">http://kdiff3.sourceforge.net/doc/documentation.html</a> + */ + kdiff3("kdiff3", + "--L1 \"$MERGED (A)\" --L2 \"$MERGED (B)\" \"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "https://docs.kde.org/trunk5/en/kdesdk/kompare/commandline-options.html">https://docs.kde.org/trunk5/en/kdesdk/kompare/commandline-options.html</a> + */ + kompare("kompare", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "ttp://meldmerge.org/help/file-mode.html">http://meldmerge.org/help/file-mode.html</a> + */ + meld("meld", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "http://www.manpagez.com/man/1/opendiff/">http://www.manpagez.com/man/1/opendiff/</a> + * <p> + * Hint: check the ' | cat' for the call + * </p> + */ + opendiff("opendiff", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html">https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html</a> + */ + p4merge("p4merge", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "http://linux.math.tifr.res.in/manuals/man/tkdiff.html">http://linux.math.tifr.res.in/manuals/man/tkdiff.html</a> + */ + tkdiff("tkdiff", "\"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + vimdiff("viewdiff", gvimdiff), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + vimdiff2(vimdiff), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + vimdiff3(vimdiff), + /** + * See: <a href= + * "http://manual.winmerge.org/Command_line.html">http://manual.winmerge.org/Command_line.html</a> + * <p> + * Hint: check how 'mergetool_find_win32_cmd "WinMergeU.exe" "WinMerge"' + * works + * </p> + */ + winmerge("WinMergeU", "-u -e \"$LOCAL\" \"$REMOTE\""), + /** + * See: <a href= + * "http://furius.ca/xxdiff/doc/xxdiff-doc.html">http://furius.ca/xxdiff/doc/xxdiff-doc.html</a> + */ + xxdiff("xxdiff", + "-R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' \"$LOCAL\" \"$REMOTE\""); + + CommandLineDiffTool(String path, String parameters) { + this.path = path; + this.parameters = parameters; + } + + CommandLineDiffTool(CommandLineDiffTool from) { + this(from.getPath(), from.getParameters()); + } + + CommandLineDiffTool(String path, CommandLineDiffTool from) { + this(path, from.getParameters()); + } + + private final String path; + + private final String parameters; + + /** + * @return path + */ + public String getPath() { + return path; + } + + /** + * @return parameters as one string + */ + public String getParameters() { + return parameters; + } + +} 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 new file mode 100644 index 0000000000..551f634f2d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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.diffmergetool; + +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Config.SectionParser; +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * Keeps track of difftool related configuration options. + */ +public class DiffToolConfig { + + /** Key for {@link Config#get(SectionParser)}. */ + public static final Config.SectionParser<DiffToolConfig> KEY = DiffToolConfig::new; + + private final String toolName; + + private final String guiToolName; + + private final boolean prompt; + + private final BooleanTriState trustExitCode; + + private final Map<String, ExternalDiffTool> tools; + + private DiffToolConfig(Config rc) { + toolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_TOOL); + guiToolName = rc.getString(CONFIG_DIFF_SECTION, null, + CONFIG_KEY_GUITOOL); + prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, CONFIG_KEY_PROMPT, + true); + String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, null, + CONFIG_KEY_TRUST_EXIT_CODE); + if (trustStr != null) { + trustExitCode = Boolean.parseBoolean(trustStr) + ? BooleanTriState.TRUE + : BooleanTriState.FALSE; + } else { + trustExitCode = BooleanTriState.UNSET; + } + tools = new HashMap<>(); + Set<String> subsections = rc.getSubsections(CONFIG_DIFFTOOL_SECTION); + for (String name : subsections) { + String cmd = rc.getString(CONFIG_DIFFTOOL_SECTION, name, + CONFIG_KEY_CMD); + String path = rc.getString(CONFIG_DIFFTOOL_SECTION, name, + CONFIG_KEY_PATH); + if ((cmd != null) || (path != null)) { + tools.put(name, new UserDefinedDiffTool(name, path, cmd)); + } + } + } + + /** + * @return the default diff tool name (diff.tool) + */ + public String getDefaultToolName() { + return toolName; + } + + /** + * @return the default GUI diff tool name (diff.guitool) + */ + public String getDefaultGuiToolName() { + return guiToolName; + } + + /** + * @return the diff tool "prompt" option (difftool.prompt) + */ + public boolean isPrompt() { + return prompt; + } + + /** + * @return the diff tool "trust exit code" option (difftool.trustExitCode) + */ + public boolean isTrustExitCode() { + return trustExitCode == BooleanTriState.TRUE; + } + + /** + * @return the tools map + */ + public Map<String, ExternalDiffTool> getTools() { + return tools; + } + + /** + * @return the tool names + */ + public Set<String> getToolNames() { + return tools.keySet(); + } +} 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 new file mode 100644 index 0000000000..39729a4eec --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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.diffmergetool; + +import java.util.TreeMap; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * Manages diff tools. + */ +public class DiffTools { + + private final DiffToolConfig config; + + private Map<String, ExternalDiffTool> predefinedTools; + + private Map<String, ExternalDiffTool> userDefinedTools; + + /** + * Creates the external diff-tools manager for given repository. + * + * @param repo + * the repository + */ + public DiffTools(Repository repo) { + config = repo.getConfig().get(DiffToolConfig.KEY); + setupPredefinedTools(); + setupUserDefinedTools(); + } + + /** + * Compare two versions of a file. + * + * @param newPath + * the new file path + * @param oldPath + * the old file path + * @param newId + * the new object ID + * @param oldId + * the old object ID + * @param toolName + * the selected tool name (can be null) + * @param prompt + * the prompt option + * @param gui + * the GUI option + * @param trustExitCode + * the "trust exit code" option + * @return the return code from executed tool + */ + public int compare(String newPath, String oldPath, String newId, + String oldId, String toolName, BooleanTriState prompt, + BooleanTriState gui, BooleanTriState trustExitCode) { + return 0; + } + + /** + * @return the tool names + */ + public Set<String> getToolNames() { + return config.getToolNames(); + } + + /** + * @return the user defined tools + */ + public Map<String, ExternalDiffTool> getUserDefinedTools() { + return Collections.unmodifiableMap(userDefinedTools); + } + + /** + * @return the available predefined tools + */ + public Map<String, ExternalDiffTool> getAvailableTools() { + return Collections.unmodifiableMap(predefinedTools); + } + + /** + * @return the NOT available predefined tools + */ + public Map<String, ExternalDiffTool> getNotAvailableTools() { + return Collections.unmodifiableMap(new TreeMap<>()); + } + + /** + * @param gui + * use the diff.guitool setting ? + * @return the default tool name + */ + public String getDefaultToolName(BooleanTriState gui) { + return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ + : "my_default_toolname"; //$NON-NLS-1$ + } + + /** + * @return is interactive (config prompt enabled) ? + */ + public boolean isInteractive() { + return false; + } + + private void setupPredefinedTools() { + predefinedTools = new TreeMap<>(); + for (CommandLineDiffTool tool : CommandLineDiffTool.values()) { + predefinedTools.put(tool.name(), new PreDefinedDiffTool(tool)); + } + } + + private void setupUserDefinedTools() { + userDefinedTools = new TreeMap<>(); + Map<String, ExternalDiffTool> userTools = config.getTools(); + for (String name : userTools.keySet()) { + ExternalDiffTool userTool = userTools.get(name); + // if difftool.<name>.cmd is defined we have user defined tool + if (userTool.getCommand() != null) { + userDefinedTools.put(name, userTool); + } else if (userTool.getPath() != null) { + // if difftool.<name>.path is defined we just overload the path + // of predefined tool + PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefinedTools + .get(name); + if (predefTool != null) { + predefTool.setPath(userTool.getPath()); + } + } + } + } + +} 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 new file mode 100644 index 0000000000..f2d7e828cb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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.diffmergetool; + +/** + * The external tool interface. + */ +public interface ExternalDiffTool { + + /** + * @return the tool name + */ + String getName(); + + /** + * @return the tool path + */ + String getPath(); + + /** + * @return the tool command + */ + String getCommand(); + +} 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 new file mode 100644 index 0000000000..1c69fb4911 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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.diffmergetool; + +/** + * The pre-defined diff tool. + */ +public class PreDefinedDiffTool extends UserDefinedDiffTool { + + /** + * Create a pre-defined diff tool + * + * @param name + * the name + * @param path + * the path + * @param parameters + * the tool parameters as one string that is used together with + * path as command + */ + public PreDefinedDiffTool(String name, String path, String parameters) { + super(name, path, parameters); + } + + /** + * Creates the pre-defined diff tool + * + * @param tool + * the command line diff tool + * + */ + public PreDefinedDiffTool(CommandLineDiffTool tool) { + this(tool.name(), tool.getPath(), tool.getParameters()); + } + + /** + * @param path + */ + @Override + public void setPath(String path) { + // handling of spaces in path + if (path.contains(" ")) { //$NON-NLS-1$ + // add quotes before if needed + if (!path.startsWith("\"")) { //$NON-NLS-1$ + path = "\"" + path; //$NON-NLS-1$ + } + // add quotes after if needed + if (!path.endsWith("\"")) { //$NON-NLS-1$ + path = path + "\""; //$NON-NLS-1$ + } + } + super.setPath(path); + } + + /** + * {@inheritDoc} + * + * @return the concatenated path and command of the pre-defined diff tool + */ + @Override + public String getCommand() { + return getPath() + " " + super.getCommand(); //$NON-NLS-1$ + } + +} 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 new file mode 100644 index 0000000000..012296eb35 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * 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.diffmergetool; + +/** + * The user-defined diff tool. + */ +public class UserDefinedDiffTool implements ExternalDiffTool { + + /** + * the diff tool name + */ + private final String name; + + /** + * the diff tool path + */ + private String path; + + /** + * the diff tool command + */ + private final String cmd; + + /** + * Creates the diff tool + * + * @param name + * the name + * @param path + * the path + * @param cmd + * the command + */ + public UserDefinedDiffTool(String name, String path, String cmd) { + this.name = name; + this.path = path; + this.cmd = cmd; + } + + /** + * @return the diff tool name + */ + @Override + public String getName() { + return name; + } + + /** + * The path of the diff tool. + * + * <p> + * The path to a pre-defined external diff tool can be overridden by + * specifying {@code difftool.<tool>.path} in a configuration file. + * </p> + * <p> + * For a user defined diff tool (that does not override a pre-defined diff + * tool), the path is ignored when invoking the tool. + * </p> + * + * @return the diff tool path + * + * @see <a href= + * "https://git-scm.com/docs/git-difftool">https://git-scm.com/docs/git-difftool</a> + */ + @Override + public String getPath() { + return path; + } + + /** + * The command of the diff tool. + * + * <p> + * A pre-defined external diff tool can be overridden using the tools name + * in a configuration file. The overwritten tool is then a user defined tool + * and the command of the diff tool is specified with + * {@code difftool.<tool>.cmd}. This command must work without prepending + * the value of {@link #getPath()} and can sometimes include tool + * parameters. + * </p> + * + * @return the diff tool command + * + * @see <a href= + * "https://git-scm.com/docs/git-difftool">https://git-scm.com/docs/git-difftool</a> + */ + @Override + public String getCommand() { + return cmd; + } + + /** + * Overrides the path for the given tool. Equivalent to setting + * {@code difftool.<tool>.path}. + * + * @param path + * the new diff tool path + * + * @see <a href= + * "https://git-scm.com/docs/git-difftool">https://git-scm.com/docs/git-difftool</a> + */ + public void setPath(String path) { + this.path = path; + } +} 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 54c527c03c..b30d50921a 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 @@ -12,6 +12,10 @@ package org.eclipse.jgit.internal.storage.dfs; import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; @@ -166,6 +170,12 @@ public final class DfsBlockCache { /** 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); @@ -213,6 +223,7 @@ public final class DfsBlockCache { cacheHotLimits[i] = DfsBlockCacheConfig.DEFAULT_CACHE_HOT_MAX; } } + indexEventConsumer = cfg.getIndexEventConsumer(); } boolean shouldCopyThroughCache(long length) { @@ -461,6 +472,7 @@ public final class DfsBlockCache { live -= dead.size; getStat(liveBytes, dead.key).addAndGet(-dead.size); getStat(statEvict, dead.key).incrementAndGet(); + reportIndexEvicted(dead); } while (maxBytes < live); clockHand = prev; } @@ -515,11 +527,13 @@ public final class DfsBlockCache { <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; } @@ -532,6 +546,8 @@ public final class DfsBlockCache { ref = scanRef(e2, key, position); if (ref != null) { getStat(statHit, key).incrementAndGet(); + reportIndexRequested(ref, true /* cacheHit */, + start); return ref; } } @@ -556,6 +572,7 @@ public final class DfsBlockCache { } finally { regionLock.unlock(); } + reportIndexRequested(ref, false /* cacheHit */, start); return ref; } @@ -682,8 +699,9 @@ public final class DfsBlockCache { } private static HashEntry clean(HashEntry top) { - while (top != null && top.ref.next == null) + while (top != null && top.ref.next == null) { top = top.next; + } if (top == null) { return null; } @@ -691,6 +709,44 @@ public final class DfsBlockCache { return n == top.next ? top : new HashEntry(n, top.ref); } + private void reportIndexRequested(Ref<?> ref, boolean cacheHit, + long start) { + if (indexEventConsumer == null + || !isIndexOrBitmapExtPos(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() + || !isIndexOrBitmapExtPos(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 isIndexOrBitmapExtPos(int packExtPos) { + return packExtPos == PackExt.INDEX.getPosition() + || packExtPos == PackExt.BITMAP_INDEX.getPosition(); + } + private static final class HashEntry { /** Next entry in the hash table's chain list. */ final HashEntry next; @@ -712,6 +768,7 @@ public final class DfsBlockCache { Ref next; private volatile int hotCount; + private AtomicInteger totalHitCount = new AtomicInteger(); Ref(DfsStreamKey key, long position, long size, T v) { this.key = key; @@ -736,6 +793,7 @@ public final class DfsBlockCache { int cap = DfsBlockCache .getInstance().cacheHotLimits[key.packExtPos]; hotCount = Math.min(cap, hotCount + 1); + totalHitCount.incrementAndGet(); } void markColder() { @@ -747,6 +805,34 @@ public final class DfsBlockCache { } } + 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); + } + } + @FunctionalInterface interface RefLoader<T> { Ref<T> load() throws IOException; @@ -763,4 +849,4 @@ public final class DfsBlockCache { */ ReadableChannel get() throws IOException; } -} +}
\ No newline at end of file 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 2716f79a1a..69a37058bf 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 @@ -18,6 +18,7 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CONCURRENCY_LEVEL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO; import java.text.MessageFormat; +import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -46,9 +47,10 @@ public class DfsBlockCacheConfig { private int concurrencyLevel; private Consumer<Long> refLock; - private Map<PackExt, Integer> cacheHotMap; + private IndexEventConsumer indexEventConsumer; + /** * Create a default configuration. */ @@ -216,6 +218,28 @@ public class DfsBlockCacheConfig { } /** + * Get the consumer of cache index events. + * + * @return consumer of cache index events. + */ + public IndexEventConsumer getIndexEventConsumer() { + return indexEventConsumer; + } + + /** + * Set the consumer of cache index events. + * + * @param indexEventConsumer + * consumer of cache index events. + * @return {@code this} + */ + public DfsBlockCacheConfig setIndexEventConsumer( + IndexEventConsumer indexEventConsumer) { + this.indexEventConsumer = indexEventConsumer; + return this; + } + + /** * Update properties by setting fields from the configuration. * <p> * If a property is not defined in the configuration, then it is left @@ -272,4 +296,52 @@ public class DfsBlockCacheConfig { } return this; } -} + + /** Consumer of DfsBlockCache loading and eviction events for indexes. */ + public interface IndexEventConsumer { + /** + * Accept an event of an index requested. It could be loaded from either + * cache or storage. + * + * @param packExtPos + * position in {@code PackExt} enum + * @param cacheHit + * true if an index was already in cache. Otherwise, the + * index was loaded from storage into the cache in the + * current request, + * @param loadMicros + * time to load an index from cache or storage in + * microseconds + * @param bytes + * number of bytes loaded + * @param lastEvictionDuration + * time since last eviction, 0 if was not evicted yet + */ + void acceptRequestedEvent(int packExtPos, boolean cacheHit, + long loadMicros, long bytes, Duration lastEvictionDuration); + + /** + * Accept an event of an index evicted from cache. + * + * @param packExtPos + * position in {@code PackExt} enum + * @param bytes + * number of bytes evicted + * @param totalCacheHitCount + * number of times an index was accessed while in cache + * @param lastEvictionDuration + * time since last eviction, 0 if was not evicted yet + */ + default void acceptEvictedEvent(int packExtPos, long bytes, + int totalCacheHitCount, Duration lastEvictionDuration) { + // Off by default. + } + + /** + * @return true if reporting evicted events is enabled. + */ + default boolean shouldReportEvictedEvent() { + return false; + } + } +}
\ No newline at end of file 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 bb76df1d5d..f7a2c94d48 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 @@ -1061,7 +1061,8 @@ public final class DfsPackFile extends BlockBasedFile { } in = new BufferedInputStream(in, bs); bmidx = PackBitmapIndex.read(in, () -> idx(ctx), - () -> getReverseIdx(ctx)); + () -> getReverseIdx(ctx), + ctx.getOptions().shouldLoadRevIndexInParallel()); } finally { size = rc.position(); ctx.stats.readBitmapIdxBytes += size; 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 89de53460c..146f76167d 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 @@ -34,6 +34,8 @@ public class DfsReaderOptions { private int streamPackBufferSize; + private boolean loadRevIndexInParallel; + /** * Create a default reader configuration. */ @@ -113,6 +115,28 @@ public class DfsReaderOptions { } /** + * Check if reverse index should be loaded in parallel. + * + * @return true if reverse index is loaded in parallel for bitmap index. + */ + public boolean shouldLoadRevIndexInParallel() { + return loadRevIndexInParallel; + } + + /** + * Enable (or disable) parallel loading of reverse index. + * + * @param loadRevIndexInParallel + * whether to load reverse index in parallel. + * @return {@code this} + */ + public DfsReaderOptions setLoadRevIndexInParallel( + boolean loadRevIndexInParallel) { + this.loadRevIndexInParallel = loadRevIndexInParallel; + return this; + } + + /** * Update properties by setting fields from the configuration. * <p> * If a property is not defined in the configuration, then it is left 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 5b6894da9c..99da222395 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 @@ -165,6 +165,15 @@ public class InMemoryRepository extends DfsRepository { } }; } + + @Override + public long getApproximateObjectCount() { + long count = 0; + for (DfsPackDescription p : packs) { + count += p.getObjectCount(); + } + return count; + } } private static class MemPack extends DfsPackDescription { 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 b0612f9395..cd4f168d86 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 @@ -73,7 +73,6 @@ final class LargePackedWholeObject extends ObjectLoader { public ObjectStream openStream() throws MissingObjectException, IOException { PackInputStream packIn; // ctx is closed by PackInputStream, or explicitly in the finally block - @SuppressWarnings("resource") DfsReader ctx = db.newReader(); try { try { 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 7dedeb57ab..094fdc1559 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 @@ -263,4 +263,17 @@ class CachedObjectDirectory extends FileObjectDatabase { private AlternateHandle.Id getAlternateId() { return wrapped.getAlternateId(); } + + @Override + public long getApproximateObjectCount() { + long count = 0; + for (Pack p : getPacks()) { + try { + count += p.getObjectCount(); + } catch (IOException e) { + return -1; + } + } + return count; + } } 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 f02c8613a0..5152367d23 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 @@ -40,6 +40,7 @@ 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.util.FileUtils; +import org.eclipse.jgit.util.SystemReader; /** * A mutable stack of reftables on local filesystem storage. Not thread-safe. @@ -527,11 +528,19 @@ public class FileReftableStack implements AutoCloseable { return false; } + reload(); for (File f : deleteOnSuccess) { - Files.delete(f.toPath()); + try { + Files.delete(f.toPath()); + } catch (IOException e) { + // Ignore: this can happen on Windows in case of concurrent processes. + // leave the garbage and continue. + if (!SystemReader.getInstance().isWindows()) { + throw e; + } + } } - reload(); return true; } finally { if (tmpTable != null) { 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 b37155717c..3e92cddacd 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 @@ -672,18 +672,20 @@ public class FileRepository extends Repository { if (writeLogs) { List<ReflogEntry> logs = oldDb.getReflogReader(r.getName()) - .getReverseEntries(); + .getReverseEntries(); Collections.reverse(logs); for (ReflogEntry e : logs) { logWriter.log(r.getName(), e); } - } + } } try (RevWalk rw = new RevWalk(this)) { bru.execute(rw, NullProgressMonitor.INSTANCE); } + oldDb.close(); + List<String> failed = new ArrayList<>(); for (ReceiveCommand cmd : bru.getCommands()) { if (cmd.getResult() != ReceiveCommand.Result.OK) { 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 33621a1e9f..b9af83d24d 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 @@ -14,6 +14,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.StandardCopyOption; @@ -24,6 +25,8 @@ import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.FileObjectDatabase.InsertLooseObjectResult; 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.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -52,15 +55,22 @@ class LooseObjects { private final UnpackedObjectCache unpackedObjectCache; + private final boolean trustFolderStat; + /** * Initialize a reference to an on-disk object directory. * + * @param config + * configuration for the loose objects handler. * @param dir * the location of the <code>objects</code> directory. */ - LooseObjects(File dir) { + LooseObjects(Config config, File dir) { directory = dir; unpackedObjectCache = new UnpackedObjectCache(); + trustFolderStat = config.getBoolean( + ConfigConstants.CONFIG_CORE_SECTION, + ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true); } /** @@ -98,6 +108,19 @@ class LooseObjects { * @return {@code true} if the specified object is stored as a loose object. */ boolean has(AnyObjectId objectId) { + boolean exists = hasWithoutRefresh(objectId); + if (trustFolderStat || exists) { + return exists; + } + try (InputStream stream = Files.newInputStream(directory.toPath())) { + // refresh directory to work around NFS caching issue + } catch (IOException e) { + return false; + } + return hasWithoutRefresh(objectId); + } + + private boolean hasWithoutRefresh(AnyObjectId objectId) { return fileFor(objectId).exists(); } @@ -183,6 +206,22 @@ class LooseObjects { */ ObjectLoader getObjectLoader(WindowCursor curs, File path, AnyObjectId id) throws IOException { + try { + return getObjectLoaderWithoutRefresh(curs, path, id); + } catch (FileNotFoundException e) { + if (trustFolderStat) { + throw e; + } + try (InputStream stream = Files + .newInputStream(directory.toPath())) { + // refresh directory to work around NFS caching issues + } + return getObjectLoaderWithoutRefresh(curs, path, id); + } + } + + private ObjectLoader getObjectLoaderWithoutRefresh(WindowCursor curs, + File path, AnyObjectId id) throws IOException { try (FileInputStream in = new FileInputStream(path)) { unpackedObjectCache().add(id); return UnpackedObject.open(in, path, id, curs); @@ -203,16 +242,34 @@ class LooseObjects { } long getSize(WindowCursor curs, AnyObjectId id) throws IOException { + try { + return getSizeWithoutRefresh(curs, id); + } catch (FileNotFoundException noFile) { + try { + if (trustFolderStat) { + throw noFile; + } + try (InputStream stream = Files + .newInputStream(directory.toPath())) { + // refresh directory to work around NFS caching issue + } + return getSizeWithoutRefresh(curs, id); + } catch (FileNotFoundException e) { + if (fileFor(id).exists()) { + throw noFile; + } + unpackedObjectCache().remove(id); + return -1; + } + } + } + + private long getSizeWithoutRefresh(WindowCursor curs, AnyObjectId id) + throws IOException { File f = fileFor(id); try (FileInputStream in = new FileInputStream(f)) { unpackedObjectCache().add(id); return UnpackedObject.getSize(in, id, curs); - } catch (FileNotFoundException noFile) { - if (f.exists()) { - throw noFile; - } - unpackedObjectCache().remove(id); - return -1; } } 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 627facca02..531fd78cd0 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 @@ -120,7 +120,7 @@ public class ObjectDirectory extends FileObjectDatabase { File packDirectory = new File(objects, "pack"); //$NON-NLS-1$ File preservedDirectory = new File(packDirectory, "preserved"); //$NON-NLS-1$ alternatesFile = new File(objects, Constants.INFO_ALTERNATES); - loose = new LooseObjects(objects); + loose = new LooseObjects(config, objects); packed = new PackDirectory(config, packDirectory); preserved = new PackDirectory(config, preservedDirectory); this.fs = fs; @@ -212,6 +212,20 @@ public class ObjectDirectory extends FileObjectDatabase { return packed.getPacks(); } + /** {@inheritDoc} */ + @Override + public long getApproximateObjectCount() { + long count = 0; + for (Pack p : getPacks()) { + try { + count += p.getIndex().getObjectCount(); + } catch (IOException e) { + return -1; + } + } + return count; + } + /** * {@inheritDoc} * <p> 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 8401f0718a..8fb17fcf21 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 @@ -95,7 +95,7 @@ public abstract class PackBitmapIndex { */ public static PackBitmapIndex read(InputStream fd, PackIndex packIndex, PackReverseIndex reverseIndex) throws IOException { - return new PackBitmapIndexV1(fd, () -> packIndex, () -> reverseIndex); + return new PackBitmapIndexV1(fd, packIndex, reverseIndex); } /** @@ -114,6 +114,8 @@ public abstract class PackBitmapIndex { * @param reverseIndexSupplier * the supplier for pack reverse index for the corresponding pack * file. + * @param loadParallelRevIndex + * whether reverse index should be loaded in parallel * @return a copy of the index in-memory. * @throws java.io.IOException * the stream cannot be read. @@ -122,10 +124,11 @@ public abstract class PackBitmapIndex { */ public static PackBitmapIndex read(InputStream fd, SupplierWithIOException<PackIndex> packIndexSupplier, - SupplierWithIOException<PackReverseIndex> reverseIndexSupplier) + SupplierWithIOException<PackReverseIndex> reverseIndexSupplier, + boolean loadParallelRevIndex) throws IOException { return new PackBitmapIndexV1(fd, packIndexSupplier, - reverseIndexSupplier); + reverseIndexSupplier, loadParallelRevIndex); } /** Footer checksum applied on the bottom of the pack file. */ 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 6846e3bcad..21aba3e6a3 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 @@ -17,6 +17,12 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.internal.JGitText; @@ -40,6 +46,23 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { private static final int MAX_XOR_OFFSET = 126; + private static final ExecutorService executor = Executors + .newCachedThreadPool(new ThreadFactory() { + private final ThreadFactory baseFactory = Executors + .defaultThreadFactory(); + + private final AtomicInteger threadNumber = new AtomicInteger(0); + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = baseFactory.newThread(runnable); + thread.setName("JGit-PackBitmapIndexV1-" //$NON-NLS-1$ + + threadNumber.getAndIncrement()); + thread.setDaemon(true); + return thread; + } + }); + private final PackIndex packIndex; private final PackReverseIndex reverseIndex; private final EWAHCompressedBitmap commits; @@ -49,15 +72,28 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { private final ObjectIdOwnerMap<StoredBitmap> bitmaps; + PackBitmapIndexV1(final InputStream fd, PackIndex packIndex, + PackReverseIndex reverseIndex) throws IOException { + this(fd, () -> packIndex, () -> reverseIndex, false); + } + PackBitmapIndexV1(final InputStream fd, SupplierWithIOException<PackIndex> packIndexSupplier, - SupplierWithIOException<PackReverseIndex> reverseIndexSupplier) + SupplierWithIOException<PackReverseIndex> reverseIndexSupplier, + boolean loadParallelRevIndex) 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>()); this.bitmaps = getBitmaps(); + // Optionally start loading reverse index in parallel to loading bitmap + // from storage. + Future<PackReverseIndex> reverseIndexFuture = null; + if (loadParallelRevIndex) { + reverseIndexFuture = executor.submit(reverseIndexSupplier::get); + } + final byte[] scratch = new byte[32]; IO.readFully(fd, scratch, 0, scratch.length); @@ -164,7 +200,18 @@ class PackBitmapIndexV1 extends BasePackBitmapIndex { bitmaps.add(sb); } - this.reverseIndex = reverseIndexSupplier.get(); + PackReverseIndex computedReverseIndex; + if (loadParallelRevIndex && reverseIndexFuture != null) { + try { + computedReverseIndex = reverseIndexFuture.get(); + } catch (InterruptedException | ExecutionException e) { + // Fallback to loading reverse index through a supplier. + computedReverseIndex = reverseIndexSupplier.get(); + } + } else { + computedReverseIndex = reverseIndexSupplier.get(); + } + this.reverseIndex = computedReverseIndex; } /** {@inheritDoc} */ 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 f32909f44d..a7f28c6778 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 @@ -63,12 +63,12 @@ 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; + /** * Initialize a reference to an on-disk 'pack' directory. * @@ -78,9 +78,16 @@ 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); } /** @@ -331,16 +338,6 @@ class PackDirectory { } boolean searchPacksAgain(PackList old) { - // 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 - // lastmodified attribute of the folder and assume that no new - // pack files can be in this folder if his modification time has - // not changed. - boolean trustFolderStat = config.getBoolean( - ConfigConstants.CONFIG_CORE_SECTION, - ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true); - return ((!trustFolderStat) || old.snapshot.isModified(directory)) && old != scanPacks(old); } 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 b46ffe3670..c7322b17bb 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 @@ -30,10 +30,12 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.InterruptedIOException; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.security.DigestInputStream; import java.security.MessageDigest; @@ -60,6 +62,7 @@ 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.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.Ref; @@ -177,6 +180,10 @@ public class RefDirectory extends RefDatabase { private List<Integer> retrySleepMs = RETRY_SLEEP_MS; + private final boolean trustFolderStat; + + private final TrustPackedRefsStat trustPackedRefsStat; + RefDirectory(FileRepository db) { final FS fs = db.getFS(); parent = db; @@ -188,6 +195,13 @@ public class RefDirectory extends RefDatabase { 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); } Repository getRepository() { @@ -889,13 +903,30 @@ public class RefDirectory extends RefDatabase { } PackedRefList getPackedRefs() throws IOException { - boolean trustFolderStat = getRepository().getConfig().getBoolean( - ConfigConstants.CONFIG_CORE_SECTION, - ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true); - final PackedRefList curList = packedRefs.get(); - if (trustFolderStat && !curList.snapshot.isModified(packedRefsFile)) { - return curList; + + switch (trustPackedRefsStat) { + case NEVER: + break; + case AFTER_OPEN: + try (InputStream stream = Files + .newInputStream(packedRefsFile.toPath())) { + // open the file to refresh attributes (on some NFS clients) + } catch (FileNotFoundException | NoSuchFileException e) { + // Ignore as packed-refs may not exist + } + //$FALL-THROUGH$ + case ALWAYS: + if (!curList.snapshot.isModified(packedRefsFile)) { + return curList; + } + break; + case UNSET: + if (trustFolderStat + && !curList.snapshot.isModified(packedRefsFile)) { + return curList; + } + break; } final PackedRefList newList = readPackedRefs(); 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 new file mode 100644 index 0000000000..ca2095feec --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStream.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2022, Tencent. + * + * 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.io; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ProgressMonitor; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.security.MessageDigest; + +/** + * An OutputStream that keeps a digest and checks every N bytes for + * cancellation. + */ +public class CancellableDigestOutputStream extends OutputStream { + + /** The OutputStream checks every this value for cancellation **/ + public static final int BYTES_TO_WRITE_BEFORE_CANCEL_CHECK = 128 * 1024; + + private final ProgressMonitor writeMonitor; + + private final OutputStream out; + + private final MessageDigest md = Constants.newMessageDigest(); + + private long count; + + private long checkCancelAt; + + /** + * Initialize a CancellableDigestOutputStream. + * + * @param writeMonitor + * monitor to update on output progress and check cancel. + * @param out + * target stream to receive all contents. + */ + public CancellableDigestOutputStream(ProgressMonitor writeMonitor, + OutputStream out) { + this.writeMonitor = writeMonitor; + this.out = out; + this.checkCancelAt = BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; + } + + /** + * Get the monitor which is used to update on output progress and check + * cancel. + * + * @return the monitor + */ + public final ProgressMonitor getWriteMonitor() { + return writeMonitor; + } + + /** + * Obtain the current SHA-1 digest. + * + * @return SHA-1 digest + */ + public final byte[] getDigest() { + return md.digest(); + } + + /** + * Get total number of bytes written since stream start. + * + * @return total number of bytes written since stream start. + */ + public final long length() { + return count; + } + + /** {@inheritDoc} */ + @Override + public final void write(int b) throws IOException { + if (checkCancelAt <= count) { + if (writeMonitor.isCancelled()) { + throw new InterruptedIOException(); + } + checkCancelAt = count + BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; + } + + out.write(b); + md.update((byte) b); + count++; + } + + /** {@inheritDoc} */ + @Override + public final void write(byte[] b, int off, int len) throws IOException { + while (0 < len) { + if (checkCancelAt <= count) { + if (writeMonitor.isCancelled()) { + throw new InterruptedIOException(); + } + checkCancelAt = count + BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; + } + + int n = Math.min(len, BYTES_TO_WRITE_BEFORE_CANCEL_CHECK); + out.write(b, off, n); + md.update(b, off, n); + count += n; + + off += n; + len -= n; + } + } + + /** {@inheritDoc} */ + @Override + public void flush() throws IOException { + out.flush(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java index 7104b9453e..2d0fe28dae 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java @@ -17,10 +17,8 @@ import static org.eclipse.jgit.lib.Constants.PACK_SIGNATURE; import java.io.IOException; import java.io.OutputStream; -import java.security.MessageDigest; -import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.internal.storage.io.CancellableDigestOutputStream; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.util.NB; @@ -28,25 +26,14 @@ import org.eclipse.jgit.util.NB; * Custom output stream to support * {@link org.eclipse.jgit.internal.storage.pack.PackWriter}. */ -public final class PackOutputStream extends OutputStream { - private static final int BYTES_TO_WRITE_BEFORE_CANCEL_CHECK = 128 * 1024; - - private final ProgressMonitor writeMonitor; - - private final OutputStream out; +public final class PackOutputStream extends CancellableDigestOutputStream { private final PackWriter packWriter; - private final MessageDigest md = Constants.newMessageDigest(); - - private long count; - private final byte[] headerBuffer = new byte[32]; private final byte[] copyBuffer = new byte[64 << 10]; - private long checkCancelAt; - private boolean ofsDelta; /** @@ -66,48 +53,8 @@ public final class PackOutputStream extends OutputStream { */ public PackOutputStream(final ProgressMonitor writeMonitor, final OutputStream out, final PackWriter pw) { - this.writeMonitor = writeMonitor; - this.out = out; + super(writeMonitor, out); this.packWriter = pw; - this.checkCancelAt = BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; - } - - /** {@inheritDoc} */ - @Override - public final void write(int b) throws IOException { - count++; - out.write(b); - md.update((byte) b); - } - - /** {@inheritDoc} */ - @Override - public final void write(byte[] b, int off, int len) - throws IOException { - while (0 < len) { - final int n = Math.min(len, BYTES_TO_WRITE_BEFORE_CANCEL_CHECK); - count += n; - - if (checkCancelAt <= count) { - if (writeMonitor.isCancelled()) { - throw new IOException( - JGitText.get().packingCancelledDuringObjectsWriting); - } - checkCancelAt = count + BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; - } - - out.write(b, off, n); - md.update(b, off, n); - - off += n; - len -= n; - } - } - - /** {@inheritDoc} */ - @Override - public void flush() throws IOException { - out.flush(); } final void writeFileHeader(int version, long objectCount) @@ -160,7 +107,7 @@ public final class PackOutputStream extends OutputStream { ObjectToPack b = otp.getDeltaBase(); if (b != null && (b.isWritten() & ofsDelta)) { // Non-short-circuit logic is intentional int n = objectHeader(rawLength, OBJ_OFS_DELTA, headerBuffer); - n = ofsDelta(count - b.getOffset(), headerBuffer, n); + n = ofsDelta(length() - b.getOffset(), headerBuffer, n); write(headerBuffer, 0, n); } else if (otp.isDeltaRepresentation()) { int n = objectHeader(rawLength, OBJ_REF_DELTA, headerBuffer); @@ -209,20 +156,6 @@ public final class PackOutputStream extends OutputStream { } void endObject() { - writeMonitor.update(1); - } - - /** - * Get total number of bytes written since stream start. - * - * @return total number of bytes written since stream start. - */ - public final long length() { - return count; - } - - /** @return obtain the current SHA-1 digest. */ - final byte[] getDigest() { - return md.digest(); + getWriteMonitor().update(1); } } 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 648d4a1821..659ccb8c55 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 @@ -502,6 +502,98 @@ public class OpenSshConfigFile implements SshConfigStore { } /** + * Converts an OpenSSH time value into a number of seconds. The format is + * defined by OpenSSH as a sequence of (positive) integers with suffixes for + * seconds, minutes, hours, days, and weeks. + * + * @param value + * to convert + * @return the parsed value as a number of seconds, or -1 if the value is + * not a valid OpenSSH time value + * @see <a href="https://man.openbsd.org/sshd_config.5#TIME_FORMATS">OpenBSD + * man 5 sshd_config, section TIME FORMATS</a> + */ + public static int timeSpec(String value) { + if (value == null) { + return -1; + } + try { + int length = value.length(); + int i = 0; + int seconds = 0; + boolean valueSeen = false; + while (i < length) { + // Skip whitespace + char ch = value.charAt(i); + if (Character.isWhitespace(ch)) { + i++; + continue; + } + if (ch == '+') { + // OpenSSH uses strtol with base 10: a leading plus sign is + // allowed. + i++; + } + int val = 0; + int j = i; + while (j < length) { + ch = value.charAt(j++); + if (ch >= '0' && ch <= '9') { + val = Math.addExact(Math.multiplyExact(val, 10), + ch - '0'); + } else { + j--; + break; + } + } + if (i == j) { + // No digits seen + return -1; + } + i = j; + int multiplier = 1; + if (i < length) { + ch = value.charAt(i++); + switch (ch) { + case 's': + case 'S': + break; + case 'm': + case 'M': + multiplier = 60; + break; + case 'h': + case 'H': + multiplier = 3600; + break; + case 'd': + case 'D': + multiplier = 24 * 3600; + break; + case 'w': + case 'W': + multiplier = 7 * 24 * 3600; + break; + default: + if (Character.isWhitespace(ch)) { + break; + } + // Invalid time spec + return -1; + } + } + seconds = Math.addExact(seconds, + Math.multiplyExact(val, multiplier)); + valueSeen = true; + } + return valueSeen ? seconds : -1; + } catch (ArithmeticException e) { + // Overflow + return -1; + } + } + + /** * Retrieves the local user name as given in the constructor. * * @return the user name @@ -549,6 +641,7 @@ public class OpenSshConfigFile implements SshConfigStore { LIST_KEYS.add(SshConstants.GLOBAL_KNOWN_HOSTS_FILE); LIST_KEYS.add(SshConstants.SEND_ENV); LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE); + LIST_KEYS.add(SshConstants.ADD_KEYS_TO_AGENT); // confirm timeSpec } /** @@ -871,7 +964,8 @@ public class OpenSshConfigFile implements SshConfigStore { if (options != null) { // HOSTNAME already done above String value = options.get(SshConstants.IDENTITY_AGENT); - if (value != null) { + if (value != null && !SshConstants.NONE.equals(value) + && !SshConstants.ENV_SSH_AUTH_SOCKET.equals(value)) { value = r.substitute(value, Replacer.DEFAULT_TOKENS, true); value = toFile(value, home).getPath(); options.put(SshConstants.IDENTITY_AGENT, value); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java new file mode 100644 index 0000000000..9109cfd769 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2022, 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 + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; +import static org.eclipse.jgit.lib.TypedConfigGetter.UNSET_INT; + +import java.text.MessageFormat; + +import org.eclipse.jgit.api.errors.InvalidConfigurationException; +import org.eclipse.jgit.internal.JGitText; + +/** + * Git configuration option <a + * href=https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreabbrev"> + * core.abbrev</a> + * + * @since 6.1 + */ +public final class AbbrevConfig { + private static final String VALUE_NO = "no"; //$NON-NLS-1$ + + private static final String VALUE_AUTO = "auto"; //$NON-NLS-1$ + + /** + * The minimum value of abbrev + */ + public static final int MIN_ABBREV = 4; + + /** + * Cap configured core.abbrev to range between minimum of 4 and number of + * hex-digits of a full object id. + * + * @param len + * configured number of hex-digits to abbreviate object ids to + * @return core.abbrev capped to range between minimum of 4 and number of + * hex-digits of a full object id + */ + public static int capAbbrev(int len) { + return Math.min(Math.max(MIN_ABBREV, len), + Constants.OBJECT_ID_STRING_LENGTH); + } + + /** + * No abbreviation + */ + public final static AbbrevConfig NO = new AbbrevConfig( + Constants.OBJECT_ID_STRING_LENGTH); + + /** + * Parse string value of core.abbrev git option for a given repository + * + * @param repo + * repository + * @return the parsed AbbrevConfig + * @throws InvalidConfigurationException + * if value of core.abbrev is invalid + */ + public static AbbrevConfig parseFromConfig(Repository repo) + throws InvalidConfigurationException { + Config config = repo.getConfig(); + String value = config.getString(ConfigConstants.CONFIG_CORE_SECTION, + null, ConfigConstants.CONFIG_KEY_ABBREV); + if (value == null || value.equalsIgnoreCase(VALUE_AUTO)) { + return auto(repo); + } + if (value.equalsIgnoreCase(VALUE_NO)) { + return NO; + } + try { + int len = config.getIntInRange(ConfigConstants.CONFIG_CORE_SECTION, + ConfigConstants.CONFIG_KEY_ABBREV, MIN_ABBREV, + Constants.OBJECT_ID_STRING_LENGTH, UNSET_INT); + if (len == UNSET_INT) { + // Unset was checked above. If we get UNSET_INT here, then + // either the value was UNSET_INT, or it was an invalid value + // (not an integer, or out of range), and EGit's + // ReportingTypedGetter caught the exception and has logged a + // warning. In either case we should fall back to some sane + // default. + len = OBJECT_ID_ABBREV_STRING_LENGTH; + } + return new AbbrevConfig(len); + } catch (IllegalArgumentException e) { + throw new InvalidConfigurationException(MessageFormat + .format(JGitText.get().invalidCoreAbbrev, value), e); + } + } + + /** + * An appropriate value is computed based on the approximate number of + * packed objects in a repository, which hopefully is enough for abbreviated + * object names to stay unique for some time. + * + * @param repo + * @return appropriate value computed based on the approximate number of + * packed objects in a repository + */ + private static AbbrevConfig auto(Repository repo) { + long count = repo.getObjectDatabase().getApproximateObjectCount(); + if (count == -1) { + return new AbbrevConfig(OBJECT_ID_ABBREV_STRING_LENGTH); + } + // find msb, round to next power of 2 + int len = 63 - Long.numberOfLeadingZeros(count) + 1; + // With the order of 2^len objects, we expect a collision at + // 2^(len/2). But we also care about hex chars, not bits, and + // there are 4 bits per hex. So all together we need to divide + // by 2; but we also want to round odd numbers up, hence adding + // one before dividing. + len = (len + 1) / 2; + // for small repos use at least fallback length + return new AbbrevConfig(Math.max(len, OBJECT_ID_ABBREV_STRING_LENGTH)); + } + + /** + * All other possible abbreviation lengths. Valid range 4 to number of + * hex-digits of an unabbreviated object id (40 for SHA1 object ids, jgit + * doesn't support SHA256 yet). + */ + private int abbrev; + + /** + * @param abbrev + */ + private AbbrevConfig(int abbrev) { + this.abbrev = capAbbrev(abbrev); + } + + /** + * Get the configured abbreviation length for object ids. + * + * @return the configured abbreviation length for object ids + */ + public int get() { + return abbrev; + } + + @Override + public String toString() { + return Integer.toString(abbrev); + } +}
\ No newline at end of file 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 6da6f1204a..aa613d07eb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java @@ -138,6 +138,18 @@ public class BranchConfig { } /** + * Get the remote this branch is configured to push to. + * + * @return the remote this branch is configured to push to, or {@code null} + * if not defined + * @since 6.1 + */ + public String getPushRemote() { + return config.getString(ConfigConstants.CONFIG_BRANCH_SECTION, + branchName, ConfigConstants.CONFIG_KEY_PUSH_REMOTE); + } + + /** * Get the name of the upstream branch as it is called on the remote * * @return the name of the upstream branch as it is called on the remote, or 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 22e1f98181..55cc02683a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java @@ -18,11 +18,13 @@ import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; import java.text.MessageFormat; +import java.util.Locale; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Config.ConfigEnum; import org.eclipse.jgit.lib.Config.SectionParser; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.IO; @@ -34,22 +36,76 @@ import org.eclipse.jgit.util.RawParseUtils; * @since 5.13 */ public class CommitConfig { + /** * Key for {@link Config#get(SectionParser)}. */ public static final Config.SectionParser<CommitConfig> KEY = CommitConfig::new; + private static final String CUT = " ------------------------ >8 ------------------------\n"; //$NON-NLS-1$ + + /** + * How to clean up commit messages when committing. + * + * @since 6.1 + */ + public enum CleanupMode implements ConfigEnum { + + /** + * {@link #WHITESPACE}, additionally remove comment lines. + */ + STRIP, + + /** + * Remove trailing whitespace and leading and trailing empty lines; + * collapse multiple empty lines to a single one. + */ + WHITESPACE, + + /** + * Make no changes. + */ + VERBATIM, + + /** + * Omit everything from the first "scissor" line on, then apply + * {@link #WHITESPACE}. + */ + SCISSORS, + + /** + * Use {@link #STRIP} for user-edited messages, otherwise + * {@link #WHITESPACE}, unless overridden by a git config setting other + * than DEFAULT. + */ + DEFAULT; + + @Override + public String toConfigValue() { + return name().toLowerCase(Locale.ROOT); + } + + @Override + public boolean matchConfigValue(String in) { + return toConfigValue().equals(in); + } + } + private final static Charset DEFAULT_COMMIT_MESSAGE_ENCODING = StandardCharsets.UTF_8; private String i18nCommitEncoding; private String commitTemplatePath; + private CleanupMode cleanupMode; + private CommitConfig(Config rc) { commitTemplatePath = rc.getString(ConfigConstants.CONFIG_COMMIT_SECTION, null, ConfigConstants.CONFIG_KEY_COMMIT_TEMPLATE); i18nCommitEncoding = rc.getString(ConfigConstants.CONFIG_SECTION_I18N, null, ConfigConstants.CONFIG_KEY_COMMIT_ENCODING); + cleanupMode = rc.getEnum(ConfigConstants.CONFIG_COMMIT_SECTION, null, + ConfigConstants.CONFIG_KEY_CLEANUP, CleanupMode.DEFAULT); } /** @@ -75,6 +131,48 @@ public class CommitConfig { } /** + * Retrieves the {@link CleanupMode} as given by git config + * {@code commit.cleanup}. + * + * @return the {@link CleanupMode}; {@link CleanupMode#DEFAULT} if the git + * config is not set + * @since 6.1 + */ + @NonNull + public CleanupMode getCleanupMode() { + return cleanupMode; + } + + /** + * Computes a non-default {@link CleanupMode} from the given mode and the + * git config. + * + * @param mode + * {@link CleanupMode} to resolve + * @param defaultStrip + * if {@code true} return {@link CleanupMode#STRIP} if the git + * config is also "default", otherwise return + * {@link CleanupMode#WHITESPACE} + * @return the {@code mode}, if it is not {@link CleanupMode#DEFAULT}, + * otherwise the resolved mode, which is never + * {@link CleanupMode#DEFAULT} + * @since 6.1 + */ + @NonNull + public CleanupMode resolve(@NonNull CleanupMode mode, + boolean defaultStrip) { + if (CleanupMode.DEFAULT == mode) { + CleanupMode defaultMode = getCleanupMode(); + if (CleanupMode.DEFAULT == defaultMode) { + return defaultStrip ? CleanupMode.STRIP + : CleanupMode.WHITESPACE; + } + return defaultMode; + } + return mode; + } + + /** * Get the content to the commit template as defined in * {@code commit.template}. If no {@code i18n.commitEncoding} is specified, * UTF-8 fallback is used. @@ -135,4 +233,86 @@ public class CommitConfig { return commitMessageEncoding; } + + /** + * Processes a text according to the given {@link CleanupMode}. + * + * @param text + * text to process + * @param mode + * {@link CleanupMode} to use + * @param commentChar + * comment character (normally {@code #}) to use if {@code mode} + * is {@link CleanupMode#STRIP} or {@link CleanupMode#SCISSORS} + * @return the processed text + * @throws IllegalArgumentException + * if {@code mode} is {@link CleanupMode#DEFAULT} (use + * {@link #resolve(CleanupMode, boolean)} first) + * @since 6.1 + */ + public static String cleanText(@NonNull String text, + @NonNull CleanupMode mode, char commentChar) { + String toProcess = text; + boolean strip = false; + switch (mode) { + case VERBATIM: + return text; + case SCISSORS: + String cut = commentChar + CUT; + if (text.startsWith(cut)) { + return ""; //$NON-NLS-1$ + } + int cutPos = text.indexOf('\n' + cut); + if (cutPos >= 0) { + toProcess = text.substring(0, cutPos + 1); + } + break; + case STRIP: + strip = true; + break; + case WHITESPACE: + break; + case DEFAULT: + default: + // Internal error; no translation + throw new IllegalArgumentException("Invalid clean-up mode " + mode); //$NON-NLS-1$ + } + // WHITESPACE + StringBuilder result = new StringBuilder(); + boolean lastWasEmpty = true; + for (String line : toProcess.split("\n")) { //$NON-NLS-1$ + line = line.stripTrailing(); + if (line.isEmpty()) { + if (!lastWasEmpty) { + result.append('\n'); + lastWasEmpty = true; + } + } else if (!strip || !isComment(line, commentChar)) { + lastWasEmpty = false; + result.append(line).append('\n'); + } + } + int bufferSize = result.length(); + if (lastWasEmpty && bufferSize > 0) { + bufferSize--; + result.setLength(bufferSize); + } + if (bufferSize > 0 && !toProcess.endsWith("\n")) { //$NON-NLS-1$ + if (result.charAt(bufferSize - 1) == '\n') { + result.setLength(bufferSize - 1); + } + } + return result.toString(); + } + + private static boolean isComment(String text, char commentChar) { + int len = text.length(); + for (int i = 0; i < len; i++) { + char ch = text.charAt(i); + if (!Character.isWhitespace(ch)) { + return ch == commentChar; + } + } + return false; + } }
\ No newline at end of file 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 1ce3e312e2..d1d66d280e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -278,6 +278,54 @@ public class Config { } /** + * 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 + * @param defaultValue + * default value to return if no value was present. + * @return an integer value from the configuration, or defaultValue. + * @since 6.1 + */ + public int getIntInRange(String section, String name, int minValue, + int maxValue, int defaultValue) { + return typedGetter.getIntInRange(this, 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 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 + * @param defaultValue + * default value to return if no value was present. + * @return an integer value from the configuration, or defaultValue. + * @since 6.1 + */ + public int getIntInRange(String section, String subsection, String name, + int minValue, int maxValue, int defaultValue) { + return typedGetter.getIntInRange(this, section, subsection, name, + minValue, maxValue, defaultValue); + } + + /** * Obtain an integer value from the configuration. * * @param section 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 634e3f7f86..d4bd6c0e71 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -1,7 +1,8 @@ /* * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com> * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> - * Copyright (C) 2012, 2020, Robin Rosenberg and others + * Copyright (C) 2012-2013, Robin Rosenberg + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.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 @@ -29,6 +30,48 @@ public final class ConfigConstants { /** The "diff" section */ public static final String CONFIG_DIFF_SECTION = "diff"; + /** + * The "tool" key within "diff" section + * + * @since 6.1 + */ + public static final String CONFIG_KEY_TOOL = "tool"; + + /** + * The "guitool" key within "diff" section + * + * @since 6.1 + */ + public static final String CONFIG_KEY_GUITOOL = "guitool"; + + /** + * The "difftool" section + * + * @since 6.1 + */ + public static final String CONFIG_DIFFTOOL_SECTION = "difftool"; + + /** + * The "prompt" key within "difftool" section + * + * @since 6.1 + */ + public static final String CONFIG_KEY_PROMPT = "prompt"; + + /** + * The "trustExitCode" key within "difftool" section + * + * @since 6.1 + */ + public static final String CONFIG_KEY_TRUST_EXIT_CODE = "trustExitCode"; + + /** + * The "cmd" key within "difftool.*." section + * + * @since 6.1 + */ + public static final String CONFIG_KEY_CMD = "cmd"; + /** The "dfs" section */ public static final String CONFIG_DFS_SECTION = "dfs"; @@ -139,6 +182,13 @@ public final class ConfigConstants { public static final String CONFIG_TAG_SECTION = "tag"; /** + * The "cleanup" key + * + * @since 6.1 + */ + public static final String CONFIG_KEY_CLEANUP = "cleanup"; + + /** * The "gpgSign" key * * @since 5.2 @@ -279,6 +329,20 @@ public final class ConfigConstants { /** The "remote" key */ public static final String CONFIG_KEY_REMOTE = "remote"; + /** + * The "pushRemote" key. + * + * @since 6.1 + */ + public static final String CONFIG_KEY_PUSH_REMOTE = "pushRemote"; + + /** + * The "pushDefault" key. + * + * @since 6.1 + */ + public static final String CONFIG_KEY_PUSH_DEFAULT = "pushDefault"; + /** The "merge" key */ public static final String CONFIG_KEY_MERGE = "merge"; @@ -778,6 +842,34 @@ public final class ConfigConstants { public static final String CONFIG_KEY_SEARCH_FOR_REUSE_TIMEOUT = "searchforreusetimeout"; /** + * The "push" section. + * + * @since 6.1 + */ + public static final String CONFIG_PUSH_SECTION = "push"; + + /** + * The "default" key. + * + * @since 6.1 + */ + public static final String CONFIG_KEY_DEFAULT = "default"; + + /** + * The "abbrev" key + * + * @since 6.1 + */ + public static final String CONFIG_KEY_ABBREV = "abbrev"; + + /** + * The "trustPackedRefsStat" key + * + * @since 6.1.1 + */ + public static final String CONFIG_KEY_TRUST_PACKED_REFS_STAT = "trustPackedRefsStat"; + + /** * The "pack.preserveOldPacks" key * * @since 5.13.2 @@ -790,5 +882,4 @@ public final class ConfigConstants { * @since 5.13.2 */ public static final String CONFIG_KEY_PRUNE_PRESERVED = "prunepreserved"; - } 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 92367ebd0c..cf2e69dbb5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -48,6 +48,15 @@ public final class Constants { */ public static final int OBJECT_ID_STRING_LENGTH = OBJECT_ID_LENGTH * 2; + /** + * The historic length of an abbreviated Git object hash string. Git 2.11 + * changed this static number to a dynamically calculated one that scales + * as the repository grows. + * + * @since 6.1 + */ + public static final int OBJECT_ID_ABBREV_STRING_LENGTH = 7; + /** Special name for the "HEAD" symbolic-ref. */ public static final String HEAD = "HEAD"; 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 f23c6e08d1..fc82a5fead 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java @@ -116,6 +116,27 @@ public class CoreConfig { ALWAYS } + /** + * Permissible values for {@code core.trustPackedRefsStat}. + * + * @since 6.1.1 + */ + public enum TrustPackedRefsStat { + /** Do not trust file attributes of the packed-refs file. */ + NEVER, + + /** 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. */ + AFTER_OPEN, + + /** {@code core.trustPackedRefsStat} defaults to this when it is + * not set */ + UNSET + } + private final int compression; private final int packIndexVersion; 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 9f96bce251..86409403b0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java @@ -120,6 +120,26 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { /** {@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); + 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().integerValueNotInRangeSubSection, section, + subsection, name, Integer.valueOf(val), + Integer.valueOf(minValue), Integer.valueOf(maxValue))); + } + + /** {@inheritDoc} */ + @Override public long getLong(Config config, String section, String subsection, String name, long defaultValue) { final String str = config.getString(section, subsection, name); 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 28ea927b14..df9fd47efa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java @@ -568,6 +568,9 @@ public class IndexDiff { if (ignoreSubmoduleMode != IgnoreSubmoduleMode.ALL) { try (SubmoduleWalk smw = new SubmoduleWalk(repository)) { smw.setTree(new DirCacheIterator(dirCache)); + if (filter != null) { + smw.setFilter(filter); + } smw.setBuilderFactory(factory); while (smw.next()) { IgnoreSubmoduleMode localIgnoreSubmoduleMode = ignoreSubmoduleMode; 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 04262c07ae..70009cba35 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java @@ -155,4 +155,14 @@ public abstract class ObjectDatabase implements AutoCloseable { public ObjectDatabase newCachedDatabase() { return this; } + + /** + * Get a quick, rough count of objects in this repository. Ignores loose + * objects. Returns {@code -1} if an exception occurs. + * + * @return quick, rough count of objects in this repository, {@code -1} if + * an exception occurs + * @since 6.1 + */ + public abstract long getApproximateObjectCount(); } 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 a2c7381ce7..26c3ff6718 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.lib; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -76,7 +78,7 @@ public abstract class ObjectReader implements AutoCloseable { */ public AbbreviatedObjectId abbreviate(AnyObjectId objectId) throws IOException { - return abbreviate(objectId, 7); + return abbreviate(objectId, OBJECT_ID_ABBREV_STRING_LENGTH); } /** 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 428a6b959c..93710299b4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java @@ -14,6 +14,8 @@ package org.eclipse.jgit.lib; import java.io.Serializable; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -206,6 +208,20 @@ public class PersonIdent implements Serializable { } /** + * Copy a {@link org.eclipse.jgit.lib.PersonIdent}, but alter the clone's + * time stamp + * + * @param pi + * original {@link org.eclipse.jgit.lib.PersonIdent} + * @param aWhen + * local time as Instant + * @since 6.1 + */ + public PersonIdent(PersonIdent pi, Instant aWhen) { + this(pi.getName(), pi.getEmailAddress(), aWhen.toEpochMilli(), pi.tzOffset); + } + + /** * Construct a PersonIdent from simple data * * @param aName a {@link java.lang.String} object. @@ -222,6 +238,27 @@ public class PersonIdent implements Serializable { } /** + * Construct a PersonIdent from simple data + * + * @param aName + * a {@link java.lang.String} object. + * @param aEmailAddress + * a {@link java.lang.String} object. + * @param aWhen + * local time stamp + * @param zoneId + * time zone id + * @since 6.1 + */ + public PersonIdent(final String aName, String aEmailAddress, Instant aWhen, + ZoneId zoneId) { + this(aName, aEmailAddress, aWhen.toEpochMilli(), + TimeZone.getTimeZone(zoneId) + .getOffset(aWhen + .toEpochMilli()) / (60 * 1000)); + } + + /** * Copy a PersonIdent, but alter the clone's time stamp * * @param pi @@ -304,6 +341,16 @@ public class PersonIdent implements Serializable { } /** + * Get when attribute as instant + * + * @return timestamp + * @since 6.1 + */ + public Instant getWhenAsInstant() { + return Instant.ofEpochMilli(when); + } + + /** * Get this person's declared time zone * * @return this person's declared time zone; null if time zone is unknown. @@ -313,6 +360,16 @@ public class PersonIdent implements Serializable { } /** + * Get the time zone id + * + * @return the time zone id + * @since 6.1 + */ + public ZoneId getZoneId() { + return getTimeZone().toZoneId(); + } + + /** * Get this person's declared time zone as minutes east of UTC. * * @return this person's declared time zone as minutes east of UTC. If the 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 0f2f6cff8a..c4eb8f10d5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java @@ -29,6 +29,13 @@ import org.eclipse.jgit.util.FS; public interface TypedConfigGetter { /** + * Use {@code Integer#MIN_VALUE} as unset int value + * + * @since 6.1 + */ + public static final int UNSET_INT = Integer.MIN_VALUE; + + /** * Get a boolean value from a git {@link Config}. * * @param config @@ -87,6 +94,32 @@ public interface TypedConfigGetter { 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 6.1 + */ + int getIntInRange(Config config, String section, String subsection, + String name, int minValue, int maxValue, int defaultValue); + + /** * Obtain a long value from a git {@link Config}. * * @param config diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BooleanTriState.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BooleanTriState.java new file mode 100644 index 0000000000..44d3bb36e0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BooleanTriState.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Simeon Andreev <simeon.danailov.andreev@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 + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib.internal; + +/** + * A boolean value that can also have an unset state. + */ +public enum BooleanTriState { + /** + * Value equivalent to {@code true}. + */ + TRUE, + /** + * Value equivalent to {@code false}. + */ + FALSE, + /** + * Value is not set. + */ + UNSET; +} 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 f7966a267f..e0c083f55c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java @@ -92,36 +92,62 @@ public class MergeMessageFormatter { } /** - * Add section with conflicting paths to merge message. + * 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 + * the original merge message + * @param conflictingPaths + * the paths with conflicts + * @param commentChar + * comment character to use for prefixing the conflict lines + * @return merge message with conflicting paths added + * @since 6.1 + */ + public String formatWithConflicts(String message, + Iterable<String> conflictingPaths, char commentChar) { StringBuilder sb = new StringBuilder(); String[] lines = message.split("\n"); //$NON-NLS-1$ int firstFooterLine = ChangeIdUtil.indexOfFirstFooterLine(lines); - for (int i = 0; i < firstFooterLine; i++) + for (int i = 0; i < firstFooterLine; i++) { sb.append(lines[i]).append('\n'); - if (firstFooterLine == lines.length && message.length() != 0) + } + if (firstFooterLine == lines.length && message.length() != 0) { sb.append('\n'); - addConflictsMessage(conflictingPaths, sb); - if (firstFooterLine < lines.length) + } + addConflictsMessage(conflictingPaths, sb, commentChar); + if (firstFooterLine < lines.length) { sb.append('\n'); - for (int i = firstFooterLine; i < lines.length; i++) + } + for (int i = firstFooterLine; i < lines.length; i++) { sb.append(lines[i]).append('\n'); + } return sb.toString(); } - private static void addConflictsMessage(List<String> conflictingPaths, - StringBuilder sb) { - sb.append("Conflicts:\n"); //$NON-NLS-1$ + private static void addConflictsMessage(Iterable<String> conflictingPaths, + StringBuilder sb, char commentChar) { + sb.append(commentChar).append(" Conflicts:\n"); //$NON-NLS-1$ for (String conflictingPath : conflictingPaths) { - sb.append('\t').append(conflictingPath).append('\n'); + sb.append(commentChar).append('\t').append(conflictingPath) + .append('\n'); } } 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 7767662867..b9ab1d1b7a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -3,7 +3,7 @@ * 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, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 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 @@ -276,11 +276,15 @@ public class ResolveMerger extends ThreeWayMerger { private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT; /** - * Keeps {@link CheckoutMetadata} for {@link #checkout()} and - * {@link #cleanUp()}. + * Keeps {@link CheckoutMetadata} for {@link #checkout()}. */ private Map<String, CheckoutMetadata> checkoutMetadata; + /** + * Keeps {@link CheckoutMetadata} for {@link #cleanUp()}. + */ + private Map<String, CheckoutMetadata> cleanupMetadata; + private static MergeAlgorithm getMergeAlgorithm(Config config) { SupportedAlgorithm diffAlg = config.getEnum( CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM, @@ -383,12 +387,14 @@ public class ResolveMerger extends ThreeWayMerger { } if (!inCore) { checkoutMetadata = new HashMap<>(); + cleanupMetadata = new HashMap<>(); } try { return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], false); } finally { checkoutMetadata = null; + cleanupMetadata = null; if (implicitDirCache) { dircache.unlock(); } @@ -447,7 +453,7 @@ public class ResolveMerger extends ThreeWayMerger { DirCacheEntry entry = dc.getEntry(mpath); if (entry != null) { DirCacheCheckout.checkoutEntry(db, entry, reader, false, - checkoutMetadata.get(mpath)); + cleanupMetadata.get(mpath)); } mpathsIt.remove(); } @@ -501,22 +507,26 @@ public class ResolveMerger extends ThreeWayMerger { * Remembers the {@link CheckoutMetadata} for the given path; it may be * needed in {@link #checkout()} or in {@link #cleanUp()}. * + * @param map + * to add the metadata to * @param path * of the current node * @param attributes - * for the current node + * to use for determining the metadata * @throws IOException * if the smudge filter cannot be determined - * @since 5.1 + * @since 6.1 */ - protected void addCheckoutMetadata(String path, Attributes attributes) + protected void addCheckoutMetadata(Map<String, CheckoutMetadata> map, + String path, Attributes attributes) throws IOException { - if (checkoutMetadata != null) { + if (map != null) { EolStreamType eol = EolStreamTypeUtil.detectStreamType( - OperationType.CHECKOUT_OP, workingTreeOptions, attributes); + OperationType.CHECKOUT_OP, workingTreeOptions, + attributes); CheckoutMetadata data = new CheckoutMetadata(eol, - tw.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE)); - checkoutMetadata.put(path, data); + tw.getSmudgeCommand(attributes)); + map.put(path, data); } } @@ -529,15 +539,17 @@ public class ResolveMerger extends ThreeWayMerger { * @param entry * to add * @param attributes - * for the current entry + * the {@link Attributes} of the trees * @throws IOException * if the {@link CheckoutMetadata} cannot be determined - * @since 5.1 + * @since 6.1 */ protected void addToCheckout(String path, DirCacheEntry entry, - Attributes attributes) throws IOException { + Attributes[] attributes) + throws IOException { toBeCheckedOut.put(path, entry); - addCheckoutMetadata(path, attributes); + addCheckoutMetadata(cleanupMetadata, path, attributes[T_OURS]); + addCheckoutMetadata(checkoutMetadata, path, attributes[T_THEIRS]); } /** @@ -549,7 +561,7 @@ public class ResolveMerger extends ThreeWayMerger { * @param isFile * whether it is a file * @param attributes - * for the entry + * to use for determining the {@link CheckoutMetadata} * @throws IOException * if the {@link CheckoutMetadata} cannot be determined * @since 5.1 @@ -558,7 +570,7 @@ public class ResolveMerger extends ThreeWayMerger { Attributes attributes) throws IOException { toBeDeleted.add(path); if (isFile) { - addCheckoutMetadata(path, attributes); + addCheckoutMetadata(cleanupMetadata, path, attributes); } } @@ -599,7 +611,7 @@ public class ResolveMerger extends ThreeWayMerger { * see * {@link org.eclipse.jgit.merge.ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} * @param attributes - * the attributes defined for this entry + * the {@link Attributes} for the three trees * @return <code>false</code> if the merge will fail because the index entry * didn't match ours or the working-dir file was dirty and a * conflict occurred @@ -607,12 +619,12 @@ public class ResolveMerger extends ThreeWayMerger { * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException * @throws org.eclipse.jgit.errors.CorruptObjectException * @throws java.io.IOException - * @since 4.9 + * @since 6.1 */ protected boolean processEntry(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, DirCacheBuildIterator index, WorkingTreeIterator work, - boolean ignoreConflicts, Attributes attributes) + boolean ignoreConflicts, Attributes[] attributes) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { enterSubtree = true; @@ -729,7 +741,7 @@ public class ResolveMerger extends ThreeWayMerger { // Base, ours, and theirs all contain a folder: don't delete return true; } - addDeletion(tw.getPathString(), nonTree(modeO), attributes); + addDeletion(tw.getPathString(), nonTree(modeO), attributes[T_OURS]); return true; } @@ -772,7 +784,7 @@ public class ResolveMerger extends ThreeWayMerger { if (nonTree(modeO) && nonTree(modeT)) { // Check worktree before modifying files boolean worktreeDirty = isWorktreeDirty(work, ourDce); - if (!attributes.canBeContentMerged() && worktreeDirty) { + if (!attributes[T_OURS].canBeContentMerged() && worktreeDirty) { return false; } @@ -791,7 +803,7 @@ public class ResolveMerger extends ThreeWayMerger { mergeResults.put(tw.getPathString(), result); unmergedPaths.add(tw.getPathString()); return true; - } else if (!attributes.canBeContentMerged()) { + } else if (!attributes[T_OURS].canBeContentMerged()) { // File marked as binary switch (getContentMergeStrategy()) { case OURS: @@ -842,13 +854,16 @@ public class ResolveMerger extends ThreeWayMerger { if (ignoreConflicts) { result.setContainsConflicts(false); } - updateIndex(base, ours, theirs, result, attributes); + updateIndex(base, ours, theirs, result, attributes[T_OURS]); String currentPath = tw.getPathString(); if (result.containsConflicts() && !ignoreConflicts) { unmergedPaths.add(currentPath); } modifiedFiles.add(currentPath); - addCheckoutMetadata(currentPath, attributes); + addCheckoutMetadata(cleanupMetadata, currentPath, + attributes[T_OURS]); + addCheckoutMetadata(checkoutMetadata, currentPath, + attributes[T_THEIRS]); } else if (modeO != modeT) { // OURS or THEIRS has been deleted if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw @@ -881,7 +896,8 @@ public class ResolveMerger extends ThreeWayMerger { // markers). But also stage 0 of the index is filled // with that content. result.setContainsConflicts(false); - updateIndex(base, ours, theirs, result, attributes); + updateIndex(base, ours, theirs, result, + attributes[T_OURS]); } else { add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); @@ -896,11 +912,9 @@ public class ResolveMerger extends ThreeWayMerger { if (isWorktreeDirty(work, ourDce)) { return false; } - if (nonTree(modeT)) { - if (e != null) { - addToCheckout(tw.getPathString(), e, - attributes); - } + if (nonTree(modeT) && e != null) { + addToCheckout(tw.getPathString(), e, + attributes); } } @@ -945,14 +959,16 @@ public class ResolveMerger extends ThreeWayMerger { */ private MergeResult<RawText> contentMerge(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, - Attributes attributes, ContentMergeStrategy strategy) + Attributes[] attributes, ContentMergeStrategy strategy) throws BinaryBlobException, IOException { + // TW: The attributes here are used to determine the LFS smudge filter. + // Is doing a content merge on LFS items really a good idea?? RawText baseText = base == null ? RawText.EMPTY_TEXT - : getRawText(base.getEntryObjectId(), attributes); + : getRawText(base.getEntryObjectId(), attributes[T_BASE]); RawText ourText = ours == null ? RawText.EMPTY_TEXT - : getRawText(ours.getEntryObjectId(), attributes); + : getRawText(ours.getEntryObjectId(), attributes[T_OURS]); RawText theirsText = theirs == null ? RawText.EMPTY_TEXT - : getRawText(theirs.getEntryObjectId(), attributes); + : getRawText(theirs.getEntryObjectId(), attributes[T_THEIRS]); mergeAlgorithm.setContentMergeStrategy(strategy); return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText, ourText, theirsText); @@ -1342,7 +1358,7 @@ public class ResolveMerger extends ThreeWayMerger { tw = new NameConflictTreeWalk(db, reader); tw.addTree(baseTree); - tw.addTree(headTree); + tw.setHead(tw.addTree(headTree)); tw.addTree(mergeTree); int dciPos = tw.addTree(buildIt); if (workingTreeIterator != null) { @@ -1403,6 +1419,13 @@ public class ResolveMerger extends ThreeWayMerger { boolean hasAttributeNodeProvider = treeWalk .getAttributesNodeProvider() != null; while (treeWalk.next()) { + Attributes[] attributes = { NO_ATTRIBUTES, NO_ATTRIBUTES, + NO_ATTRIBUTES }; + if (hasAttributeNodeProvider) { + attributes[T_BASE] = treeWalk.getAttributes(T_BASE); + attributes[T_OURS] = treeWalk.getAttributes(T_OURS); + attributes[T_THEIRS] = treeWalk.getAttributes(T_THEIRS); + } if (!processEntry( treeWalk.getTree(T_BASE, CanonicalTreeParser.class), treeWalk.getTree(T_OURS, CanonicalTreeParser.class), @@ -1410,9 +1433,7 @@ public class ResolveMerger extends ThreeWayMerger { treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class), hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, WorkingTreeIterator.class) : null, - ignoreConflicts, hasAttributeNodeProvider - ? treeWalk.getAttributes() - : NO_ATTRIBUTES)) { + ignoreConflicts, attributes)) { cleanUp(); return false; } 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 e6f9580bf7..4e48a5c328 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2022 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 @@ -139,7 +139,7 @@ public class ObjectWalk extends RevWalk { * the repository the walker will obtain data from. */ public ObjectWalk(Repository repo) { - this(repo.newObjectReader()); + this(repo.newObjectReader(), true); } /** @@ -151,7 +151,11 @@ public class ObjectWalk extends RevWalk { * required. */ public ObjectWalk(ObjectReader or) { - super(or); + this(or, false); + } + + private ObjectWalk(ObjectReader or, boolean closeReader) { + super(or, closeReader); setRetainBody(false); rootObjects = new ArrayList<>(); pendingObjects = new BlockObjQueue(); 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 8d571f5b14..a50eaf1a8a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java @@ -215,7 +215,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { this(or, false); } - private RevWalk(ObjectReader or, boolean closeReader) { + RevWalk(ObjectReader or, boolean closeReader) { reader = or; idBuffer = new MutableObjectId(); objects = new ObjectIdOwnerMap<>(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java index fda7a8152a..c8774d546a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017, David Pursehouse <david.pursehouse@gmail.com> and others + * Copyright (C) 2017, 2022 David Pursehouse <david.pursehouse@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 @@ -10,7 +10,10 @@ package org.eclipse.jgit.transport; +import java.util.Locale; + import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.util.StringUtils; /** @@ -19,8 +22,9 @@ import org.eclipse.jgit.util.StringUtils; * @since 4.9 */ public class PushConfig { + /** - * Config values for push.recurseSubmodules. + * Git config values for {@code push.recurseSubmodules}. */ public enum PushRecurseSubmodulesMode implements Config.ConfigEnum { /** @@ -59,4 +63,100 @@ public class PushConfig { || configValue.equalsIgnoreCase(s); } } + + /** + * Git config values for {@code push.default}. + * + * @since 6.1 + */ + public enum PushDefault implements Config.ConfigEnum { + + /** + * Do not push if there are no explicit refspecs. + */ + NOTHING, + + /** + * Push the current branch to an upstream branch of the same name. + */ + CURRENT, + + /** + * Push the current branch to an upstream branch determined by git + * config {@code branch.<currentBranch>.merge}. + */ + UPSTREAM("tracking"), //$NON-NLS-1$ + + /** + * Like {@link #UPSTREAM}, but only if the upstream name is the same as + * the name of the current local branch. + */ + SIMPLE, + + /** + * Push all current local branches that match a configured push refspec + * of the remote configuration. + */ + MATCHING; + + private final String alias; + + private PushDefault() { + alias = null; + } + + private PushDefault(String alias) { + this.alias = alias; + } + + @Override + public String toConfigValue() { + return name().toLowerCase(Locale.ROOT); + } + + @Override + public boolean matchConfigValue(String in) { + return toConfigValue().equalsIgnoreCase(in) + || (alias != null && alias.equalsIgnoreCase(in)); + } + } + + private final PushRecurseSubmodulesMode recurseSubmodules; + + private final PushDefault pushDefault; + + /** + * Creates a new instance. + * + * @param config + * {@link Config} to fill the {@link PushConfig} from + * @since 6.1 + */ + public PushConfig(Config config) { + recurseSubmodules = config.getEnum(ConfigConstants.CONFIG_PUSH_SECTION, + null, ConfigConstants.CONFIG_KEY_RECURSE_SUBMODULES, + PushRecurseSubmodulesMode.NO); + pushDefault = config.getEnum(ConfigConstants.CONFIG_PUSH_SECTION, null, + ConfigConstants.CONFIG_KEY_DEFAULT, PushDefault.SIMPLE); + } + + /** + * Retrieves the value of git config {@code push.recurseSubmodules}. + * + * @return the value + * @since 6.1 + */ + public PushRecurseSubmodulesMode getRecurseSubmodules() { + return recurseSubmodules; + } + + /** + * Retrieves the value of git config {@code push.default}. + * + * @return the value + * @since 6.1 + */ + public PushDefault getPushDefault() { + return pushDefault; + } } 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 a244c55a38..942dad46e0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java @@ -18,11 +18,15 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.hooks.PrePushHook; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; @@ -58,6 +62,8 @@ class PushProcess { /** A list of option strings associated with this push */ private List<String> pushOptions; + private final PrePushHook prePush; + /** * Create process for specified transport and refs updates specification. * @@ -66,12 +72,14 @@ class PushProcess { * connection. * @param toPush * specification of refs updates (and local tracking branches). - * + * @param prePush + * {@link PrePushHook} to run after the remote advertisement has + * been gotten * @throws TransportException */ - PushProcess(final Transport transport, - final Collection<RemoteRefUpdate> toPush) throws TransportException { - this(transport, toPush, null); + PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush, + PrePushHook prePush) throws TransportException { + this(transport, toPush, prePush, null); } /** @@ -82,16 +90,19 @@ class PushProcess { * connection. * @param toPush * specification of refs updates (and local tracking branches). + * @param prePush + * {@link PrePushHook} to run after the remote advertisement has + * been gotten * @param out * OutputStream to write messages to * @throws TransportException */ - PushProcess(final Transport transport, - final Collection<RemoteRefUpdate> toPush, OutputStream out) - throws TransportException { + PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush, + PrePushHook prePush, OutputStream out) throws TransportException { this.walker = new RevWalk(transport.local); this.transport = transport; this.toPush = new LinkedHashMap<>(); + this.prePush = prePush; this.out = out; this.pushOptions = transport.getPushOptions(); for (RemoteRefUpdate rru : toPush) { @@ -129,10 +140,38 @@ class PushProcess { res.setAdvertisedRefs(transport.getURI(), connection .getRefsMap()); res.peerUserAgent = connection.getPeerUserAgent(); - res.setRemoteUpdates(toPush); monitor.endTask(); + Map<String, RemoteRefUpdate> expanded = expandMatching(); + toPush.clear(); + toPush.putAll(expanded); + + res.setRemoteUpdates(toPush); final Map<String, RemoteRefUpdate> preprocessed = prepareRemoteUpdates(); + List<RemoteRefUpdate> willBeAttempted = preprocessed.values() + .stream().filter(u -> { + switch (u.getStatus()) { + case NON_EXISTING: + case REJECTED_NODELETE: + case REJECTED_NONFASTFORWARD: + case REJECTED_OTHER_REASON: + case REJECTED_REMOTE_CHANGED: + case UP_TO_DATE: + return false; + default: + return true; + } + }).collect(Collectors.toList()); + if (!willBeAttempted.isEmpty()) { + if (prePush != null) { + try { + prePush.setRefs(willBeAttempted); + prePush.call(); + } catch (AbortedByHookException | IOException e) { + throw new TransportException(e.getMessage(), e); + } + } + } if (transport.isDryRun()) modifyUpdatesForDryRun(); else if (!preprocessed.isEmpty()) @@ -201,25 +240,8 @@ class PushProcess { continue; } - // check for fast-forward: - // - both old and new ref must point to commits, AND - // - both of them must be known for us, exist in repository, AND - // - old commit must be ancestor of new commit - boolean fastForward = true; - try { - RevObject oldRev = walker.parseAny(advertisedOld); - final RevObject newRev = walker.parseAny(rru.getNewObjectId()); - if (!(oldRev instanceof RevCommit) - || !(newRev instanceof RevCommit) - || !walker.isMergedInto((RevCommit) oldRev, - (RevCommit) newRev)) - fastForward = false; - } catch (MissingObjectException x) { - fastForward = false; - } catch (Exception x) { - throw new TransportException(transport.getURI(), MessageFormat.format( - JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x); - } + boolean fastForward = isFastForward(advertisedOld, + rru.getNewObjectId()); rru.setFastForward(fastForward); if (!fastForward && !rru.isForceUpdate()) { rru.setStatus(Status.REJECTED_NONFASTFORWARD); @@ -233,6 +255,134 @@ class PushProcess { return result; } + /** + * Determines whether an update from {@code oldOid} to {@code newOid} is a + * fast-forward update: + * <ul> + * <li>both old and new must be commits, AND</li> + * <li>both of them must be known to us and exist in the repository, + * AND</li> + * <li>the old commit must be an ancestor of the new commit.</li> + * </ul> + * + * @param oldOid + * {@link ObjectId}Â of the old commit + * @param newOid + * {@link ObjectId}Â of the new commit + * @return {@code true} if the update fast-forwards, {@code false} otherwise + * @throws TransportException + */ + private boolean isFastForward(ObjectId oldOid, ObjectId newOid) + throws TransportException { + try { + RevObject oldRev = walker.parseAny(oldOid); + RevObject newRev = walker.parseAny(newOid); + if (!(oldRev instanceof RevCommit) || !(newRev instanceof RevCommit) + || !walker.isMergedInto((RevCommit) oldRev, + (RevCommit) newRev)) { + return false; + } + } catch (MissingObjectException x) { + return false; + } catch (Exception x) { + throw new TransportException(transport.getURI(), + MessageFormat.format(JGitText + .get().readingObjectsFromLocalRepositoryFailed, + x.getMessage()), + x); + } + return true; + } + + /** + * Expands all placeholder {@link RemoteRefUpdate}s for "matching" + * {@link RefSpec}s ":" in {@link #toPush} and returns the resulting map in + * which the placeholders have been replaced by their expansion. + * + * @return a new map of {@link RemoteRefUpdate}s keyed by remote name + * @throws TransportException + * if the expansion results in duplicate updates + */ + private Map<String, RemoteRefUpdate> expandMatching() + throws TransportException { + Map<String, RemoteRefUpdate> result = new LinkedHashMap<>(); + boolean hadMatch = false; + for (RemoteRefUpdate update : toPush.values()) { + if (update.isMatching()) { + if (hadMatch) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + ":")); //$NON-NLS-1$ + } + expandMatching(result, update); + hadMatch = true; + } else if (result.put(update.getRemoteName(), update) != null) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + update.getRemoteName())); + } + } + return result; + } + + /** + * Expands the placeholder {@link RemoteRefUpdate} {@code match} for a + * "matching" {@link RefSpec} ":" or "+:" and puts the expansion into the + * given map {@code updates}. + * + * @param updates + * map to put the expansion in + * @param match + * the placeholder {@link RemoteRefUpdate} to expand + * + * @throws TransportException + * if the expansion results in duplicate updates, or the local + * branches cannot be determined + */ + private void expandMatching(Map<String, RemoteRefUpdate> updates, + RemoteRefUpdate match) throws TransportException { + try { + Map<String, Ref> advertisement = connection.getRefsMap(); + Collection<RefSpec> fetchSpecs = match.getFetchSpecs(); + boolean forceUpdate = match.isForceUpdate(); + for (Ref local : transport.local.getRefDatabase() + .getRefsByPrefix(Constants.R_HEADS)) { + if (local.isSymbolic()) { + continue; + } + String name = local.getName(); + Ref advertised = advertisement.get(name); + if (advertised == null || advertised.isSymbolic()) { + continue; + } + ObjectId oldOid = advertised.getObjectId(); + if (oldOid == null || ObjectId.zeroId().equals(oldOid)) { + continue; + } + ObjectId newOid = local.getObjectId(); + if (newOid == null || ObjectId.zeroId().equals(newOid)) { + continue; + } + + RemoteRefUpdate rru = new RemoteRefUpdate(transport.local, name, + newOid, name, forceUpdate, + Transport.findTrackingRefName(name, fetchSpecs), + oldOid); + if (updates.put(rru.getRemoteName(), rru) != null) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + rru.getRemoteName())); + } + } + } catch (IOException x) { + throw new TransportException(transport.getURI(), + MessageFormat.format(JGitText + .get().readingObjectsFromLocalRepositoryFailed, + x.getMessage()), + x); + } + } + private Map<String, RemoteRefUpdate> rejectAll() { for (RemoteRefUpdate rru : toPush.values()) { if (rru.getStatus() == Status.NOT_ATTEMPTED) { 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 ac357afdae..56d0036a20 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java @@ -12,11 +12,11 @@ package org.eclipse.jgit.transport; import java.io.Serializable; import java.text.MessageFormat; +import java.util.Objects; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.util.References; /** * Describes how refs in one repository copy into another repository. @@ -50,6 +50,9 @@ public class RefSpec implements Serializable { /** Is this specification actually a wildcard match? */ private boolean wildcard; + /** Is this the special ":" RefSpec? */ + private boolean matching; + /** * How strict to be about wildcards. * @@ -71,6 +74,7 @@ public class RefSpec implements Serializable { */ ALLOW_MISMATCH } + /** Whether a wildcard is allowed on one side but not the other. */ private WildcardMode allowMismatchedWildcards; @@ -87,6 +91,7 @@ public class RefSpec implements Serializable { * applications, as at least one field must be set to match a source name. */ public RefSpec() { + matching = false; force = false; wildcard = false; srcName = Constants.HEAD; @@ -133,17 +138,25 @@ public class RefSpec implements Serializable { s = s.substring(1); } + boolean matchPushSpec = false; final int c = s.lastIndexOf(':'); if (c == 0) { s = s.substring(1); - if (isWildcard(s)) { + if (s.isEmpty()) { + matchPushSpec = true; wildcard = true; - if (mode == WildcardMode.REQUIRE_MATCH) { - throw new IllegalArgumentException(MessageFormat - .format(JGitText.get().invalidWildcards, spec)); + srcName = Constants.R_HEADS + '*'; + dstName = srcName; + } else { + if (isWildcard(s)) { + wildcard = true; + if (mode == WildcardMode.REQUIRE_MATCH) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidWildcards, spec)); + } } + dstName = checkValid(s); } - dstName = checkValid(s); } else if (c > 0) { String src = s.substring(0, c); String dst = s.substring(c + 1); @@ -168,6 +181,7 @@ public class RefSpec implements Serializable { } srcName = checkValid(s); } + matching = matchPushSpec; } /** @@ -195,6 +209,7 @@ public class RefSpec implements Serializable { } private RefSpec(RefSpec p) { + matching = false; force = p.isForceUpdate(); wildcard = p.isWildcard(); srcName = p.getSource(); @@ -203,6 +218,17 @@ public class RefSpec implements Serializable { } /** + * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":" + * for pushing. + * + * @return whether this is a "matching" RefSpec + * @since 6.1 + */ + public boolean isMatching() { + return matching; + } + + /** * Check if this specification wants to forcefully update the destination. * * @return true if this specification asks for updates without merge tests. @@ -220,6 +246,7 @@ public class RefSpec implements Serializable { */ public RefSpec setForceUpdate(boolean forceUpdate) { final RefSpec r = new RefSpec(this); + r.matching = matching; r.force = forceUpdate; return r; } @@ -322,8 +349,7 @@ public class RefSpec implements Serializable { * The wildcard status of the new source disagrees with the * wildcard status of the new destination. */ - public RefSpec setSourceDestination(final String source, - final String destination) { + public RefSpec setSourceDestination(String source, String destination) { if (isWildcard(source) != isWildcard(destination)) throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch); final RefSpec r = new RefSpec(this); @@ -541,37 +567,36 @@ public class RefSpec implements Serializable { if (!(obj instanceof RefSpec)) return false; final RefSpec b = (RefSpec) obj; - if (isForceUpdate() != b.isForceUpdate()) - return false; - if (isWildcard() != b.isWildcard()) + if (isForceUpdate() != b.isForceUpdate()) { return false; - if (!eq(getSource(), b.getSource())) - return false; - if (!eq(getDestination(), b.getDestination())) - return false; - return true; - } - - private static boolean eq(String a, String b) { - if (References.isSameObject(a, b)) { - return true; } - if (a == null || b == null) + if (isMatching()) { + return b.isMatching(); + } else if (b.isMatching()) { return false; - return a.equals(b); + } + return isWildcard() == b.isWildcard() + && Objects.equals(getSource(), b.getSource()) + && Objects.equals(getDestination(), b.getDestination()); } /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); - if (isForceUpdate()) + if (isForceUpdate()) { r.append('+'); - if (getSource() != null) - r.append(getSource()); - if (getDestination() != null) { + } + if (isMatching()) { r.append(':'); - r.append(getDestination()); + } else { + if (getSource() != null) { + r.append(getSource()); + } + if (getDestination() != null) { + r.append(':'); + r.append(getDestination()); + } } return r.toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java index 43eaac7927..218e62c10a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java @@ -12,7 +12,9 @@ package org.eclipse.jgit.transport; import java.io.IOException; import java.text.MessageFormat; +import java.util.Collection; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -116,6 +118,12 @@ public class RemoteRefUpdate { private RefUpdate localUpdate; /** + * If set, the RemoteRefUpdate is a placeholder for the "matching" RefSpec + * to be expanded after the advertisements have been received in a push. + */ + private Collection<RefSpec> fetchSpecs; + + /** * Construct remote ref update request by providing an update specification. * Object is created with default * {@link org.eclipse.jgit.transport.RemoteRefUpdate.Status#NOT_ATTEMPTED} @@ -157,9 +165,8 @@ public class RemoteRefUpdate { * @throws java.lang.IllegalArgumentException * if some required parameter was null */ - public RemoteRefUpdate(final Repository localDb, final String srcRef, - final String remoteName, final boolean forceUpdate, - final String localName, final ObjectId expectedOldObjectId) + public RemoteRefUpdate(Repository localDb, String srcRef, String remoteName, + boolean forceUpdate, String localName, ObjectId expectedOldObjectId) throws IOException { this(localDb, srcRef, srcRef != null ? localDb.resolve(srcRef) : ObjectId.zeroId(), remoteName, forceUpdate, localName, @@ -203,9 +210,8 @@ public class RemoteRefUpdate { * @throws java.lang.IllegalArgumentException * if some required parameter was null */ - public RemoteRefUpdate(final Repository localDb, final Ref srcRef, - final String remoteName, final boolean forceUpdate, - final String localName, final ObjectId expectedOldObjectId) + public RemoteRefUpdate(Repository localDb, Ref srcRef, String remoteName, + boolean forceUpdate, String localName, ObjectId expectedOldObjectId) throws IOException { this(localDb, srcRef != null ? srcRef.getName() : null, srcRef != null ? srcRef.getObjectId() : null, remoteName, @@ -255,28 +261,41 @@ public class RemoteRefUpdate { * @throws java.lang.IllegalArgumentException * if some required parameter was null */ - public RemoteRefUpdate(final Repository localDb, final String srcRef, - final ObjectId srcId, final String remoteName, - final boolean forceUpdate, final String localName, - final ObjectId expectedOldObjectId) throws IOException { - if (remoteName == null) - throw new IllegalArgumentException(JGitText.get().remoteNameCannotBeNull); - if (srcId == null && srcRef != null) - throw new IOException(MessageFormat.format( - JGitText.get().sourceRefDoesntResolveToAnyObject, srcRef)); - - if (srcRef != null) + public RemoteRefUpdate(Repository localDb, String srcRef, ObjectId srcId, + String remoteName, boolean forceUpdate, String localName, + ObjectId expectedOldObjectId) throws IOException { + this(localDb, srcRef, srcId, remoteName, forceUpdate, localName, null, + expectedOldObjectId); + } + + private RemoteRefUpdate(Repository localDb, String srcRef, ObjectId srcId, + String remoteName, boolean forceUpdate, String localName, + Collection<RefSpec> fetchSpecs, ObjectId expectedOldObjectId) + throws IOException { + if (fetchSpecs == null) { + if (remoteName == null) { + throw new IllegalArgumentException( + JGitText.get().remoteNameCannotBeNull); + } + if (srcId == null && srcRef != null) { + throw new IOException(MessageFormat.format( + JGitText.get().sourceRefDoesntResolveToAnyObject, + srcRef)); + } + } + if (srcRef != null) { this.srcRef = srcRef; - else if (srcId != null && !srcId.equals(ObjectId.zeroId())) + } else if (srcId != null && !srcId.equals(ObjectId.zeroId())) { this.srcRef = srcId.name(); - else + } else { this.srcRef = null; - - if (srcId != null) + } + if (srcId != null) { this.newObjectId = srcId; - else + } else { this.newObjectId = ObjectId.zeroId(); - + } + this.fetchSpecs = fetchSpecs; this.remoteName = remoteName; this.forceUpdate = forceUpdate; if (localName != null && localDb != null) { @@ -292,8 +311,9 @@ public class RemoteRefUpdate { ? localUpdate.getOldObjectId() : ObjectId.zeroId(), newObjectId); - } else + } else { trackingRefUpdate = null; + } this.localDb = localDb; this.expectedOldObjectId = expectedOldObjectId; this.status = Status.NOT_ATTEMPTED; @@ -316,11 +336,57 @@ public class RemoteRefUpdate { * local tracking branch or srcRef of base object no longer can * be resolved to any object. */ - public RemoteRefUpdate(final RemoteRefUpdate base, - final ObjectId newExpectedOldObjectId) throws IOException { - this(base.localDb, base.srcRef, base.remoteName, base.forceUpdate, + public RemoteRefUpdate(RemoteRefUpdate base, + ObjectId newExpectedOldObjectId) throws IOException { + this(base.localDb, base.srcRef, base.newObjectId, base.remoteName, + base.forceUpdate, (base.trackingRefUpdate == null ? null : base.trackingRefUpdate - .getLocalName()), newExpectedOldObjectId); + .getLocalName()), + base.fetchSpecs, newExpectedOldObjectId); + } + + /** + * Creates a "placeholder" update for the "matching" RefSpec ":". + * + * @param localDb + * local repository to push from + * @param forceUpdate + * whether non-fast-forward updates shall be allowed + * @param fetchSpecs + * The fetch {@link RefSpec}s to use when this placeholder is + * expanded to determine remote tracking branch updates + */ + RemoteRefUpdate(Repository localDb, boolean forceUpdate, + @NonNull Collection<RefSpec> fetchSpecs) { + this.localDb = localDb; + this.forceUpdate = forceUpdate; + this.fetchSpecs = fetchSpecs; + this.trackingRefUpdate = null; + this.srcRef = null; + this.remoteName = null; + this.newObjectId = null; + this.status = Status.NOT_ATTEMPTED; + } + + /** + * Tells whether this {@link RemoteRefUpdate} is a placeholder for a + * "matching" {@link RefSpec}. + * + * @return {@code true} if this is a placeholder, {@code false} otherwise + * @since 6.1 + */ + public boolean isMatching() { + return fetchSpecs != null; + } + + /** + * Retrieves the fetch {@link RefSpec}s of this {@link RemoteRefUpdate}. + * + * @return the fetch {@link RefSpec}s, or {@code null} if + * {@code this.}{@link #isMatching()} {@code == false} + */ + Collection<RefSpec> getFetchSpecs() { + return fetchSpecs; } /** 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 212a4e46c1..48cacf0964 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 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 @@ -44,6 +44,14 @@ public final class SshConstants { // Config file keys + /** + * Property to control whether private keys are added to an SSH agent, if + * one is running, after having been loaded. + * + * @since 6.1 + */ + public static final String ADD_KEYS_TO_AGENT = "AddKeysToAgent"; + /** Key in an ssh config file. */ public static final String BATCH_MODE = "BatchMode"; @@ -62,6 +70,15 @@ public final class SshConstants { /** Key in an ssh config file. */ public static final String CONNECTION_ATTEMPTS = "ConnectionAttempts"; + /** + * An OpenSSH time value for the connection timeout. In OpenSSH, this + * includes everything until the end of the initial key exchange; in JGit it + * covers only the underlying TCP connect. + * + * @since 6.1 + */ + public static final String CONNECT_TIMEOUT = "ConnectTimeout"; + /** Key in an ssh config file. */ public static final String CONTROL_PATH = "ControlPath"; @@ -159,6 +176,14 @@ public final class SshConstants { /** Key in an ssh config file. */ public static final String REMOTE_FORWARD = "RemoteForward"; + /** + * (Absolute) path to a middleware library the SSH agent shall use to load + * SK (U2F) keys. + * + * @since 6.1 + */ + public static final String SECURITY_KEY_PROVIDER = "SecurityKeyProvider"; + /** Key in an ssh config file. */ public static final String SEND_ENV = "SendEnv"; @@ -229,4 +254,12 @@ public final class SshConstants { public static final String[] DEFAULT_IDENTITIES = { // ID_RSA, ID_DSA, ID_ECDSA, ID_ED25519 }; + + /** + * Name of the environment variable holding the Unix domain socket for + * communication with an SSH agent. + * + * @since 6.1 + */ + public static final String ENV_SSH_AUTH_SOCKET = "SSH_AUTH_SOCK"; } 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 696ca7cf46..51bc07cb94 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java @@ -12,6 +12,8 @@ package org.eclipse.jgit.transport; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.RefUpdate; @@ -184,9 +186,13 @@ public class TrackingRefUpdate { if (forceUpdate) sb.append(" (forced)"); sb.append(" "); - sb.append(oldObjectId == null ? "" : oldObjectId.abbreviate(7).name()); + sb.append(oldObjectId == null ? "" + : oldObjectId.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name()); sb.append(".."); - sb.append(newObjectId == null ? "" : newObjectId.abbreviate(7).name()); + sb.append(newObjectId == null ? "" + : newObjectId.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name()); sb.append("]"); return sb.toString(); } 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 5b781ac25f..0eab4434ed 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -2,7 +2,7 @@ * Copyright (C) 2008, 2009 Google Inc. * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2022 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 @@ -40,7 +40,6 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; -import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.hooks.Hooks; @@ -590,6 +589,11 @@ public abstract class Transport implements AutoCloseable { final Collection<RefSpec> procRefs = expandPushWildcardsFor(db, specs); for (RefSpec spec : procRefs) { + if (spec.isMatching()) { + result.add(new RemoteRefUpdate(db, spec.isForceUpdate(), + fetchSpecs)); + continue; + } String srcSpec = spec.getSource(); final Ref srcRef = db.findRef(srcSpec); if (srcRef != null) @@ -656,14 +660,18 @@ public abstract class Transport implements AutoCloseable { private static Collection<RefSpec> expandPushWildcardsFor( final Repository db, final Collection<RefSpec> specs) throws IOException { - final List<Ref> localRefs = db.getRefDatabase().getRefs(); final Collection<RefSpec> procRefs = new LinkedHashSet<>(); + List<Ref> localRefs = null; for (RefSpec spec : specs) { - if (spec.isWildcard()) { + if (!spec.isMatching() && spec.isWildcard()) { + if (localRefs == null) { + localRefs = db.getRefDatabase().getRefs(); + } for (Ref localRef : localRefs) { - if (spec.matchSource(localRef)) + if (spec.matchSource(localRef)) { procRefs.add(spec.expandFromSource(localRef)); + } } } else { procRefs.add(spec); @@ -672,7 +680,7 @@ public abstract class Transport implements AutoCloseable { return procRefs; } - private static String findTrackingRefName(final String remoteName, + static String findTrackingRefName(final String remoteName, final Collection<RefSpec> fetchSpecs) { // try to find matching tracking refs for (RefSpec fetchSpec : fetchSpecs) { @@ -1371,16 +1379,9 @@ public abstract class Transport implements AutoCloseable { if (toPush.isEmpty()) throw new TransportException(JGitText.get().nothingToPush); } - if (prePush != null) { - try { - prePush.setRefs(toPush); - prePush.call(); - } catch (AbortedByHookException | IOException e) { - throw new TransportException(e.getMessage(), e); - } - } - final PushProcess pushProcess = new PushProcess(this, toPush, out); + final PushProcess pushProcess = new PushProcess(this, toPush, prePush, + out); return pushProcess.execute(monitor); } 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 6cb8fe43c0..1617c5063d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -2358,7 +2358,7 @@ public class UploadPack implements Closeable { : req.getDepth() - 1; pw.setShallowPack(req.getDepth(), unshallowCommits); - @SuppressWarnings("resource") // Ownership is transferred below + // Ownership is transferred below DepthWalk.RevWalk dw = new DepthWalk.RevWalk( walk.getObjectReader(), walkDepth); dw.setDeepenSince(req.getDeepenSince()); 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 3d15ef5e72..046f395049 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2010, Google Inc. and others + * Copyright (C) 2009-2022, 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 @@ -18,11 +18,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.StringUtils; /** * Default resolver serving from the local filesystem. @@ -67,7 +67,7 @@ public class FileResolver<C> implements RepositoryResolver<C> { if (isUnreasonableName(name)) throw new RepositoryNotFoundException(name); - Repository db = exports.get(nameWithDotGit(name)); + Repository db = exports.get(StringUtils.nameWithDotGit(name)); if (db != null) { db.incrementOpen(); return db; @@ -154,7 +154,7 @@ public class FileResolver<C> implements RepositoryResolver<C> { * the repository instance. */ public void exportRepository(String name, Repository db) { - exports.put(nameWithDotGit(name), db); + exports.put(StringUtils.nameWithDotGit(name), db); } /** @@ -197,12 +197,6 @@ public class FileResolver<C> implements RepositoryResolver<C> { return false; } - private static String nameWithDotGit(String name) { - if (name.endsWith(Constants.DOT_GIT_EXT)) - return name; - return name + Constants.DOT_GIT_EXT; - } - private static boolean isUnreasonableName(String name) { if (name.length() == 0) return true; // no empty paths 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 1f614e31f6..8269666d26 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.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, 2022 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 @@ -14,6 +14,7 @@ package org.eclipse.jgit.treewalk; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -73,6 +74,7 @@ import org.eclipse.jgit.util.io.EolStreamTypeUtil; * threads. */ public class TreeWalk implements AutoCloseable, AttributesProvider { + private static final AbstractTreeIterator[] NO_TREES = {}; /** @@ -92,7 +94,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } /** - * Type of operation you want to retrieve the git attributes for. + * Type of operation you want to retrieve the git attributes for. */ private OperationType operationType = OperationType.CHECKOUT_OP; @@ -284,11 +286,20 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { AbstractTreeIterator currentHead; - /** Cached attribute for the current entry */ - private Attributes attrs = null; + /** + * Cached attributes for the current entry; per tree. Index i+1 is for tree + * i; index 0 is for the deprecated legacy behavior. + */ + private Attributes[] attrs; + + /** + * Cached attributes handler; per tree. Index i+1 is for tree i; index 0 is + * for the deprecated legacy behavior. + */ + private AttributesHandler[] attributesHandlers; - /** Cached attributes handler */ - private AttributesHandler attributesHandler; + /** Can be set to identify the tree to use for {@link #getAttributes()}. */ + private int headIndex = -1; private Config config; @@ -515,6 +526,24 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } /** + * Identifies the tree at the given index as the head tree. This is the tree + * use by default to determine attributes and EOL modes. + * + * @param index + * of the tree to use as head + * @throws IllegalArgumentException + * if the index is out of range + * @since 6.1 + */ + public void setHead(int index) { + if (index < 0 || index >= trees.length) { + throw new IllegalArgumentException("Head index " + index //$NON-NLS-1$ + + " out of range [0," + trees.length + ')'); //$NON-NLS-1$ + } + headIndex = index; + } + + /** * {@inheritDoc} * <p> * Retrieve the git attributes for the current entry. @@ -556,25 +585,51 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { */ @Override public Attributes getAttributes() { - if (attrs != null) - return attrs; + return getAttributes(headIndex); + } + /** + * Retrieves the git attributes based on the given tree. + * + * @param index + * of the tree to use as base for the attributes + * @return the attributes + * @since 6.1 + */ + public Attributes getAttributes(int index) { + int attrIndex = index + 1; + Attributes result = attrs[attrIndex]; + if (result != null) { + return result; + } if (attributesNodeProvider == null) { - // The work tree should have a AttributesNodeProvider to be able to - // retrieve the info and global attributes node throw new IllegalStateException( "The tree walk should have one AttributesNodeProvider set in order to compute the git attributes."); //$NON-NLS-1$ } try { - // Lazy create the attributesHandler on the first access of - // attributes. This requires the info, global and root - // attributes nodes - if (attributesHandler == null) { - attributesHandler = new AttributesHandler(this); + AttributesHandler handler = attributesHandlers[attrIndex]; + if (handler == null) { + if (index < 0) { + // Legacy behavior (headIndex not set, getAttributes() above + // called) + handler = new AttributesHandler(this, () -> { + return getTree(CanonicalTreeParser.class); + }); + } else { + handler = new AttributesHandler(this, () -> { + AbstractTreeIterator tree = trees[index]; + if (tree instanceof CanonicalTreeParser) { + return (CanonicalTreeParser) tree; + } + return null; + }); + } + attributesHandlers[attrIndex] = handler; } - attrs = attributesHandler.getAttributes(); - return attrs; + result = handler.getAttributes(); + attrs[attrIndex] = result; + return result; } catch (IOException e) { throw new JGitInternalException("Error while parsing attributes", //$NON-NLS-1$ e); @@ -595,11 +650,34 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { */ @Nullable public EolStreamType getEolStreamType(OperationType opType) { - if (attributesNodeProvider == null || config == null) + if (attributesNodeProvider == null || config == null) { return null; - return EolStreamTypeUtil.detectStreamType( - opType != null ? opType : operationType, - config.get(WorkingTreeOptions.KEY), getAttributes()); + } + OperationType op = opType != null ? opType : operationType; + return EolStreamTypeUtil.detectStreamType(op, + config.get(WorkingTreeOptions.KEY), getAttributes()); + } + + /** + * Get the EOL stream type of the current entry for checking out using the + * config and {@link #getAttributes()}. + * + * @param tree + * index of the tree the check-out is to be from + * @return the EOL stream type of the current entry using the config and + * {@link #getAttributes()}. Note that this method may return null + * if the {@link org.eclipse.jgit.treewalk.TreeWalk} is not based on + * a working tree + * @since 6.1 + */ + @Nullable + public EolStreamType getCheckoutEolStreamType(int tree) { + if (attributesNodeProvider == null || config == null) { + return null; + } + Attributes attr = getAttributes(tree); + return EolStreamTypeUtil.detectStreamType(OperationType.CHECKOUT_OP, + config.get(WorkingTreeOptions.KEY), attr); } /** @@ -607,7 +685,8 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { */ public void reset() { attrs = null; - attributesHandler = null; + attributesHandlers = null; + headIndex = -1; trees = NO_TREES; advance = false; depth = 0; @@ -651,7 +730,9 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { advance = false; depth = 0; - attrs = null; + attrs = new Attributes[2]; + attributesHandlers = new AttributesHandler[2]; + headIndex = -1; } /** @@ -701,7 +782,14 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { trees = r; advance = false; depth = 0; - attrs = null; + if (oldLen == newLen) { + Arrays.fill(attrs, null); + Arrays.fill(attributesHandlers, null); + } else { + attrs = new Attributes[newLen + 1]; + attributesHandlers = new AttributesHandler[newLen + 1]; + } + headIndex = -1; } /** @@ -758,6 +846,16 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { p.matchShift = 0; trees = newTrees; + if (attrs == null) { + attrs = new Attributes[n + 2]; + } else { + attrs = Arrays.copyOf(attrs, n + 2); + } + if (attributesHandlers == null) { + attributesHandlers = new AttributesHandler[n + 2]; + } else { + attributesHandlers = Arrays.copyOf(attributesHandlers, n + 2); + } return n; } @@ -800,7 +898,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } for (;;) { - attrs = null; + Arrays.fill(attrs, null); final AbstractTreeIterator t = min(); if (t.eof()) { if (depth > 0) { @@ -1255,7 +1353,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { */ public void enterSubtree() throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { - attrs = null; + Arrays.fill(attrs, null); final AbstractTreeIterator ch = currentHead; final AbstractTreeIterator[] tmp = new AbstractTreeIterator[trees.length]; for (int i = 0; i < trees.length; i++) { @@ -1374,11 +1472,12 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { /** * Inspect config and attributes to return a filtercommand applicable for - * the current path, but without expanding %f occurences + * the current path. * * @param filterCommandType * which type of filterCommand should be executed. E.g. "clean", - * "smudge" + * "smudge". For "smudge" consider using + * {{@link #getSmudgeCommand(int)} instead. * @return a filter command * @throws java.io.IOException * @since 4.2 @@ -1407,6 +1506,54 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } /** + * Inspect config and attributes to return a filtercommand applicable for + * the current path. + * + * @param index + * of the tree the item to be smudged is in + * @return a filter command + * @throws java.io.IOException + * @since 6.1 + */ + public String getSmudgeCommand(int index) + throws IOException { + return getSmudgeCommand(getAttributes(index)); + } + + /** + * Inspect config and attributes to return a filtercommand applicable for + * the current path. + * + * @param attributes + * to use + * @return a filter command + * @throws java.io.IOException + * @since 6.1 + */ + public String getSmudgeCommand(Attributes attributes) throws IOException { + if (attributes == null) { + return null; + } + Attribute f = attributes.get(Constants.ATTR_FILTER); + if (f == null) { + return null; + } + String filterValue = f.getValue(); + if (filterValue == null) { + return null; + } + + String filterCommand = getFilterCommandDefinition(filterValue, + Constants.ATTR_FILTER_TYPE_SMUDGE); + if (filterCommand == null) { + return null; + } + return filterCommand.replaceAll("%f", //$NON-NLS-1$ + Matcher.quoteReplacement( + QuotedString.BOURNE.quote((getPathString())))); + } + + /** * Get the filter command how it is defined in gitconfig. The returned * string may contain "%f" which needs to be replaced by the current path * before executing the filter command. These filter definitions are cached 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 50ce15ebc9..427eac5b53 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -2,7 +2,7 @@ * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> * Copyright (C) 2010, Matthias Sohn <matthias.sohn@sap.com> - * Copyright (C) 2012-2021, Robin Rosenberg and others + * Copyright (C) 2012, 2022, 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 @@ -387,8 +387,8 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { state.initializeReadBuffer(); final long len = e.getLength(); - InputStream filteredIs = possiblyFilteredInputStream(e, is, len, - OperationType.CHECKIN_OP); + InputStream filteredIs = possiblyFilteredInputStream(e, is, + len); return computeHash(filteredIs, canonLen); } finally { safeClose(is); @@ -400,23 +400,18 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } private InputStream possiblyFilteredInputStream(final Entry e, - final InputStream is, final long len) throws IOException { - return possiblyFilteredInputStream(e, is, len, null); - - } - - private InputStream possiblyFilteredInputStream(final Entry e, - final InputStream is, final long len, OperationType opType) + final InputStream is, final long len) throws IOException { if (getCleanFilterCommand() == null - && getEolStreamType(opType) == EolStreamType.DIRECT) { + && getEolStreamType( + OperationType.CHECKIN_OP) == EolStreamType.DIRECT) { canonLen = len; return is; } if (len <= MAXIMUM_FILE_SIZE_TO_READ_FULLY) { ByteBuffer rawbuf = IO.readWholeStream(is, (int) len); - rawbuf = filterClean(rawbuf.array(), rawbuf.limit(), opType); + rawbuf = filterClean(rawbuf.array(), rawbuf.limit()); canonLen = rawbuf.limit(); return new ByteArrayInputStream(rawbuf.array(), 0, (int) canonLen); } @@ -426,14 +421,13 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { return is; } - final InputStream lenIs = filterClean(e.openInputStream(), - opType); + final InputStream lenIs = filterClean(e.openInputStream()); try { canonLen = computeLength(lenIs); } finally { safeClose(lenIs); } - return filterClean(is, opType); + return filterClean(is); } private static void safeClose(InputStream in) { @@ -455,23 +449,20 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } } - private ByteBuffer filterClean(byte[] src, int n, OperationType opType) + private ByteBuffer filterClean(byte[] src, int n) throws IOException { InputStream in = new ByteArrayInputStream(src); try { - return IO.readWholeStream(filterClean(in, opType), n); + return IO.readWholeStream(filterClean(in), n); } finally { safeClose(in); } } - private InputStream filterClean(InputStream in) throws IOException { - return filterClean(in, null); - } - - private InputStream filterClean(InputStream in, OperationType opType) + private InputStream filterClean(InputStream in) throws IOException { - in = handleAutoCRLF(in, opType); + in = EolStreamTypeUtil.wrapInputStream(in, + getEolStreamType(OperationType.CHECKIN_OP)); String filterCommand = getCleanFilterCommand(); if (filterCommand != null) { if (FilterCommandRegistry.isRegistered(filterCommand)) { @@ -509,11 +500,6 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { return in; } - private InputStream handleAutoCRLF(InputStream in, OperationType opType) - throws IOException { - return EolStreamTypeUtil.wrapInputStream(in, getEolStreamType(opType)); - } - /** * Returns the working tree options used by this iterator. * @@ -664,7 +650,8 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { public InputStream openEntryStream() throws IOException { InputStream rawis = current().openInputStream(); if (getCleanFilterCommand() == null - && getEolStreamType() == EolStreamType.DIRECT) { + && getEolStreamType( + OperationType.CHECKIN_OP) == EolStreamType.DIRECT) { return rawis; } return filterClean(rawis); 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 ff094f6975..ae73d3feb8 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 @@ -16,6 +16,7 @@ import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; @@ -96,6 +97,9 @@ public class FS_Win32 extends FS { /** {@inheritDoc} */ @Override public Entry[] list(File directory, FileModeStrategy fileModeStrategy) { + if (!Files.isDirectory(directory.toPath(), LinkOption.NOFOLLOW_LINKS)) { + return NO_ENTRIES; + } List<Entry> result = new ArrayList<>(); FS fs = this; boolean checkExecutable = fs.supportsExecute(); 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 8ab13385e0..917add3609 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2010, Google Inc. and others + * Copyright (C) 2009-2022, 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 @@ -15,6 +15,7 @@ import java.util.Collection; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; /** * Miscellaneous string comparison utility methods. @@ -37,6 +38,10 @@ public final class StringUtils { LC[c] = (char) ('a' + (c - 'A')); } + private StringUtils() { + // Do not create instances + } + /** * Convert the input to lowercase. * <p> @@ -269,8 +274,20 @@ public final class StringUtils { return sb.toString(); } - private StringUtils() { - // Do not create instances + /** + * Appends {@link Constants#DOT_GIT_EXT} unless the given name already ends + * with that suffix. + * + * @param name + * to complete + * @return the name ending with {@link Constants#DOT_GIT_EXT} + * @since 6.1 + */ + public static String nameWithDotGit(String name) { + if (name.endsWith(Constants.DOT_GIT_EXT)) { + return name; + } + return name + Constants.DOT_GIT_EXT; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java index 4f940d77a0..2c972b578d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * 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 @@ -90,7 +90,7 @@ public class BinaryHunkInputStream extends InputStream { byte[] encoded = new byte[Base85.encodedLength(length)]; for (int i = 0; i < encoded.length; i++) { int b = in.read(); - if (b < 0 || b == '\n') { + if (b < 0 || b == '\r' || b == '\n') { throw new EOFException(MessageFormat.format( JGitText.get().binaryHunkInvalidLength, Integer.valueOf(lineNumber))); @@ -99,6 +99,10 @@ public class BinaryHunkInputStream extends InputStream { } // Must be followed by a newline; tolerate EOF. int b = in.read(); + if (b == '\r') { + // Be lenient and accept CR-LF, too. + b = in.read(); + } if (b >= 0 && b != '\n') { throw new StreamCorruptedException(MessageFormat.format( JGitText.get().binaryHunkMissingNewline, |