diff options
Diffstat (limited to 'org.eclipse.jgit')
56 files changed, 2464 insertions, 912 deletions
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 00b89a4b3d..1683f33d0c 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -1,77 +1,27 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <component id="org.eclipse.jgit" version="2"> - <resource path="src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java" type="org.eclipse.jgit.errors.NoRemoteRepositoryException"> - <filter id="1141899266"> + <resource path="src/org/eclipse/jgit/transport/AwsRequestSignerV4.java" type="org.eclipse.jgit.transport.AwsRequestSignerV4"> + <filter id="1108344834"> <message_arguments> <message_argument value="5.13"/> - <message_argument value="6.1"/> - <message_argument value="NoRemoteRepositoryException(URIish, String, Throwable)"/> - </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/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/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter"> - <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_argument value="6.3"/> + <message_argument value="org.eclipse.jgit.transport.AwsRequestSignerV4"/> </message_arguments> </filter> </resource> - <resource path="src/org/eclipse/jgit/transport/BasePackPushConnection.java" type="org.eclipse.jgit.transport.BasePackPushConnection"> - <filter id="338792546"> + <resource path="src/org/eclipse/jgit/util/HttpSupport.java" type="org.eclipse.jgit.util.HttpSupport"> + <filter id="1141899266"> <message_arguments> - <message_argument value="org.eclipse.jgit.transport.BasePackPushConnection"/> - <message_argument value="noRepository()"/> + <message_argument value="5.13"/> + <message_argument value="6.3"/> + <message_argument value="urlEncode(String, boolean)"/> </message_arguments> </filter> </resource> - <resource path="src/org/eclipse/jgit/transport/PushConfig.java" type="org.eclipse.jgit.transport.PushConfig"> - <filter id="338722907"> + <resource path="src/org/eclipse/jgit/util/Paths.java" type="org.eclipse.jgit.util.Paths"> + <filter id="337768515"> <message_arguments> - <message_argument value="org.eclipse.jgit.transport.PushConfig"/> - <message_argument value="PushConfig()"/> + <message_argument value="org.eclipse.jgit.util.Paths"/> </message_arguments> </filter> </resource> diff --git a/org.eclipse.jgit/BUILD b/org.eclipse.jgit/BUILD index 04873b0c72..e806e7d6d0 100644 --- a/org.eclipse.jgit/BUILD +++ b/org.eclipse.jgit/BUILD @@ -23,6 +23,12 @@ java_library( "//lib:javaewah", "//lib:slf4j-api", ], + javacopts = [ + "-Xep:ReferenceEquality:OFF", + "-Xep:StringEquality:OFF", + "-Xep:TypeParameterUnusedInFormals:OFF", + "-Xep:DefaultCharset:OFF", + ] ) genrule( diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 1b34912915..307a8016e3 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.2.1.qualifier +Bundle-Version: 6.3.1.qualifier Bundle-Localization: plugin Bundle-Vendor: %Bundle-Vendor Eclipse-ExtensibleAPI: true -Export-Package: org.eclipse.jgit.annotations;version="6.2.1", - org.eclipse.jgit.api;version="6.2.1"; +Export-Package: org.eclipse.jgit.annotations;version="6.3.1", + org.eclipse.jgit.api;version="6.3.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.2.1", org.eclipse.jgit.revwalk.filter, org.eclipse.jgit.blame, org.eclipse.jgit.merge", - org.eclipse.jgit.api.errors;version="6.2.1"; + org.eclipse.jgit.api.errors;version="6.3.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.errors", - org.eclipse.jgit.attributes;version="6.2.1"; + org.eclipse.jgit.attributes;version="6.3.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.treewalk", - org.eclipse.jgit.blame;version="6.2.1"; + org.eclipse.jgit.blame;version="6.3.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.2.1"; + org.eclipse.jgit.diff;version="6.3.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.attributes, org.eclipse.jgit.revwalk, @@ -42,49 +42,51 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.1", org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.treewalk, org.eclipse.jgit.util", - org.eclipse.jgit.dircache;version="6.2.1"; + org.eclipse.jgit.dircache;version="6.3.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.2.1"; + org.eclipse.jgit.errors;version="6.3.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.2.1"; + org.eclipse.jgit.events;version="6.3.1"; uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.fnmatch;version="6.2.1", - org.eclipse.jgit.gitrepo;version="6.2.1"; + org.eclipse.jgit.fnmatch;version="6.3.1", + org.eclipse.jgit.gitrepo;version="6.3.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.2.1";x-internal:=true, - org.eclipse.jgit.hooks;version="6.2.1";uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.ignore;version="6.2.1", - org.eclipse.jgit.ignore.internal;version="6.2.1"; + org.eclipse.jgit.gitrepo.internal;version="6.3.1";x-internal:=true, + org.eclipse.jgit.hooks;version="6.3.1";uses:="org.eclipse.jgit.lib", + org.eclipse.jgit.ignore;version="6.3.1", + org.eclipse.jgit.ignore.internal;version="6.3.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal;version="6.2.1"; + org.eclipse.jgit.internal;version="6.3.1"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.http.test", - org.eclipse.jgit.internal.diffmergetool;version="6.2.1"; + org.eclipse.jgit.internal.diff;version="6.3.1"; + x-friends:="org.eclipse.jgit.test", + org.eclipse.jgit.internal.diffmergetool;version="6.3.1"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.pgm.test, org.eclipse.jgit.pgm, org.eclipse.egit.ui", - org.eclipse.jgit.internal.fsck;version="6.2.1"; + org.eclipse.jgit.internal.fsck;version="6.3.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.revwalk;version="6.2.1"; + org.eclipse.jgit.internal.revwalk;version="6.3.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.storage.dfs;version="6.2.1"; + org.eclipse.jgit.internal.storage.dfs;version="6.3.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.2.1"; + org.eclipse.jgit.internal.storage.file;version="6.3.1"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.junit, org.eclipse.jgit.junit.http, @@ -93,32 +95,32 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.1", org.eclipse.jgit.pgm, org.eclipse.jgit.pgm.test, org.eclipse.jgit.ssh.apache", - org.eclipse.jgit.internal.storage.io;version="6.2.1"; + org.eclipse.jgit.internal.storage.io;version="6.3.1"; x-friends:="org.eclipse.jgit.junit, org.eclipse.jgit.test, org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.pack;version="6.2.1"; + org.eclipse.jgit.internal.storage.pack;version="6.3.1"; x-friends:="org.eclipse.jgit.junit, org.eclipse.jgit.test, org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.reftable;version="6.2.1"; + org.eclipse.jgit.internal.storage.reftable;version="6.3.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.2.1";x-internal:=true, - org.eclipse.jgit.internal.transport.connectivity;version="6.2.1"; + org.eclipse.jgit.internal.submodule;version="6.3.1";x-internal:=true, + org.eclipse.jgit.internal.transport.connectivity;version="6.3.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.transport.http;version="6.2.1"; + org.eclipse.jgit.internal.transport.http;version="6.3.1"; x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.transport.parser;version="6.2.1"; + org.eclipse.jgit.internal.transport.parser;version="6.3.1"; x-friends:="org.eclipse.jgit.http.server, org.eclipse.jgit.test", - org.eclipse.jgit.internal.transport.ssh;version="6.2.1"; + org.eclipse.jgit.internal.transport.ssh;version="6.3.1"; x-friends:="org.eclipse.jgit.ssh.apache, org.eclipse.jgit.ssh.jsch, org.eclipse.jgit.test", - org.eclipse.jgit.lib;version="6.2.1"; + org.eclipse.jgit.lib;version="6.3.1"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.util.sha1, org.eclipse.jgit.dircache, @@ -132,12 +134,12 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.1", org.eclipse.jgit.util, org.eclipse.jgit.submodule, org.eclipse.jgit.util.time", - org.eclipse.jgit.lib.internal;version="6.2.1"; + org.eclipse.jgit.lib.internal;version="6.3.1"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.pgm, org.eclipse.egit.ui", - org.eclipse.jgit.logging;version="6.2.1", - org.eclipse.jgit.merge;version="6.2.1"; + org.eclipse.jgit.logging;version="6.3.1", + org.eclipse.jgit.merge;version="6.3.1"; uses:="org.eclipse.jgit.dircache, org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, @@ -146,40 +148,40 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.1", org.eclipse.jgit.util, org.eclipse.jgit.api, org.eclipse.jgit.attributes", - org.eclipse.jgit.nls;version="6.2.1", - org.eclipse.jgit.notes;version="6.2.1"; + org.eclipse.jgit.nls;version="6.3.1", + org.eclipse.jgit.notes;version="6.3.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk, org.eclipse.jgit.merge", - org.eclipse.jgit.patch;version="6.2.1"; + org.eclipse.jgit.patch;version="6.3.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.diff", - org.eclipse.jgit.revplot;version="6.2.1"; + org.eclipse.jgit.revplot;version="6.3.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.revwalk", - org.eclipse.jgit.revwalk;version="6.2.1"; + org.eclipse.jgit.revwalk;version="6.3.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.2.1"; + org.eclipse.jgit.revwalk.filter;version="6.3.1"; uses:="org.eclipse.jgit.revwalk, org.eclipse.jgit.lib, org.eclipse.jgit.util", - org.eclipse.jgit.storage.file;version="6.2.1"; + org.eclipse.jgit.storage.file;version="6.3.1"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.util", - org.eclipse.jgit.storage.pack;version="6.2.1"; + org.eclipse.jgit.storage.pack;version="6.3.1"; uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.submodule;version="6.2.1"; + org.eclipse.jgit.submodule;version="6.3.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.2.1"; + org.eclipse.jgit.transport;version="6.3.1"; uses:="javax.crypto, org.eclipse.jgit.util.io, org.eclipse.jgit.lib, @@ -192,21 +194,21 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.1", org.eclipse.jgit.transport.resolver, org.eclipse.jgit.storage.pack, org.eclipse.jgit.errors", - org.eclipse.jgit.transport.http;version="6.2.1"; + org.eclipse.jgit.transport.http;version="6.3.1"; uses:="javax.net.ssl", - org.eclipse.jgit.transport.resolver;version="6.2.1"; + org.eclipse.jgit.transport.resolver;version="6.3.1"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.lib", - org.eclipse.jgit.treewalk;version="6.2.1"; + org.eclipse.jgit.treewalk;version="6.3.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.2.1"; + org.eclipse.jgit.treewalk.filter;version="6.3.1"; uses:="org.eclipse.jgit.treewalk", - org.eclipse.jgit.util;version="6.2.1"; + org.eclipse.jgit.util;version="6.3.1"; uses:="org.eclipse.jgit.transport, org.eclipse.jgit.hooks, org.eclipse.jgit.revwalk, @@ -219,12 +221,12 @@ Export-Package: org.eclipse.jgit.annotations;version="6.2.1", org.eclipse.jgit.treewalk, javax.net.ssl, org.eclipse.jgit.util.time", - org.eclipse.jgit.util.io;version="6.2.1"; + org.eclipse.jgit.util.io;version="6.3.1"; uses:="org.eclipse.jgit.attributes, org.eclipse.jgit.lib, org.eclipse.jgit.treewalk", - org.eclipse.jgit.util.sha1;version="6.2.1", - org.eclipse.jgit.util.time;version="6.2.1" + org.eclipse.jgit.util.sha1;version="6.3.1", + org.eclipse.jgit.util.time;version="6.3.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 4066aea51a..43e88e531c 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.2.1.qualifier -Eclipse-SourceBundle: org.eclipse.jgit;version="6.2.1.qualifier";roots="." +Bundle-Version: 6.3.1.qualifier +Eclipse-SourceBundle: org.eclipse.jgit;version="6.3.1.qualifier";roots="." diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml index e2236440a2..7e6bc0a4f4 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.2.1-SNAPSHOT</version> + <version>6.3.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 66adad5151..f3ecadd6e5 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -38,7 +38,7 @@ badIgnorePatternFull=File {0} line {1}: cannot parse pattern ''{2}'': {3} badObjectType=Bad object type: {0} badRef=Bad ref: {0}: {1} badSectionEntry=Bad section entry: {0} -badShallowLine=Bad shallow line: {0} +badShallowLine=Shallow file ''{0}'' has bad line: {1} bareRepositoryNoWorkdirAndIndex=Bare Repository has neither a working tree, nor an index base85invalidChar=Invalid base-85 character: 0x{0} base85length=Base-85 encoded data must have a length that is a multiple of 5 @@ -237,6 +237,8 @@ deletedOrphanInPackDir=Deleted orphaned file {} deleteRequiresZeroNewId=Delete requires new ID to be zero deleteTagUnexpectedResult=Delete tag returned unexpected result {0} deletingNotSupported=Deleting {0} not supported. +depthMustBeAt1=Depth must be >= 1 +depthWithUnshallow=Depth and unshallow can\'t be used together destinationIsNotAWildcard=Destination is not a wildcard. detachedHeadDetected=HEAD is detached diffToolNotGivenError=No diff tool provided and no defaults configured. @@ -518,6 +520,7 @@ notFound=not found. nothingToFetch=Nothing to fetch. nothingToPush=Nothing to push. notMergedExceptionMessage=Branch was not deleted as it has not been merged yet; use the force option to delete it anyway +notShallowedUnshallow=The server sent a unshallow for a commit that wasn''t marked as shallow: {0} noXMLParserAvailable=No XML parser available. objectAtHasBadZlibStream=Object at {0} in {1} has bad zlib stream objectIsCorrupt=Object {0} is corrupt: {1} @@ -587,6 +590,7 @@ pushNotPermitted=push not permitted pushOptionsNotSupported=Push options not supported; received {0} rawLogMessageDoesNotParseAsLogEntry=Raw log message does not parse as log entry readConfigFailed=Reading config file ''{0}'' failed +readShallowFailed=Reading shallow file ''{0}'' failed readFileStoreAttributesFailed=Reading FileStore attributes from user config failed readerIsRequired=Reader is required readingObjectsFromLocalRepositoryFailed=reading objects from local repository failed: {0} @@ -662,6 +666,7 @@ serviceNotEnabledNoName=Service not enabled serviceNotPermitted={1} not permitted on ''{0}'' sha1CollisionDetected=SHA-1 collision detected on {0} shallowCommitsAlreadyInitialized=Shallow commits have already been initialized +shallowNotSupported=The server does not support shallow shallowPacksRequireDepthWalk=Shallow packs require a DepthWalk shortCompressedStreamAt=Short compressed stream at {0} shortReadOfBlock=Short read of block. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java index 583767af3f..e7f40d811b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java @@ -9,7 +9,6 @@ */ package org.eclipse.jgit.api; -import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -25,7 +24,6 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.zip.InflaterInputStream; - import org.eclipse.jgit.api.errors.FilterFailedException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.PatchApplyException; @@ -38,15 +36,11 @@ import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheIterator; -import org.eclipse.jgit.errors.LargeObjectException; -import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectLoader; -import org.eclipse.jgit.lib.ObjectStream; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.patch.BinaryHunk; import org.eclipse.jgit.patch.FileHeader; @@ -162,13 +156,14 @@ public class ApplyCommand extends GitCommand<ApplyResult> { JGitText.get().renameFileFailed, f, dest), e); } apply(repository, fh.getOldPath(), cache, dest, fh); + r.addUpdatedFile(dest); break; case COPY: - f = getFile(fh.getOldPath(), false); - File target = getFile(fh.getNewPath(), false); - FileUtils.mkdirs(target.getParentFile(), true); - Files.copy(f.toPath(), target.toPath()); - apply(repository, fh.getOldPath(), cache, target, fh); + File src = getFile(fh.getOldPath(), false); + f = getFile(fh.getNewPath(), false); + FileUtils.mkdirs(f.getParentFile(), true); + Files.copy(src.toPath(), f.toPath()); + apply(repository, fh.getOldPath(), cache, f, fh); } r.addUpdatedFile(f); } @@ -355,60 +350,6 @@ public class ApplyCommand extends GitCommand<ApplyResult> { return result.getStdout().openInputStreamWithAutoDestroy(); } - /** - * Something that can supply an {@link InputStream}. - */ - private interface StreamSupplier { - InputStream load() throws IOException; - } - - /** - * We write the patch result to a {@link TemporaryBuffer} and then use - * {@link DirCacheCheckout}.getContent() to run the result through the CR-LF - * and smudge filters. DirCacheCheckout needs an ObjectLoader, not a - * TemporaryBuffer, so this class bridges between the two, making any Stream - * provided by a {@link StreamSupplier} look like an ordinary git blob to - * DirCacheCheckout. - */ - private static class StreamLoader extends ObjectLoader { - - private StreamSupplier data; - - private long size; - - StreamLoader(StreamSupplier data, long length) { - this.data = data; - this.size = length; - } - - @Override - public int getType() { - return Constants.OBJ_BLOB; - } - - @Override - public long getSize() { - return size; - } - - @Override - public boolean isLarge() { - return true; - } - - @Override - public byte[] getCachedBytes() throws LargeObjectException { - throw new LargeObjectException(); - } - - @Override - public ObjectStream openStream() - throws MissingObjectException, IOException { - return new ObjectStream.Filter(getType(), getSize(), - new BufferedInputStream(data.load())); - } - } - private void initHash(SHA1 hash, long size) { hash.update(Constants.encodedTypeString(Constants.OBJ_BLOB)); hash.update((byte) ' '); @@ -456,7 +397,7 @@ public class ApplyCommand extends GitCommand<ApplyResult> { } private void applyBinary(Repository repository, String path, File f, - FileHeader fh, StreamSupplier loader, ObjectId id, + FileHeader fh, DirCacheCheckout.StreamSupplier loader, ObjectId id, CheckoutMetadata checkOut) throws PatchApplyException, IOException { if (!fh.getOldId().isComplete() || !fh.getNewId().isComplete()) { @@ -488,8 +429,7 @@ public class ApplyCommand extends GitCommand<ApplyResult> { hunk.getBuffer(), start, length))))) { DirCacheCheckout.getContent(repository, path, checkOut, - new StreamLoader(() -> inflated, hunk.getSize()), - null, out); + () -> inflated, null, out); if (!fh.getNewId().toObjectId().equals(hash.toObjectId())) { throw new PatchApplyException(MessageFormat.format( JGitText.get().applyBinaryResultOidWrong, @@ -520,8 +460,7 @@ public class ApplyCommand extends GitCommand<ApplyResult> { SHA1InputStream hashed = new SHA1InputStream(hash, input)) { DirCacheCheckout.getContent(repository, path, checkOut, - new StreamLoader(() -> hashed, finalSize), null, - out); + () -> hashed, null, out); if (!fh.getNewId().toObjectId() .equals(hash.toObjectId())) { throw new PatchApplyException(MessageFormat.format( @@ -689,9 +628,7 @@ public class ApplyCommand extends GitCommand<ApplyResult> { } try (OutputStream output = new FileOutputStream(f)) { DirCacheCheckout.getContent(repository, path, checkOut, - new StreamLoader(buffer::openInputStream, - buffer.length()), - null, output); + buffer::openInputStream, null, output); } } finally { buffer.destroy(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java index 847ab0a9a8..7319ff4b2f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java @@ -55,6 +55,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; /** @@ -411,6 +412,8 @@ public class CheckoutCommand extends GitCommand<Ref> { protected CheckoutCommand checkoutPaths() throws IOException, RefNotFoundException { actuallyModifiedPaths = new HashSet<>(); + WorkingTreeOptions options = repo.getConfig() + .get(WorkingTreeOptions.KEY); DirCache dc = repo.lockDirCache(); try (RevWalk revWalk = new RevWalk(repo); TreeWalk treeWalk = new TreeWalk(repo, @@ -419,10 +422,10 @@ public class CheckoutCommand extends GitCommand<Ref> { if (!checkoutAllPaths) treeWalk.setFilter(PathFilterGroup.createFromStrings(paths)); if (isCheckoutIndex()) - checkoutPathsFromIndex(treeWalk, dc); + checkoutPathsFromIndex(treeWalk, dc, options); else { RevCommit commit = revWalk.parseCommit(getStartPointObjectId()); - checkoutPathsFromCommit(treeWalk, dc, commit); + checkoutPathsFromCommit(treeWalk, dc, commit, options); } } finally { try { @@ -439,7 +442,8 @@ public class CheckoutCommand extends GitCommand<Ref> { return this; } - private void checkoutPathsFromIndex(TreeWalk treeWalk, DirCache dc) + private void checkoutPathsFromIndex(TreeWalk treeWalk, DirCache dc, + WorkingTreeOptions options) throws IOException { DirCacheIterator dci = new DirCacheIterator(dc); treeWalk.addTree(dci); @@ -465,8 +469,9 @@ public class CheckoutCommand extends GitCommand<Ref> { if (stage > DirCacheEntry.STAGE_0) { if (checkoutStage != null) { if (stage == checkoutStage.number) { - checkoutPath(ent, r, new CheckoutMetadata( - eolStreamType, filterCommand)); + checkoutPath(ent, r, options, + new CheckoutMetadata(eolStreamType, + filterCommand)); actuallyModifiedPaths.add(path); } } else { @@ -475,8 +480,9 @@ public class CheckoutCommand extends GitCommand<Ref> { throw new JGitInternalException(e.getMessage(), e); } } else { - checkoutPath(ent, r, new CheckoutMetadata(eolStreamType, - filterCommand)); + checkoutPath(ent, r, options, + new CheckoutMetadata(eolStreamType, + filterCommand)); actuallyModifiedPaths.add(path); } } @@ -488,7 +494,7 @@ public class CheckoutCommand extends GitCommand<Ref> { } private void checkoutPathsFromCommit(TreeWalk treeWalk, DirCache dc, - RevCommit commit) throws IOException { + RevCommit commit, WorkingTreeOptions options) throws IOException { treeWalk.addTree(commit.getTree()); final ObjectReader r = treeWalk.getObjectReader(); DirCacheEditor editor = dc.editor(); @@ -510,7 +516,7 @@ public class CheckoutCommand extends GitCommand<Ref> { } ent.setObjectId(blobId); ent.setFileMode(mode); - checkoutPath(ent, r, + checkoutPath(ent, r, options, new CheckoutMetadata(eolStreamType, filterCommand)); actuallyModifiedPaths.add(path); } @@ -520,10 +526,10 @@ public class CheckoutCommand extends GitCommand<Ref> { } private void checkoutPath(DirCacheEntry entry, ObjectReader reader, - CheckoutMetadata checkoutMetadata) { + WorkingTreeOptions options, CheckoutMetadata checkoutMetadata) { try { DirCacheCheckout.checkoutEntry(repo, entry, reader, true, - checkoutMetadata); + checkoutMetadata, options); } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().checkoutConflictWithFile, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java index 69272b7547..36ca97d694 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java @@ -25,6 +25,7 @@ import org.eclipse.jgit.events.WorkingTreeModifiedEvent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.Paths; /** * Remove untracked files from the working tree @@ -91,15 +92,16 @@ public class CleanCommand extends GitCommand<Set<String>> { Set<String> notIgnoredDirs = filterIgnorePaths(untrackedDirs, status.getIgnoredNotInIndex(), false); - for (String file : notIgnoredFiles) + for (String file : notIgnoredFiles) { if (paths.isEmpty() || paths.contains(file)) { files = cleanPath(file, files); } - - for (String dir : notIgnoredDirs) + } + for (String dir : notIgnoredDirs) { if (paths.isEmpty() || paths.contains(dir)) { files = cleanPath(dir, files); } + } } catch (IOException e) { throw new JGitInternalException(e.getMessage(), e); } finally { @@ -142,14 +144,14 @@ public class CleanCommand extends GitCommand<Set<String>> { FileUtils.delete(curFile, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); } - inFiles.add(path + "/"); //$NON-NLS-1$ + inFiles.add(path + '/'); } } else { if (!dryRun) { FileUtils.delete(curFile, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); } - inFiles.add(path + "/"); //$NON-NLS-1$ + inFiles.add(path + '/'); } } } else { @@ -166,14 +168,16 @@ public class CleanCommand extends GitCommand<Set<String>> { Set<String> ignoredNotInIndex, boolean exact) { if (ignore) { Set<String> filtered = new TreeSet<>(inputPaths); - for (String path : inputPaths) - for (String ignored : ignoredNotInIndex) + for (String path : inputPaths) { + for (String ignored : ignoredNotInIndex) { if ((exact && path.equals(ignored)) - || (!exact && path.startsWith(ignored))) { + || (!exact + && Paths.isEqualOrPrefix(ignored, path))) { filtered.remove(path); break; } - + } + } return filtered; } return inputPaths; @@ -182,14 +186,14 @@ public class CleanCommand extends GitCommand<Set<String>> { private Set<String> filterFolders(Set<String> untracked, Set<String> untrackedFolders) { Set<String> filtered = new TreeSet<>(untracked); - for (String file : untracked) - for (String folder : untrackedFolders) - if (file.startsWith(folder)) { + for (String file : untracked) { + for (String folder : untrackedFolders) { + if (Paths.isEqualOrPrefix(folder, file)) { filtered.remove(file); break; } - - + } + } return filtered; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java index 3aa711455b..1f979a938c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, 2017 Chris Aniszczyk <caniszczyk@gmail.com> and others + * Copyright (C) 2011, 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 @@ -13,10 +13,13 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.text.MessageFormat; +import java.time.Instant; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRemoteException; @@ -91,6 +94,12 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { private TagOpt tagOption; + private Integer depth; + + private Instant shallowSince; + + private List<String> shallowExcludes = new ArrayList<>(); + private enum FETCH_TYPE { MULTIPLE_BRANCHES, ALL_BRANCHES, MIRROR } @@ -306,6 +315,11 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { fetchAll ? TagOpt.FETCH_TAGS : TagOpt.AUTO_FOLLOW); } command.setInitialBranch(branch); + if (depth != null) { + command.setDepth(depth.intValue()); + } + command.setShallowSince(shallowSince); + command.setShallowExcludes(shallowExcludes); configure(command); return command.call(); @@ -737,6 +751,82 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { return this; } + /** + * Creates a shallow clone with a history truncated to the specified number + * of commits. + * + * @param depth + * the depth + * @return {@code this} + * + * @since 6.3 + */ + public CloneCommand setDepth(int depth) { + if (depth < 1) { + throw new IllegalArgumentException(JGitText.get().depthMustBeAt1); + } + this.depth = Integer.valueOf(depth); + return this; + } + + /** + * Creates a shallow clone with a history after the specified time. + * + * @param shallowSince + * the timestammp; must not be {@code null} + * @return {@code this} + * + * @since 6.3 + */ + public CloneCommand setShallowSince(@NonNull OffsetDateTime shallowSince) { + this.shallowSince = shallowSince.toInstant(); + return this; + } + + /** + * Creates a shallow clone with a history after the specified time. + * + * @param shallowSince + * the timestammp; must not be {@code null} + * @return {@code this} + * + * @since 6.3 + */ + public CloneCommand setShallowSince(@NonNull Instant shallowSince) { + this.shallowSince = shallowSince; + return this; + } + + /** + * Creates a shallow clone with a history, excluding commits reachable from + * a specified remote branch or tag. + * + * @param shallowExclude + * the ref or commit; must not be {@code null} + * @return {@code this} + * + * @since 6.3 + */ + public CloneCommand addShallowExclude(@NonNull String shallowExclude) { + shallowExcludes.add(shallowExclude); + return this; + } + + /** + * Creates a shallow clone with a history, excluding commits reachable from + * a specified remote branch or tag. + * + * @param shallowExclude + * the commit; must not be {@code null} + * @return {@code this} + * + * @since 6.3 + */ + public CloneCommand addShallowExclude(@NonNull ObjectId shallowExclude) { + shallowExcludes.add(shallowExclude.name()); + return this; + } + private static void validateDirs(File directory, File gitDir, boolean bare) throws IllegalStateException { if (directory != null) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java index 90c1515b06..84bee36204 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.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 @@ -14,10 +14,13 @@ import static java.util.stream.Collectors.toList; import java.io.IOException; import java.net.URISyntaxException; import java.text.MessageFormat; +import java.time.Instant; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidConfigurationException; @@ -76,6 +79,14 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { private String initialBranch; + private Integer depth; + + private Instant deepenSince; + + private List<String> shallowExcludes = new ArrayList<>(); + + private boolean unshallow; + /** * Callback for status of fetch operation. * @@ -156,11 +167,9 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { walk.getPath()); // When the fetch mode is "yes" we always fetch. When the - // mode - // is "on demand", we only fetch if the submodule's revision - // was - // updated to an object that is not currently present in the - // submodule. + // mode is "on demand", we only fetch if the submodule's + // revision was updated to an object that is not currently + // present in the submodule. if ((recurseMode == FetchRecurseSubmodulesMode.ON_DEMAND && !submoduleRepo.getObjectDatabase() .has(walk.getObjectId())) @@ -209,6 +218,17 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { if (tagOption != null) transport.setTagOpt(tagOption); transport.setFetchThin(thin); + if (depth != null) { + transport.setDepth(depth); + } + if (unshallow) { + if (depth != null) { + throw new IllegalStateException(JGitText.get().depthWithUnshallow); + } + transport.setDepth(Constants.INFINITE_DEPTH); + } + transport.setDeepenSince(deepenSince); + transport.setDeepenNots(shallowExcludes); configure(transport); FetchResult result = transport.fetch(monitor, applyOptions(refSpecs), initialBranch); @@ -542,4 +562,105 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { this.isForceUpdate = force; return this; } + + /** + * Limits fetching to the specified number of commits from the tip of each + * remote branch history. + * + * @param depth + * the depth + * @return {@code this} + * + * @since 6.3 + */ + public FetchCommand setDepth(int depth) { + if (depth < 1) { + throw new IllegalArgumentException(JGitText.get().depthMustBeAt1); + } + this.depth = Integer.valueOf(depth); + return this; + } + + /** + * Deepens or shortens the history of a shallow repository to include all + * reachable commits after a specified time. + * + * @param shallowSince + * the timestammp; must not be {@code null} + * @return {@code this} + * + * @since 6.3 + */ + public FetchCommand setShallowSince(@NonNull OffsetDateTime shallowSince) { + this.deepenSince = shallowSince.toInstant(); + return this; + } + + /** + * Deepens or shortens the history of a shallow repository to include all + * reachable commits after a specified time. + * + * @param shallowSince + * the timestammp; must not be {@code null} + * @return {@code this} + * + * @since 6.3 + */ + public FetchCommand setShallowSince(@NonNull Instant shallowSince) { + this.deepenSince = shallowSince; + return this; + } + + /** + * Deepens or shortens the history of a shallow repository to exclude + * commits reachable from a specified remote branch or tag. + * + * @param shallowExclude + * the ref or commit; must not be {@code null} + * @return {@code this} + * + * @since 6.3 + */ + public FetchCommand addShallowExclude(@NonNull String shallowExclude) { + shallowExcludes.add(shallowExclude); + return this; + } + + /** + * Creates a shallow clone with a history, excluding commits reachable from + * a specified remote branch or tag. + * + * @param shallowExclude + * the commit; must not be {@code null} + * @return {@code this} + * + * @since 6.3 + */ + public FetchCommand addShallowExclude(@NonNull ObjectId shallowExclude) { + shallowExcludes.add(shallowExclude.name()); + return this; + } + + /** + * If the source repository is complete, converts a shallow repository to a + * complete one, removing all the limitations imposed by shallow + * repositories. + * + * If the source repository is shallow, fetches as much as possible so that + * the current repository has the same history as the source repository. + * + * @param unshallow + * whether to unshallow or not + * @return {@code this} + * + * @since 6.3 + */ + public FetchCommand setUnshallow(boolean unshallow) { + this.unshallow = unshallow; + return this; + } + + void setShallowExcludes(List<String> shallowExcludes) { + this.shallowExcludes = shallowExcludes; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java index 1004d3e50f..17036a9cd3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java @@ -48,6 +48,7 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; /** * Command class to apply a stashed commit. @@ -382,6 +383,8 @@ public class StashApplyCommand extends GitCommand<ObjectId> { private void resetUntracked(RevTree tree) throws CheckoutConflictException, IOException { Set<String> actuallyModifiedPaths = new HashSet<>(); + WorkingTreeOptions options = repo.getConfig() + .get(WorkingTreeOptions.KEY); // TODO maybe NameConflictTreeWalk ? try (TreeWalk walk = new TreeWalk(repo)) { walk.addTree(tree); @@ -413,7 +416,7 @@ public class StashApplyCommand extends GitCommand<ObjectId> { } } - checkoutPath(entry, reader, + checkoutPath(entry, reader, options, new CheckoutMetadata(eolStreamType, null)); actuallyModifiedPaths.add(entry.getPathString()); } @@ -426,10 +429,10 @@ public class StashApplyCommand extends GitCommand<ObjectId> { } private void checkoutPath(DirCacheEntry entry, ObjectReader reader, - CheckoutMetadata checkoutMetadata) { + WorkingTreeOptions options, CheckoutMetadata checkoutMetadata) { try { DirCacheCheckout.checkoutEntry(repo, entry, reader, true, - checkoutMetadata); + checkoutMetadata, options); } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().checkoutConflictWithFile, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java index 10d77528f6..77967df2e5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java @@ -41,6 +41,7 @@ import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.diff.FilteredRenameDetector; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.MutableObjectId; @@ -1109,9 +1110,10 @@ public class BlameGenerator implements AutoCloseable { treeWalk.setFilter(TreeFilter.ANY_DIFF); treeWalk.reset(parent.getTree(), commit.getTree()); - renameDetector.reset(); - renameDetector.addAll(DiffEntry.scan(treeWalk)); - for (DiffEntry ent : renameDetector.compute()) { + List<DiffEntry> diffs = DiffEntry.scan(treeWalk); + FilteredRenameDetector filteredRenameDetector = new FilteredRenameDetector( + renameDetector); + for (DiffEntry ent : filteredRenameDetector.compute(diffs, path)) { if (isRename(ent) && ent.getNewPath().equals(path.getPath())) return ent; } 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 f6fc393c45..1fb81b71e9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -21,6 +21,7 @@ import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.nio.file.StandardCopyOption; import java.text.MessageFormat; @@ -143,6 +144,8 @@ public class DirCacheCheckout { private boolean performingCheckout; + private WorkingTreeOptions options; + private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; /** @@ -362,9 +365,12 @@ public class DirCacheCheckout { * Processing an entry in the context of {@link #prescanOneTree()} when only * one tree is given * - * @param m the tree to merge - * @param i the index - * @param f the working tree + * @param m + * the tree to merge + * @param i + * the index + * @param f + * the working tree * @throws IOException */ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i, @@ -489,6 +495,8 @@ public class DirCacheCheckout { MissingObjectException, IncorrectObjectTypeException, CheckoutConflictException, IndexWriteException, CanceledException { toBeDeleted.clear(); + options = repo.getConfig() + .get(WorkingTreeOptions.KEY); try (ObjectReader objectReader = repo.getObjectDatabase().newReader()) { if (headCommitTree != null) preScanTwoTrees(); @@ -558,7 +566,8 @@ public class DirCacheCheckout { if (FileMode.GITLINK.equals(entry.getRawMode())) { checkoutGitlink(path, entry); } else { - checkoutEntry(repo, entry, objectReader, false, meta); + checkoutEntry(repo, entry, objectReader, false, meta, + options); } e = null; @@ -594,7 +603,7 @@ public class DirCacheCheckout { } if (entry.getStage() == DirCacheEntry.STAGE_3) { checkoutEntry(repo, entry, objectReader, false, - null); + null, options); break; } ++entryIdx; @@ -1226,7 +1235,7 @@ public class DirCacheCheckout { checkoutEntry(repo, e, walk.getObjectReader(), false, new CheckoutMetadata(walk.getEolStreamType(CHECKOUT_OP), walk.getFilterCommand( - Constants.ATTR_FILTER_TYPE_SMUDGE))); + Constants.ATTR_FILTER_TYPE_SMUDGE)), options); } } } @@ -1392,12 +1401,6 @@ public class DirCacheCheckout { * cannot be renamed to file or link without deleting it recursively. * </p> * - * <p> - * TODO: this method works directly on File IO, we may need another - * abstraction (like WorkingTreeIterator). This way we could tell e.g. - * Eclipse that Files in the workspace got changed - * </p> - * * @param repo * repository managing the destination work tree. * @param entry @@ -1407,15 +1410,16 @@ public class DirCacheCheckout { * @throws java.io.IOException * @since 3.6 * @deprecated since 5.1, use - * {@link #checkoutEntry(Repository, DirCacheEntry, ObjectReader, boolean, CheckoutMetadata)} + * {@link #checkoutEntry(Repository, DirCacheEntry, ObjectReader, boolean, CheckoutMetadata, WorkingTreeOptions)} * instead */ @Deprecated public static void checkoutEntry(Repository repo, DirCacheEntry entry, ObjectReader or) throws IOException { - checkoutEntry(repo, entry, or, false, null); + checkoutEntry(repo, entry, or, false, null, null); } + /** * Updates the file in the working tree with content and mode from an entry * in the index. The new content is first written to a new temporary file in @@ -1428,10 +1432,45 @@ public class DirCacheCheckout { * recursively, independently if has any content. * </p> * + * @param repo + * repository managing the destination work tree. + * @param entry + * the entry containing new mode and content + * @param or + * object reader to use for checkout + * @param deleteRecursive + * true to recursively delete final path if it exists on the file + * system + * @param checkoutMetadata + * containing + * <ul> + * <li>smudgeFilterCommand to be run for smudging the entry to be + * checked out</li> + * <li>eolStreamType used for stream conversion</li> + * </ul> + * @throws java.io.IOException + * @since 4.2 + * @deprecated since 6.3, use + * {@link #checkoutEntry(Repository, DirCacheEntry, ObjectReader, boolean, CheckoutMetadata, WorkingTreeOptions)} + * instead + */ + @Deprecated + public static void checkoutEntry(Repository repo, DirCacheEntry entry, + ObjectReader or, boolean deleteRecursive, + CheckoutMetadata checkoutMetadata) throws IOException { + checkoutEntry(repo, entry, or, deleteRecursive, checkoutMetadata, null); + } + + /** + * Updates the file in the working tree with content and mode from an entry + * in the index. The new content is first written to a new temporary file in + * the same directory as the real file. Then that new file is renamed to the + * final filename. + * * <p> - * TODO: this method works directly on File IO, we may need another - * abstraction (like WorkingTreeIterator). This way we could tell e.g. - * Eclipse that Files in the workspace got changed + * <b>Note:</b> if the entry path on local file system exists as a file, it + * will be deleted and if it exists as a directory, it will be deleted + * recursively, independently if has any content. * </p> * * @param repo @@ -1450,14 +1489,19 @@ public class DirCacheCheckout { * checked out</li> * <li>eolStreamType used for stream conversion</li> * </ul> + * @param options + * {@link WorkingTreeOptions} that are effective; if {@code null} + * they are loaded from the repository config * @throws java.io.IOException - * @since 4.2 + * @since 6.3 */ public static void checkoutEntry(Repository repo, DirCacheEntry entry, ObjectReader or, boolean deleteRecursive, - CheckoutMetadata checkoutMetadata) throws IOException { - if (checkoutMetadata == null) + CheckoutMetadata checkoutMetadata, WorkingTreeOptions options) + throws IOException { + if (checkoutMetadata == null) { checkoutMetadata = CheckoutMetadata.EMPTY; + } ObjectLoader ol = or.open(entry.getObjectId()); File f = new File(repo.getWorkTree(), entry.getPathString()); File parentDir = f.getParentFile(); @@ -1466,7 +1510,8 @@ public class DirCacheCheckout { } FileUtils.mkdirs(parentDir, true); FS fs = repo.getFS(); - WorkingTreeOptions opt = repo.getConfig().get(WorkingTreeOptions.KEY); + WorkingTreeOptions opt = options != null ? options + : repo.getConfig().get(WorkingTreeOptions.KEY); if (entry.getFileMode() == FileMode.SYMLINK && opt.getSymLinks() == SymLinks.TRUE) { byte[] bytes = ol.getBytes(); @@ -1561,6 +1606,60 @@ public class DirCacheCheckout { CheckoutMetadata checkoutMetadata, ObjectLoader ol, WorkingTreeOptions opt, OutputStream os) throws IOException { + getContent(repo, path, checkoutMetadata, ol::openStream, opt, os); + } + + + /** + * Something that can supply an {@link InputStream}. + * + * @since 6.3 + */ + public interface StreamSupplier { + + /** + * Loads the input stream. + * + * @return the loaded stream + * @throws IOException + * if any reading error occurs + */ + InputStream load() throws IOException; + } + + /** + * Return filtered content for blob contents. EOL handling and smudge-filter + * handling are applied in the same way as it would be done during a + * checkout. + * + * @param repo + * the repository + * @param path + * the path used to determine the correct filters for the object + * @param checkoutMetadata + * containing + * <ul> + * <li>smudgeFilterCommand to be run for smudging the object</li> + * <li>eolStreamType used for stream conversion (can be + * null)</li> + * </ul> + * @param inputStream + * A supplier for the raw content of the object. Each call should + * yield a fresh stream of the same object. + * @param opt + * the working tree options where only 'core.autocrlf' is used + * for EOL handling if 'checkoutMetadata.eolStreamType' is not + * valid + * @param os + * the output stream the filtered content is written to. The + * caller is responsible to close the stream. + * @throws IOException + * @since 6.3 + */ + public static void getContent(Repository repo, String path, + CheckoutMetadata checkoutMetadata, StreamSupplier inputStream, + WorkingTreeOptions opt, OutputStream os) + throws IOException { EolStreamType nonNullEolStreamType; if (checkoutMetadata.eolStreamType != null) { nonNullEolStreamType = checkoutMetadata.eolStreamType; @@ -1574,21 +1673,23 @@ public class DirCacheCheckout { if (checkoutMetadata.smudgeFilterCommand != null) { if (FilterCommandRegistry .isRegistered(checkoutMetadata.smudgeFilterCommand)) { - runBuiltinFilterCommand(repo, checkoutMetadata, ol, + runBuiltinFilterCommand(repo, checkoutMetadata, inputStream, channel); } else { - runExternalFilterCommand(repo, path, checkoutMetadata, ol, + runExternalFilterCommand(repo, path, checkoutMetadata, inputStream, channel); } } else { - ol.copyTo(channel); + try (InputStream in = inputStream.load()) { + in.transferTo(channel); + } } } } // Run an external filter command private static void runExternalFilterCommand(Repository repo, String path, - CheckoutMetadata checkoutMetadata, ObjectLoader ol, + CheckoutMetadata checkoutMetadata, StreamSupplier inputStream, OutputStream channel) throws IOException { FS fs = repo.getFS(); ProcessBuilder filterProcessBuilder = fs.runInShell( @@ -1600,7 +1701,9 @@ public class DirCacheCheckout { int rc; try { // TODO: wire correctly with AUTOCRLF - result = fs.execute(filterProcessBuilder, ol.openStream()); + try (InputStream in = inputStream.load()) { + result = fs.execute(filterProcessBuilder, in); + } rc = result.getRc(); if (rc == 0) { result.getStdout().writeTo(channel, @@ -1621,31 +1724,35 @@ public class DirCacheCheckout { // Run a builtin filter command private static void runBuiltinFilterCommand(Repository repo, - CheckoutMetadata checkoutMetadata, ObjectLoader ol, + CheckoutMetadata checkoutMetadata, StreamSupplier inputStream, OutputStream channel) throws MissingObjectException, IOException { boolean isMandatory = repo.getConfig().getBoolean( ConfigConstants.CONFIG_FILTER_SECTION, ConfigConstants.CONFIG_SECTION_LFS, ConfigConstants.CONFIG_KEY_REQUIRED, false); FilterCommand command = null; - try { - command = FilterCommandRegistry.createFilterCommand( - checkoutMetadata.smudgeFilterCommand, repo, ol.openStream(), - channel); - } catch (IOException e) { - LOG.error(JGitText.get().failedToDetermineFilterDefinition, e); - if (!isMandatory) { - // In case an IOException occurred during creating of the - // command then proceed as if there would not have been a - // builtin filter (only if the filter is not mandatory). - ol.copyTo(channel); - } else { - throw e; + try (InputStream in = inputStream.load()) { + try { + command = FilterCommandRegistry.createFilterCommand( + checkoutMetadata.smudgeFilterCommand, repo, in, + channel); + } catch (IOException e) { + LOG.error(JGitText.get().failedToDetermineFilterDefinition, e); + if (!isMandatory) { + // In case an IOException occurred during creating of the + // command then proceed as if there would not have been a + // builtin filter (only if the filter is not mandatory). + try (InputStream again = inputStream.load()) { + again.transferTo(channel); + } + } else { + throw e; + } } - } - if (command != null) { - while (command.run() != -1) { - // loop as long as command.run() tells there is work to do + if (command != null) { + while (command.run() != -1) { + // loop as long as command.run() tells there is work to do + } } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java index e6626aece3..f63cc6d644 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java @@ -23,6 +23,7 @@ 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.dircache.InvalidPathException; import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException; import org.eclipse.jgit.gitrepo.RepoCommand.RemoteFile; import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader; @@ -138,7 +139,7 @@ class BareSuperprojectWriter { } // In the last try, just propagate the exceptions return commitTreeOnCurrentTip(inserter, rw, treeId); - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | InvalidPathException e) { throw new ManifestErrorException(e); } } 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 efdb8e42e3..964debcccb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -265,6 +265,8 @@ public class JGitText extends TranslationBundle { /***/ public String deleteRequiresZeroNewId; /***/ public String deleteTagUnexpectedResult; /***/ public String deletingNotSupported; + /***/ public String depthMustBeAt1; + /***/ public String depthWithUnshallow; /***/ public String destinationIsNotAWildcard; /***/ public String detachedHeadDetected; /***/ public String diffToolNotGivenError; @@ -546,6 +548,7 @@ public class JGitText extends TranslationBundle { /***/ public String nothingToFetch; /***/ public String nothingToPush; /***/ public String notMergedExceptionMessage; + /***/ public String notShallowedUnshallow; /***/ public String noXMLParserAvailable; /***/ public String objectAtHasBadZlibStream; /***/ public String objectIsCorrupt; @@ -615,6 +618,7 @@ public class JGitText extends TranslationBundle { /***/ public String pushOptionsNotSupported; /***/ public String rawLogMessageDoesNotParseAsLogEntry; /***/ public String readConfigFailed; + /***/ public String readShallowFailed; /***/ public String readFileStoreAttributesFailed; /***/ public String readerIsRequired; /***/ public String readingObjectsFromLocalRepositoryFailed; @@ -690,6 +694,7 @@ public class JGitText extends TranslationBundle { /***/ public String serviceNotPermitted; /***/ public String sha1CollisionDetected; /***/ public String shallowCommitsAlreadyInitialized; + /***/ public String shallowNotSupported; /***/ public String shallowPacksRequireDepthWalk; /***/ public String shortCompressedStreamAt; /***/ public String shortReadOfBlock; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java new file mode 100644 index 0000000000..d65624fc6a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022, Simeon Andreev and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diff; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.ChangeType; +import org.eclipse.jgit.diff.RenameDetector; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.filter.PathFilter; + +/** + * Provides rename detection in special cases such as blame, where only a subset + * of the renames detected by {@link RenameDetector} is of interest. + */ +public class FilteredRenameDetector { + + private final RenameDetector renameDetector; + + /** + * @param repository + * The repository in which to check for renames. + */ + public FilteredRenameDetector(Repository repository) { + this(new RenameDetector(repository)); + } + + /** + * @param renameDetector + * The {@link RenameDetector} to use when checking for renames. + */ + public FilteredRenameDetector(RenameDetector renameDetector) { + this.renameDetector = renameDetector; + } + + /** + * @param diffs + * The set of changes to check. + * @param pathFilter + * Filter out changes that didn't affect this path. + * @return The subset of changes that affect only the filtered path. + * @throws IOException + */ + public List<DiffEntry> compute(List<DiffEntry> diffs, + PathFilter pathFilter) throws IOException { + return compute(diffs, Arrays.asList(pathFilter)); + } + + /** + * Tries to avoid computation overhead in {@link RenameDetector#compute()} + * by filtering diffs related to the path filters only. + * <p> + * Note: current implementation only optimizes added or removed diffs, + * further optimization is possible. + * + * @param changes + * The set of changes to check. + * @param pathFilters + * Filter out changes that didn't affect these paths. + * @return The subset of changes that affect only the filtered paths. + * @throws IOException + * @see RenameDetector#compute() + */ + public List<DiffEntry> compute(List<DiffEntry> changes, + List<PathFilter> pathFilters) throws IOException { + + if (pathFilters == null) { + throw new IllegalArgumentException("Must specify path filters"); //$NON-NLS-1$ + } + + Set<String> paths = new HashSet<>(pathFilters.size()); + for (PathFilter pathFilter : pathFilters) { + paths.add(pathFilter.getPath()); + } + + List<DiffEntry> filtered = new ArrayList<>(); + + // For new path: skip ADD's that don't match given paths + for (DiffEntry diff : changes) { + ChangeType changeType = diff.getChangeType(); + if (changeType != ChangeType.ADD + || paths.contains(diff.getNewPath())) { + filtered.add(diff); + } + } + + renameDetector.reset(); + renameDetector.addAll(filtered); + List<DiffEntry> sourceChanges = renameDetector.compute(); + + filtered.clear(); + + // For old path: skip DELETE's that don't match given paths + for (DiffEntry diff : changes) { + ChangeType changeType = diff.getChangeType(); + if (changeType != ChangeType.DELETE + || paths.contains(diff.getOldPath())) { + filtered.add(diff); + } + } + + renameDetector.reset(); + renameDetector.addAll(filtered); + List<DiffEntry> targetChanges = renameDetector.compute(); + + List<DiffEntry> result = new ArrayList<>(); + + for (DiffEntry sourceChange : sourceChanges) { + if (paths.contains(sourceChange.getNewPath())) { + result.add(sourceChange); + } + } + for (DiffEntry targetChange : targetChanges) { + if (paths.contains(targetChange.getOldPath())) { + result.add(targetChange); + } + } + + renameDetector.reset(); + return result; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java index 668adeab65..ebef5247e6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java @@ -27,6 +27,7 @@ import org.eclipse.jgit.util.FS_POSIX; import org.eclipse.jgit.util.FS_Win32; import org.eclipse.jgit.util.FS_Win32_Cygwin; import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.SystemReader; /** * Runs a command with help of FS. @@ -87,7 +88,9 @@ public class CommandExecutor { + "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + "stderr: \n" //$NON-NLS-1$ + new String( - result.getStderr().toByteArray()), + result.getStderr().toByteArray(), + SystemReader.getInstance() + .getDefaultCharset()), result, execError); } } @@ -202,7 +205,8 @@ public class CommandExecutor { commandFile = File.createTempFile(".__", //$NON-NLS-1$ "__jgit_tool" + fileExtension); //$NON-NLS-1$ try (OutputStream outStream = new FileOutputStream(commandFile)) { - byte[] strToBytes = command.getBytes(); + byte[] strToBytes = command + .getBytes(SystemReader.getInstance().getDefaultCharset()); outStream.write(strToBytes); outStream.close(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index 7cedd82995..d0034df3bc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -103,7 +103,7 @@ public class DiffTools { * @param noToolHandler * The handler to use when needing to inform the user, that no * tool is configured. - * @return the optioanl result of executing the tool if it was executed + * @return the optional result of executing the tool if it was executed * @throws ToolException * when the tool fails */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java index 7cc5bb50d9..73d3588906 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java @@ -11,7 +11,7 @@ package org.eclipse.jgit.internal.diffmergetool; import org.eclipse.jgit.util.FS.ExecutionResult; - +import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -114,7 +114,8 @@ public class ToolException extends Exception { return ""; //$NON-NLS-1$ } try { - return new String(result.getStderr().toByteArray()); + return new String(result.getStderr().toByteArray(), + SystemReader.getInstance().getDefaultCharset()); } catch (Exception e) { LOG.warn("Failed to retrieve standard error output", e); //$NON-NLS-1$ } @@ -129,7 +130,8 @@ public class ToolException extends Exception { return ""; //$NON-NLS-1$ } try { - return new String(result.getStdout().toByteArray()); + return new String(result.getStdout().toByteArray(), + SystemReader.getInstance().getDefaultCharset()); } catch (Exception e) { LOG.warn("Failed to retrieve standard output", e); //$NON-NLS-1$ } 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 99da222395..5a8207ed01 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 @@ -1,3 +1,12 @@ +/* + * Copyright (C) 2011, 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 + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ package org.eclipse.jgit.internal.storage.dfs; import java.io.ByteArrayOutputStream; @@ -6,13 +15,16 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.reftable.ReftableConfig; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.RefDatabase; /** @@ -98,6 +110,7 @@ public class InMemoryRepository extends DfsRepository { public static class MemObjDatabase extends DfsObjDatabase { private List<DfsPackDescription> packs = new ArrayList<>(); private int blockSize; + private Set<ObjectId> shallowCommits = Collections.emptySet(); MemObjDatabase(DfsRepository repo) { super(repo, new DfsReaderOptions()); @@ -167,6 +180,16 @@ public class InMemoryRepository extends DfsRepository { } @Override + public Set<ObjectId> getShallowCommits() throws IOException { + return shallowCommits; + } + + @Override + public void setShallowCommits(Set<ObjectId> shallowCommits) { + this.shallowCommits = shallowCommits; + } + + @Override public long getApproximateObjectCount() { long count = 0; for (DfsPackDescription p : packs) { 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 094fdc1559..9272bf3f59 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 @@ -1,6 +1,6 @@ /* * Copyright (C) 2010, Constantine Plotnikov <constantine.plotnikov@gmail.com> - * Copyright (C) 2010, JetBrains s.r.o. and others + * Copyright (C) 2010, 2022 JetBrains s.r.o. 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 @@ -117,10 +117,15 @@ class CachedObjectDirectory extends FileObjectDatabase { } @Override - Set<ObjectId> getShallowCommits() throws IOException { + public Set<ObjectId> getShallowCommits() throws IOException { return wrapped.getShallowCommits(); } + @Override + public void setShallowCommits(Set<ObjectId> shallowCommits) throws IOException { + wrapped.setShallowCommits(shallowCommits); + } + private CachedObjectDirectory[] myAlternates() { if (alts == null) { ObjectDirectory.AlternateHandle[] src = wrapped.myAlternates(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java index 01dd27d9fb..e97ed393a1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, Google Inc. and others + * Copyright (C) 2010, 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 @@ -50,8 +50,6 @@ abstract class FileObjectDatabase extends ObjectDatabase { abstract FS getFS(); - abstract Set<ObjectId> getShallowCommits() throws IOException; - abstract void selectObjectRepresentation(PackWriter packer, ObjectToPack otp, WindowCursor curs) throws IOException; 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 06c8cad3ac..7699439128 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009, 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 @@ -19,6 +19,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.text.MessageFormat; import java.util.ArrayList; @@ -560,31 +561,82 @@ public class ObjectDirectory extends FileObjectDatabase { } @Override - Set<ObjectId> getShallowCommits() throws IOException { + public Set<ObjectId> getShallowCommits() throws IOException { if (shallowFile == null || !shallowFile.isFile()) return Collections.emptySet(); if (shallowFileSnapshot == null || shallowFileSnapshot.isModified(shallowFile)) { - shallowCommitsIds = new HashSet<>(); + try { + shallowCommitsIds = FileUtils.readWithRetries(shallowFile, + f -> { + FileSnapshot newSnapshot = FileSnapshot.save(f); + HashSet<ObjectId> result = new HashSet<>(); + try (BufferedReader reader = open(f)) { + String line; + while ((line = reader.readLine()) != null) { + if (!ObjectId.isId(line)) { + throw new IOException( + MessageFormat.format(JGitText + .get().badShallowLine, + f.getAbsolutePath(), + line)); + + } + result.add(ObjectId.fromString(line)); + } + } + shallowFileSnapshot = newSnapshot; + return result; + }); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException( + MessageFormat.format(JGitText.get().readShallowFailed, + shallowFile.getAbsolutePath()), + e); + } + } - try (BufferedReader reader = open(shallowFile)) { - String line; - while ((line = reader.readLine()) != null) { - try { - shallowCommitsIds.add(ObjectId.fromString(line)); - } catch (IllegalArgumentException ex) { - throw new IOException(MessageFormat - .format(JGitText.get().badShallowLine, line), - ex); + return shallowCommitsIds; + } + + @Override + public void setShallowCommits(Set<ObjectId> shallowCommits) throws IOException { + this.shallowCommitsIds = shallowCommits; + LockFile lock = new LockFile(shallowFile); + if (!lock.lock()) { + throw new IOException(MessageFormat.format(JGitText.get().lockError, + shallowFile.getAbsolutePath())); + } + + try { + if (shallowCommits.isEmpty()) { + if (shallowFile.isFile()) { + shallowFile.delete(); + } + } else { + try (OutputStream out = lock.getOutputStream()) { + for (ObjectId shallowCommit : shallowCommits) { + byte[] buf = new byte[Constants.OBJECT_ID_STRING_LENGTH + 1]; + shallowCommit.copyTo(buf, 0); + buf[Constants.OBJECT_ID_STRING_LENGTH] = '\n'; + out.write(buf); } + } finally { + lock.commit(); } } + } finally { + lock.unlock(); + } + if (shallowCommits.isEmpty()) { + shallowFileSnapshot = FileSnapshot.DIRTY; + } else { shallowFileSnapshot = FileSnapshot.save(shallowFile); } - - return shallowCommitsIds; } void closeAllPackHandles(File packFile) { 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 cf2e69dbb5..30a0074195 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -1,7 +1,7 @@ /* * Copyright (C) 2008, Google Inc. * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2017, Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2006, 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 @@ -747,6 +747,13 @@ public final class Constants { */ public static final String LOCK_SUFFIX = ".lock"; //$NON-NLS-1$ + /** + * Depth used to unshallow a repository + * + * @since 6.3 + */ + public static final int INFINITE_DEPTH = 0x7fffffff; + private Constants() { // Hide the default constructor } 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 70009cba35..a39766cbd0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009, 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 @@ -11,6 +11,8 @@ package org.eclipse.jgit.lib; import java.io.IOException; +import java.util.Collections; +import java.util.Set; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -22,6 +24,9 @@ import org.eclipse.jgit.errors.MissingObjectException; * {@link org.eclipse.jgit.lib.ObjectId}. */ public abstract class ObjectDatabase implements AutoCloseable { + + private static final Set<ObjectId> shallowCommits = Collections.emptySet(); + /** * Initialize a new database instance for access. */ @@ -72,6 +77,35 @@ public abstract class ObjectDatabase implements AutoCloseable { public abstract ObjectReader newReader(); /** + * @return the shallow commits of the current repository + * + * @throws IOException the database could not be read + * + * @since 6.3 + */ + public Set<ObjectId> getShallowCommits() throws IOException { + return shallowCommits; + } + + + /** + * Update the shallow commits of the current repository + * + * @param shallowCommits the new shallow commits + * + * @throws IOException the database could not be updated + * + * @since 6.3 + */ + public void setShallowCommits(Set<ObjectId> shallowCommits) + throws IOException { + if (!shallowCommits.isEmpty()) { + throw new UnsupportedOperationException( + "Shallow commits expected to be empty."); //$NON-NLS-1$ + } + } + + /** * Close any resources held by this database. */ @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java index bf2a78f6b3..df6068925b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java @@ -195,9 +195,6 @@ public class RecursiveMerger extends ResolveMerger { inCore = oldIncore; dircache = oldDircache; workingTreeIterator = oldWTreeIt; - toBeCheckedOut.clear(); - toBeDeleted.clear(); - modifiedFiles.clear(); unmergedPaths.clear(); mergeResults.clear(); failingPaths.clear(); 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 b9ab1d1b7a..e56513d4e9 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,8 @@ * Copyright (C) 2010-2012, Matthias Sohn <matthias.sohn@sap.com> * Copyright (C) 2012, Research In Motion Limited * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) - * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <twolf@apache.org> + * Copyright (C) 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 @@ -20,9 +21,8 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_ALGORITHM; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; -import java.io.BufferedOutputStream; +import java.io.Closeable; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -32,12 +32,15 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.attributes.Attribute; import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.diff.DiffAlgorithm; import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; @@ -49,13 +52,12 @@ import org.eclipse.jgit.dircache.DirCacheBuildIterator; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; +import org.eclipse.jgit.dircache.DirCacheCheckout.StreamSupplier; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.BinaryBlobException; -import org.eclipse.jgit.errors.CorruptObjectException; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.IndexWriteException; -import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; @@ -64,6 +66,7 @@ import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.storage.pack.PackConfig; @@ -86,6 +89,595 @@ import org.eclipse.jgit.util.io.EolStreamTypeUtil; * A three-way merger performing a content-merge if necessary */ public class ResolveMerger extends ThreeWayMerger { + + /** + * Handles work tree updates on both the checkout and the index. + * <p> + * You should use a single instance for all of your file changes. In case of + * an error, make sure your instance is released, and initiate a new one if + * necessary. + * + * @since 6.3.1 + */ + protected static class WorkTreeUpdater implements Closeable { + + /** + * The result of writing the index changes. + */ + public static class Result { + + private final List<String> modifiedFiles = new LinkedList<>(); + + private final List<String> failedToDelete = new LinkedList<>(); + + private ObjectId treeId = null; + + /** + * @return Modified tree ID if any, or null otherwise. + */ + public ObjectId getTreeId() { + return treeId; + } + + /** + * @return Files that couldn't be deleted. + */ + public List<String> getFailedToDelete() { + return failedToDelete; + } + + /** + * @return Files modified during this operation. + */ + public List<String> getModifiedFiles() { + return modifiedFiles; + } + } + + Result result = new Result(); + + /** + * The repository this handler operates on. + */ + @Nullable + private final Repository repo; + + /** + * Set to true if this operation should work in-memory. The repo's + * dircache and workingtree are not touched by this method. Eventually + * needed files are created as temporary files and a new empty, + * in-memory dircache will be used instead the repo's one. Often used + * for bare repos where the repo doesn't even have a workingtree and + * dircache. + */ + private final boolean inCore; + + private final ObjectInserter inserter; + + private final ObjectReader reader; + + private DirCache dirCache; + + private boolean implicitDirCache = false; + + /** + * Builder to update the dir cache during this operation. + */ + private DirCacheBuilder builder; + + /** + * The {@link WorkingTreeOptions} are needed to determine line endings + * for affected files. + */ + private WorkingTreeOptions workingTreeOptions; + + /** + * The size limit (bytes) which controls a file to be stored in + * {@code Heap} or {@code LocalFile} during the operation. + */ + private int inCoreFileSizeLimit; + + /** + * If the operation has nothing to do for a file but check it out at the + * end of the operation, it can be added here. + */ + private final Map<String, DirCacheEntry> toBeCheckedOut = new HashMap<>(); + + /** + * Files in this list will be deleted from the local copy at the end of + * the operation. + */ + private final TreeMap<String, File> toBeDeleted = new TreeMap<>(); + + /** + * Keeps {@link CheckoutMetadata} for {@link #checkout()}. + */ + private Map<String, CheckoutMetadata> checkoutMetadataByPath; + + /** + * Keeps {@link CheckoutMetadata} for {@link #revertModifiedFiles()}. + */ + private Map<String, CheckoutMetadata> cleanupMetadataByPath; + + /** + * Whether the changes were successfully written. + */ + private boolean indexChangesWritten; + + /** + * @param repo + * the {@link Repository}. + * @param dirCache + * if set, use the provided dir cache. Otherwise, use the + * default repository one + */ + private WorkTreeUpdater(Repository repo, DirCache dirCache) { + this.repo = repo; + this.dirCache = dirCache; + + this.inCore = false; + this.inserter = repo.newObjectInserter(); + this.reader = inserter.newReader(); + Config config = repo.getConfig(); + this.workingTreeOptions = config.get(WorkingTreeOptions.KEY); + this.inCoreFileSizeLimit = getInCoreFileSizeLimit(config); + this.checkoutMetadataByPath = new HashMap<>(); + this.cleanupMetadataByPath = new HashMap<>(); + } + + /** + * Creates a new {@link WorkTreeUpdater} for the given repository. + * + * @param repo + * the {@link Repository}. + * @param dirCache + * if set, use the provided dir cache. Otherwise, use the + * default repository one + * @return the {@link WorkTreeUpdater}. + */ + public static WorkTreeUpdater createWorkTreeUpdater(Repository repo, + DirCache dirCache) { + return new WorkTreeUpdater(repo, dirCache); + } + + /** + * @param repo + * the {@link Repository}. + * @param dirCache + * if set, use the provided dir cache. Otherwise, creates a + * new one + * @param oi + * to use for writing the modified objects with. + */ + private WorkTreeUpdater(Repository repo, DirCache dirCache, + ObjectInserter oi) { + this.repo = repo; + this.dirCache = dirCache; + this.inserter = oi; + + this.inCore = true; + this.reader = oi.newReader(); + if (repo != null) { + this.inCoreFileSizeLimit = getInCoreFileSizeLimit( + repo.getConfig()); + } + } + + /** + * Creates a new {@link WorkTreeUpdater} that works in memory only. + * + * @param repo + * the {@link Repository}. + * @param dirCache + * if set, use the provided dir cache. Otherwise, creates a + * new one + * @param oi + * to use for writing the modified objects with. + * @return the {@link WorkTreeUpdater} + */ + public static WorkTreeUpdater createInCoreWorkTreeUpdater( + Repository repo, DirCache dirCache, ObjectInserter oi) { + return new WorkTreeUpdater(repo, dirCache, oi); + } + + private static int getInCoreFileSizeLimit(Config config) { + return config.getInt(ConfigConstants.CONFIG_MERGE_SECTION, + ConfigConstants.CONFIG_KEY_IN_CORE_LIMIT, 10 << 20); + } + + /** + * Gets the size limit for in-core files in this config. + * + * @return the size + */ + public int getInCoreFileSizeLimit() { + return inCoreFileSizeLimit; + } + + /** + * Gets dir cache for the repo. Locked if not inCore. + * + * @return the result dir cache + * @throws IOException + * is case the dir cache cannot be read + */ + public DirCache getLockedDirCache() throws IOException { + if (dirCache == null) { + implicitDirCache = true; + if (inCore) { + dirCache = DirCache.newInCore(); + } else { + dirCache = nonNullRepo().lockDirCache(); + } + } + if (builder == null) { + builder = dirCache.builder(); + } + return dirCache; + } + + /** + * Creates a {@link DirCacheBuildIterator} for the builder of this + * {@link WorkTreeUpdater}. + * + * @return the {@link DirCacheBuildIterator} + */ + public DirCacheBuildIterator createDirCacheBuildIterator() { + return new DirCacheBuildIterator(builder); + } + + /** + * Writes the changes to the working tree (but not to the index). + * + * @param shouldCheckoutTheirs + * before committing the changes + * @throws IOException + * if any of the writes fail + */ + public void writeWorkTreeChanges(boolean shouldCheckoutTheirs) + throws IOException { + handleDeletedFiles(); + + if (inCore) { + builder.finish(); + return; + } + if (shouldCheckoutTheirs) { + // No problem found. The only thing left to be done is to + // check out all files from "theirs" which have been selected to + // go into the new index. + checkout(); + } + + // All content operations are successfully done. If we can now write + // the + // new index we are on quite safe ground. Even if the checkout of + // files coming from "theirs" fails the user can work around such + // failures by checking out the index again. + if (!builder.commit()) { + revertModifiedFiles(); + throw new IndexWriteException(); + } + } + + /** + * Writes the changes to the index. + * + * @return the {@link Result} of the operation. + * @throws IOException + * if any of the writes fail + */ + public Result writeIndexChanges() throws IOException { + result.treeId = getLockedDirCache().writeTree(inserter); + indexChangesWritten = true; + return result; + } + + /** + * Adds a {@link DirCacheEntry} for direct checkout and remembers its + * {@link CheckoutMetadata}. + * + * @param path + * of the entry + * @param entry + * to add + * @param cleanupStreamType + * to use for the cleanup metadata + * @param cleanupSmudgeCommand + * to use for the cleanup metadata + * @param checkoutStreamType + * to use for the checkout metadata + * @param checkoutSmudgeCommand + * to use for the checkout metadata + */ + public void addToCheckout(String path, DirCacheEntry entry, + EolStreamType cleanupStreamType, String cleanupSmudgeCommand, + EolStreamType checkoutStreamType, + String checkoutSmudgeCommand) { + if (entry != null) { + // In some cases, we just want to add the metadata. + toBeCheckedOut.put(path, entry); + } + addCheckoutMetadata(cleanupMetadataByPath, path, cleanupStreamType, + cleanupSmudgeCommand); + addCheckoutMetadata(checkoutMetadataByPath, path, + checkoutStreamType, checkoutSmudgeCommand); + } + + /** + * Gets a map which maps the paths of files which have to be checked out + * because the operation created new fully-merged content for this file + * into the index. + * <p> + * This means: the operation wrote a new stage 0 entry for this path. + * </p> + * + * @return the map + */ + public Map<String, DirCacheEntry> getToBeCheckedOut() { + return toBeCheckedOut; + } + + /** + * Remembers the given file to be deleted. + * <p> + * Note the actual deletion is only done in + * {@link #writeWorkTreeChanges}. + * + * @param path + * of the file to be deleted + * @param file + * to be deleted + * @param streamType + * to use for cleanup metadata + * @param smudgeCommand + * to use for cleanup metadata + */ + public void deleteFile(String path, File file, EolStreamType streamType, + String smudgeCommand) { + toBeDeleted.put(path, file); + if (file != null && file.isFile()) { + addCheckoutMetadata(cleanupMetadataByPath, path, streamType, + smudgeCommand); + } + } + + /** + * Remembers the {@link CheckoutMetadata} for the given path; it may be + * needed in {@link #checkout()} or in {@link #revertModifiedFiles()}. + * + * @param map + * to add the metadata to + * @param path + * of the current node + * @param streamType + * to use for the metadata + * @param smudgeCommand + * to use for the metadata + */ + private void addCheckoutMetadata(Map<String, CheckoutMetadata> map, + String path, EolStreamType streamType, String smudgeCommand) { + if (inCore || map == null) { + return; + } + map.put(path, new CheckoutMetadata(streamType, smudgeCommand)); + } + + /** + * Detects if CRLF conversion has been configured. + * <p> + * </p> + * See {@link EolStreamTypeUtil#detectStreamType} for more info. + * + * @param attributes + * of the file for which the type is to be detected + * @return the detected type + */ + public EolStreamType detectCheckoutStreamType(Attributes attributes) { + if (inCore) { + return null; + } + return EolStreamTypeUtil.detectStreamType(OperationType.CHECKOUT_OP, + workingTreeOptions, attributes); + } + + private void handleDeletedFiles() { + // Iterate in reverse so that "folder/file" is deleted before + // "folder". Otherwise, this could result in a failing path because + // of a non-empty directory, for which delete() would fail. + for (String path : toBeDeleted.descendingKeySet()) { + File file = inCore ? null : toBeDeleted.get(path); + if (file != null && !file.delete()) { + if (!file.isDirectory()) { + result.failedToDelete.add(path); + } + } + } + } + + /** + * Marks the given path as modified in the operation. + * + * @param path + * to mark as modified + */ + public void markAsModified(String path) { + result.modifiedFiles.add(path); + } + + /** + * Gets the list of files which were modified in this operation. + * + * @return the list + */ + public List<String> getModifiedFiles() { + return result.modifiedFiles; + } + + private void checkout() throws NoWorkTreeException, IOException { + for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut + .entrySet()) { + DirCacheEntry dirCacheEntry = entry.getValue(); + if (dirCacheEntry.getFileMode() == FileMode.GITLINK) { + new File(nonNullRepo().getWorkTree(), entry.getKey()) + .mkdirs(); + } else { + DirCacheCheckout.checkoutEntry(repo, dirCacheEntry, reader, + false, checkoutMetadataByPath.get(entry.getKey()), + workingTreeOptions); + result.modifiedFiles.add(entry.getKey()); + } + } + } + + /** + * Reverts any uncommitted changes in the worktree. We know that for all + * modified files the old content was in the old index and the index + * contained only stage 0. In case of inCore operation just clear the + * history of modified files. + * + * @throws IOException + * in case the cleaning up failed + */ + public void revertModifiedFiles() throws IOException { + if (inCore) { + result.modifiedFiles.clear(); + return; + } + if (indexChangesWritten) { + return; + } + for (String path : result.modifiedFiles) { + DirCacheEntry entry = dirCache.getEntry(path); + if (entry != null) { + DirCacheCheckout.checkoutEntry(repo, entry, reader, false, + cleanupMetadataByPath.get(path), + workingTreeOptions); + } + } + } + + @Override + public void close() throws IOException { + if (implicitDirCache) { + dirCache.unlock(); + } + } + + /** + * Updates the file in the checkout with the given content. + * + * @param inputStream + * the content to be updated + * @param streamType + * for parsing the content + * @param smudgeCommand + * for formatting the content + * @param path + * of the file to be updated + * @param file + * to be updated + * @throws IOException + * if the file cannot be updated + */ + public void updateFileWithContent(StreamSupplier inputStream, + EolStreamType streamType, String smudgeCommand, String path, + File file) throws IOException { + if (inCore) { + return; + } + CheckoutMetadata metadata = new CheckoutMetadata(streamType, + smudgeCommand); + + try (OutputStream outputStream = new FileOutputStream(file)) { + DirCacheCheckout.getContent(repo, path, metadata, inputStream, + workingTreeOptions, outputStream); + } + } + + /** + * Creates a path with the given content, and adds it to the specified + * stage to the index builder. + * + * @param input + * the content to be updated + * @param path + * of the file to be updated + * @param fileMode + * of the modified file + * @param entryStage + * of the new entry + * @param lastModified + * instant of the modified file + * @param len + * of the content + * @param lfsAttribute + * for checking for LFS enablement + * @return the entry which was added to the index + * @throws IOException + * if inserting the content fails + */ + public DirCacheEntry insertToIndex(InputStream input, byte[] path, + FileMode fileMode, int entryStage, Instant lastModified, + int len, Attribute lfsAttribute) throws IOException { + return addExistingToIndex(insertResult(input, lfsAttribute, len), + path, fileMode, entryStage, lastModified, len); + } + + /** + * Adds a path with the specified stage to the index builder. + * + * @param objectId + * of the existing object to add + * @param path + * of the modified file + * @param fileMode + * of the modified file + * @param entryStage + * of the new entry + * @param lastModified + * instant of the modified file + * @param len + * of the modified file content + * @return the entry which was added to the index + */ + public DirCacheEntry addExistingToIndex(ObjectId objectId, byte[] path, + FileMode fileMode, int entryStage, Instant lastModified, + int len) { + DirCacheEntry dce = new DirCacheEntry(path, entryStage); + dce.setFileMode(fileMode); + if (lastModified != null) { + dce.setLastModified(lastModified); + } + dce.setLength(inCore ? 0 : len); + dce.setObjectId(objectId); + builder.add(dce); + return dce; + } + + private ObjectId insertResult(InputStream input, Attribute lfsAttribute, + long length) throws IOException { + try (LfsInputStream is = LfsFactory.getInstance() + .applyCleanFilter(repo, input, length, lfsAttribute)) { + return inserter.insert(OBJ_BLOB, is.getLength(), is); + } + } + + /** + * Gets the non-null repository instance of this + * {@link WorkTreeUpdater}. + * + * @return non-null repository instance + * @throws NullPointerException + * if the handler was constructed without a repository. + */ + @NonNull + private Repository nonNullRepo() throws NullPointerException { + return Objects.requireNonNull(repo, + () -> JGitText.get().repositoryIsRequired); + } + } + /** * If the merge fails (means: not stopped because of unresolved conflicts) * this enum is used to explain why it failed @@ -149,11 +741,11 @@ public class ResolveMerger extends ThreeWayMerger { protected static final int T_FILE = 4; /** - * Builder to update the cache during this merge. + * Handler for repository I/O actions. * - * @since 3.4 + * @since 6.3 */ - protected DirCacheBuilder builder; + protected WorkTreeUpdater workTreeUpdater; /** * merge result as tree @@ -163,35 +755,17 @@ public class ResolveMerger extends ThreeWayMerger { protected ObjectId resultTree; /** - * Paths that could not be merged by this merger because of an unsolvable - * conflict. - * - * @since 3.4 - */ - protected List<String> unmergedPaths = new ArrayList<>(); - - /** - * Files modified during this merge operation. - * - * @since 3.4 - */ - protected List<String> modifiedFiles = new LinkedList<>(); - - /** - * If the merger has nothing to do for a file but check it out at the end of - * the operation, it can be added here. - * - * @since 3.4 + * Files modified during this operation. Note this list is only updated after a successful write. */ - protected Map<String, DirCacheEntry> toBeCheckedOut = new HashMap<>(); + protected List<String> modifiedFiles = new ArrayList<>(); /** - * Paths in this list will be deleted from the local copy at the end of the - * operation. + * Paths that could not be merged by this merger because of an unsolvable + * conflict. * * @since 3.4 */ - protected List<String> toBeDeleted = new ArrayList<>(); + protected List<String> unmergedPaths = new ArrayList<>(); /** * Low-level textual merge results. Will be passed on to the callers in case @@ -227,15 +801,6 @@ public class ResolveMerger extends ThreeWayMerger { protected boolean inCore; /** - * Set to true if this merger should use the default dircache of the - * repository and should handle locking and unlocking of the dircache. If - * this merger should work in-core or if an explicit dircache was specified - * during construction then this field is set to false. - * @since 3.0 - */ - protected boolean implicitDirCache; - - /** * Directory cache * @since 3.0 */ @@ -255,36 +820,12 @@ public class ResolveMerger extends ThreeWayMerger { protected MergeAlgorithm mergeAlgorithm; /** - * The {@link WorkingTreeOptions} are needed to determine line endings for - * merged files. - * - * @since 4.11 - */ - protected WorkingTreeOptions workingTreeOptions; - - /** - * The size limit (bytes) which controls a file to be stored in {@code Heap} - * or {@code LocalFile} during the merge. - */ - private int inCoreLimit; - - /** * The {@link ContentMergeStrategy} to use for "resolve" and "recursive" * merges. */ @NonNull private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT; - /** - * 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, @@ -292,13 +833,8 @@ public class ResolveMerger extends ThreeWayMerger { return new MergeAlgorithm(DiffAlgorithm.getAlgorithm(diffAlg)); } - private static int getInCoreLimit(Config config) { - return config.getInt( - ConfigConstants.CONFIG_MERGE_SECTION, ConfigConstants.CONFIG_KEY_IN_CORE_LIMIT, 10 << 20); - } - private static String[] defaultCommitNames() { - return new String[] { "BASE", "OURS", "THEIRS" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + return new String[]{"BASE", "OURS", "THEIRS"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } private static final Attributes NO_ATTRIBUTES = new Attributes(); @@ -315,17 +851,8 @@ public class ResolveMerger extends ThreeWayMerger { super(local); Config config = local.getConfig(); mergeAlgorithm = getMergeAlgorithm(config); - inCoreLimit = getInCoreLimit(config); commitNames = defaultCommitNames(); this.inCore = inCore; - - if (inCore) { - implicitDirCache = false; - dircache = DirCache.newInCore(); - } else { - implicitDirCache = true; - workingTreeOptions = local.getConfig().get(WorkingTreeOptions.KEY); - } } /** @@ -352,8 +879,6 @@ public class ResolveMerger extends ThreeWayMerger { mergeAlgorithm = getMergeAlgorithm(config); commitNames = defaultCommitNames(); inCore = true; - implicitDirCache = false; - dircache = DirCache.newInCore(); } /** @@ -382,81 +907,8 @@ public class ResolveMerger extends ThreeWayMerger { /** {@inheritDoc} */ @Override protected boolean mergeImpl() throws IOException { - if (implicitDirCache) { - dircache = nonNullRepo().lockDirCache(); - } - 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(); - } - } - } - - private void checkout() throws NoWorkTreeException, IOException { - // Iterate in reverse so that "folder/file" is deleted before - // "folder". Otherwise this could result in a failing path because - // of a non-empty directory, for which delete() would fail. - for (int i = toBeDeleted.size() - 1; i >= 0; i--) { - String fileName = toBeDeleted.get(i); - File f = new File(nonNullRepo().getWorkTree(), fileName); - if (!f.delete()) - if (!f.isDirectory()) - failingPaths.put(fileName, - MergeFailureReason.COULD_NOT_DELETE); - modifiedFiles.add(fileName); - } - for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut - .entrySet()) { - DirCacheEntry cacheEntry = entry.getValue(); - if (cacheEntry.getFileMode() == FileMode.GITLINK) { - new File(nonNullRepo().getWorkTree(), entry.getKey()).mkdirs(); - } else { - DirCacheCheckout.checkoutEntry(db, cacheEntry, reader, false, - checkoutMetadata.get(entry.getKey())); - modifiedFiles.add(entry.getKey()); - } - } - } - - /** - * Reverts the worktree after an unsuccessful merge. We know that for all - * modified files the old content was in the old index and the index - * contained only stage 0. In case if inCore operation just clear the - * history of modified files. - * - * @throws java.io.IOException - * @throws org.eclipse.jgit.errors.CorruptObjectException - * @throws org.eclipse.jgit.errors.NoWorkTreeException - * @since 3.4 - */ - protected void cleanUp() throws NoWorkTreeException, - CorruptObjectException, - IOException { - if (inCore) { - modifiedFiles.clear(); - return; - } - - DirCache dc = nonNullRepo().readDirCache(); - Iterator<String> mpathsIt=modifiedFiles.iterator(); - while(mpathsIt.hasNext()) { - String mpath = mpathsIt.next(); - DirCacheEntry entry = dc.getEntry(mpath); - if (entry != null) { - DirCacheCheckout.checkoutEntry(db, entry, reader, false, - cleanupMetadata.get(mpath)); - } - mpathsIt.remove(); - } + return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], + false); } /** @@ -472,18 +924,33 @@ public class ResolveMerger extends ThreeWayMerger { private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage, Instant lastMod, long len) { if (p != null && !p.getEntryFileMode().equals(FileMode.TREE)) { - DirCacheEntry e = new DirCacheEntry(path, stage); - e.setFileMode(p.getEntryFileMode()); - e.setObjectId(p.getEntryObjectId()); - e.setLastModified(lastMod); - e.setLength(len); - builder.add(e); - return e; + return workTreeUpdater.addExistingToIndex(p.getEntryObjectId(), path, + p.getEntryFileMode(), stage, + lastMod, (int) len); } return null; } /** + * Adds the conflict stages for the current path of {@link #tw} to the index + * builder and returns the "theirs" stage; if present. + * + * @param base + * of the conflict + * @param ours + * of the conflict + * @param theirs + * of the conflict + * @return the {@link DirCacheEntry} for the "theirs" stage, or {@code null} + */ + private DirCacheEntry addConflict(CanonicalTreeParser base, + CanonicalTreeParser ours, CanonicalTreeParser theirs) { + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); + return add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + } + + /** * adds a entry to the index builder which is a copy of the specified * DirCacheEntry * @@ -493,41 +960,8 @@ public class ResolveMerger extends ThreeWayMerger { * @return the entry which was added to the index */ private DirCacheEntry keep(DirCacheEntry e) { - DirCacheEntry newEntry = new DirCacheEntry(e.getRawPath(), - e.getStage()); - newEntry.setFileMode(e.getFileMode()); - newEntry.setObjectId(e.getObjectId()); - newEntry.setLastModified(e.getLastModifiedInstant()); - newEntry.setLength(e.getLength()); - builder.add(newEntry); - return newEntry; - } - - /** - * 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 - * to use for determining the metadata - * @throws IOException - * if the smudge filter cannot be determined - * @since 6.1 - */ - protected void addCheckoutMetadata(Map<String, CheckoutMetadata> map, - String path, Attributes attributes) - throws IOException { - if (map != null) { - EolStreamType eol = EolStreamTypeUtil.detectStreamType( - OperationType.CHECKOUT_OP, workingTreeOptions, - attributes); - CheckoutMetadata data = new CheckoutMetadata(eol, - tw.getSmudgeCommand(attributes)); - map.put(path, data); - } + return workTreeUpdater.addExistingToIndex(e.getObjectId(), e.getRawPath(), e.getFileMode(), + e.getStage(), e.getLastModifiedInstant(), e.getLength()); } /** @@ -547,14 +981,17 @@ public class ResolveMerger extends ThreeWayMerger { protected void addToCheckout(String path, DirCacheEntry entry, Attributes[] attributes) throws IOException { - toBeCheckedOut.put(path, entry); - addCheckoutMetadata(cleanupMetadata, path, attributes[T_OURS]); - addCheckoutMetadata(checkoutMetadata, path, attributes[T_THEIRS]); + EolStreamType cleanupStreamType = workTreeUpdater.detectCheckoutStreamType(attributes[T_OURS]); + String cleanupSmudgeCommand = tw.getSmudgeCommand(attributes[T_OURS]); + EolStreamType checkoutStreamType = workTreeUpdater.detectCheckoutStreamType(attributes[T_THEIRS]); + String checkoutSmudgeCommand = tw.getSmudgeCommand(attributes[T_THEIRS]); + workTreeUpdater.addToCheckout(path, entry, cleanupStreamType, cleanupSmudgeCommand, + checkoutStreamType, checkoutSmudgeCommand); } /** * Remember a path for deletion, and remember its {@link CheckoutMetadata} - * in case it has to be restored in {@link #cleanUp()}. + * in case it has to be restored in the cleanUp. * * @param path * of the entry @@ -568,10 +1005,13 @@ public class ResolveMerger extends ThreeWayMerger { */ protected void addDeletion(String path, boolean isFile, Attributes attributes) throws IOException { - toBeDeleted.add(path); - if (isFile) { - addCheckoutMetadata(cleanupMetadata, path, attributes); - } + if (db == null || nonNullRepo().isBare() || !isFile) + return; + + File file = new File(nonNullRepo().getWorkTree(), path); + EolStreamType streamType = workTreeUpdater.detectCheckoutStreamType(attributes); + String smudgeCommand = tw.getSmudgeCommand(attributes); + workTreeUpdater.deleteFile(path, file, streamType, smudgeCommand); } /** @@ -615,9 +1055,6 @@ public class ResolveMerger extends ThreeWayMerger { * @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 - * @throws org.eclipse.jgit.errors.MissingObjectException - * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException - * @throws org.eclipse.jgit.errors.CorruptObjectException * @throws java.io.IOException * @since 6.1 */ @@ -625,20 +1062,21 @@ public class ResolveMerger extends ThreeWayMerger { CanonicalTreeParser ours, CanonicalTreeParser theirs, DirCacheBuildIterator index, WorkingTreeIterator work, boolean ignoreConflicts, Attributes[] attributes) - throws MissingObjectException, IncorrectObjectTypeException, - CorruptObjectException, IOException { + throws IOException { enterSubtree = true; final int modeO = tw.getRawMode(T_OURS); final int modeT = tw.getRawMode(T_THEIRS); final int modeB = tw.getRawMode(T_BASE); boolean gitLinkMerging = isGitLink(modeO) || isGitLink(modeT) || isGitLink(modeB); - if (modeO == 0 && modeT == 0 && modeB == 0) + if (modeO == 0 && modeT == 0 && modeB == 0) { // File is either untracked or new, staged but uncommitted return true; + } - if (isIndexDirty()) + if (isIndexDirty()) { return false; + } DirCacheEntry ourDce = null; @@ -693,9 +1131,7 @@ public class ResolveMerger extends ThreeWayMerger { // length. // This path can be skipped on ignoreConflicts, so the caller // could use virtual commit. - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + addConflict(base, ours, theirs); unmergedPaths.add(tw.getPathString()); mergeResults.put(tw.getPathString(), new MergeResult<>(Collections.emptyList())); @@ -706,8 +1142,9 @@ public class ResolveMerger extends ThreeWayMerger { if (modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) { // THEIRS was not changed compared to BASE. All changes must be in // OURS. OURS is chosen. We can keep the existing entry. - if (ourDce != null) + if (ourDce != null) { keep(ourDce); + } // no checkout needed! return true; } @@ -717,8 +1154,9 @@ public class ResolveMerger extends ThreeWayMerger { // THEIRS. THEIRS is chosen. // Check worktree before checking out THEIRS - if (isWorktreeDirty(work, ourDce)) + if (isWorktreeDirty(work, ourDce)) { return false; + } if (nonTree(modeT)) { // we know about length and lastMod only after we have written // the new content. @@ -759,12 +1197,15 @@ public class ResolveMerger extends ThreeWayMerger { enterSubtree = false; return true; } - if (nonTree(modeB)) + if (nonTree(modeB)) { add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); - if (nonTree(modeO)) + } + if (nonTree(modeO)) { add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); - if (nonTree(modeT)) + } + if (nonTree(modeT)) { add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + } unmergedPaths.add(tw.getPathString()); enterSubtree = false; return true; @@ -774,8 +1215,9 @@ public class ResolveMerger extends ThreeWayMerger { // tells us we are in a subtree because of index or working-dir). // If they are both folders no content-merge is required - we can // return here. - if (!nonTree(modeO)) + if (!nonTree(modeO)) { return true; + } // ours and theirs are both files, just fall out of the if block // and do the content merge @@ -794,9 +1236,7 @@ public class ResolveMerger extends ThreeWayMerger { add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0, EPOCH, 0); return true; } else if (gitLinkMerging) { - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + addConflict(base, ours, theirs); MergeResult<SubmoduleConflict> result = createGitLinksMergeResult( base, ours, theirs); result.setContainsConflicts(true); @@ -806,20 +1246,18 @@ public class ResolveMerger extends ThreeWayMerger { } else if (!attributes[T_OURS].canBeContentMerged()) { // File marked as binary switch (getContentMergeStrategy()) { - case OURS: - keep(ourDce); - return true; - case THEIRS: - DirCacheEntry theirEntry = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_0, EPOCH, 0); - addToCheckout(tw.getPathString(), theirEntry, attributes); - return true; - default: - break; + case OURS: + keep(ourDce); + return true; + case THEIRS: + DirCacheEntry theirEntry = add(tw.getRawPath(), theirs, + DirCacheEntry.STAGE_0, EPOCH, 0); + addToCheckout(tw.getPathString(), theirEntry, attributes); + return true; + default: + break; } - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + addConflict(base, ours, theirs); // attribute merge issues are conflicts but not failures unmergedPaths.add(tw.getPathString()); @@ -832,18 +1270,27 @@ public class ResolveMerger extends ThreeWayMerger { } MergeResult<RawText> result = null; - try { - result = contentMerge(base, ours, theirs, attributes, - getContentMergeStrategy()); - } catch (BinaryBlobException e) { + boolean hasSymlink = FileMode.SYMLINK.equals(modeO) + || FileMode.SYMLINK.equals(modeT); + if (!hasSymlink) { + try { + result = contentMerge(base, ours, theirs, attributes, + getContentMergeStrategy()); + } catch (BinaryBlobException e) { + // result == null + } + } + if (result == null) { switch (getContentMergeStrategy()) { case OURS: keep(ourDce); return true; case THEIRS: - DirCacheEntry theirEntry = add(tw.getRawPath(), theirs, + DirCacheEntry e = add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_0, EPOCH, 0); - addToCheckout(tw.getPathString(), theirEntry, attributes); + if (e != null) { + addToCheckout(tw.getPathString(), e, attributes); + } return true; default: result = new MergeResult<>(Collections.emptyList()); @@ -854,16 +1301,36 @@ public class ResolveMerger extends ThreeWayMerger { if (ignoreConflicts) { result.setContainsConflicts(false); } - updateIndex(base, ours, theirs, result, attributes[T_OURS]); String currentPath = tw.getPathString(); + if (hasSymlink) { + if (ignoreConflicts) { + if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE)) { + DirCacheEntry e = add(tw.getRawPath(), theirs, + DirCacheEntry.STAGE_0, EPOCH, 0); + addToCheckout(currentPath, e, attributes); + } else { + keep(ourDce); + } + } else { + // Record the conflict + DirCacheEntry e = addConflict(base, ours, theirs); + mergeResults.put(currentPath, result); + // If theirs is a file, check it out. In link/file + // conflicts, C git prefers the file. + if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE) + && e != null) { + addToCheckout(currentPath, e, attributes); + } + } + } else { + updateIndex(base, ours, theirs, result, attributes[T_OURS]); + } if (result.containsConflicts() && !ignoreConflicts) { unmergedPaths.add(currentPath); } - modifiedFiles.add(currentPath); - addCheckoutMetadata(cleanupMetadata, currentPath, - attributes[T_OURS]); - addCheckoutMetadata(checkoutMetadata, currentPath, - attributes[T_THEIRS]); + workTreeUpdater.markAsModified(currentPath); + // Entry is null - only adds the metadata. + addToCheckout(currentPath, null, attributes); } else if (modeO != modeT) { // OURS or THEIRS has been deleted if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw @@ -871,40 +1338,58 @@ public class ResolveMerger extends ThreeWayMerger { if (gitLinkMerging && ignoreConflicts) { add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0, EPOCH, 0); } else if (gitLinkMerging) { - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + addConflict(base, ours, theirs); MergeResult<SubmoduleConflict> result = createGitLinksMergeResult( base, ours, theirs); result.setContainsConflicts(true); mergeResults.put(tw.getPathString(), result); unmergedPaths.add(tw.getPathString()); } else { + boolean isSymLink = ((modeO | modeT) + & FileMode.TYPE_MASK) == FileMode.TYPE_SYMLINK; // Content merge strategy does not apply to delete-modify // conflicts! MergeResult<RawText> result; - try { - result = contentMerge(base, ours, theirs, attributes, - ContentMergeStrategy.CONFLICT); - } catch (BinaryBlobException e) { + if (isSymLink) { + // No need to do a content merge result = new MergeResult<>(Collections.emptyList()); result.setContainsConflicts(true); + } else { + try { + result = contentMerge(base, ours, theirs, + attributes, ContentMergeStrategy.CONFLICT); + } catch (BinaryBlobException e) { + result = new MergeResult<>(Collections.emptyList()); + result.setContainsConflicts(true); + } } if (ignoreConflicts) { - // In case a conflict is detected the working tree file - // is again filled with new content (containing conflict - // markers). But also stage 0 of the index is filled - // with that content. result.setContainsConflicts(false); - updateIndex(base, ours, theirs, result, - attributes[T_OURS]); + if (isSymLink) { + if (modeO != 0) { + keep(ourDce); + } else { + // Check out theirs + if (isWorktreeDirty(work, ourDce)) { + return false; + } + DirCacheEntry e = add(tw.getRawPath(), theirs, + DirCacheEntry.STAGE_0, EPOCH, 0); + if (e != null) { + addToCheckout(tw.getPathString(), e, + attributes); + } + } + } else { + // In case a conflict is detected the working tree + // file is again filled with new content (containing + // conflict markers). But also stage 0 of the index + // is filled with that content. + updateIndex(base, ours, theirs, result, + attributes[T_OURS]); + } } else { - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, - 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, - 0); - DirCacheEntry e = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_3, EPOCH, 0); + DirCacheEntry e = addConflict(base, ours, theirs); // OURS was deleted checkout THEIRS if (modeO == 0) { @@ -975,8 +1460,9 @@ public class ResolveMerger extends ThreeWayMerger { } private boolean isIndexDirty() { - if (inCore) + if (inCore) { return false; + } final int modeI = tw.getRawMode(T_INDEX); final int modeO = tw.getRawMode(T_OURS); @@ -984,37 +1470,42 @@ public class ResolveMerger extends ThreeWayMerger { // Index entry has to match ours to be considered clean final boolean isDirty = nonTree(modeI) && !(modeO == modeI && tw.idEqual(T_INDEX, T_OURS)); - if (isDirty) + if (isDirty) { failingPaths .put(tw.getPathString(), MergeFailureReason.DIRTY_INDEX); + } return isDirty; } private boolean isWorktreeDirty(WorkingTreeIterator work, DirCacheEntry ourDce) throws IOException { - if (work == null) + if (work == null) { return false; + } final int modeF = tw.getRawMode(T_FILE); final int modeO = tw.getRawMode(T_OURS); // Worktree entry has to match ours to be considered clean boolean isDirty; - if (ourDce != null) + if (ourDce != null) { isDirty = work.isModified(ourDce, true, reader); - else { + } else { isDirty = work.isModeDifferent(modeO); - if (!isDirty && nonTree(modeF)) + if (!isDirty && nonTree(modeF)) { isDirty = !tw.idEqual(T_FILE, T_OURS); + } } // Ignore existing empty directories if (isDirty && modeF == FileMode.TYPE_TREE - && modeO == FileMode.TYPE_MISSING) + && modeO == FileMode.TYPE_MISSING) { isDirty = false; - if (isDirty) + } + if (isDirty) { failingPaths.put(tw.getPathString(), MergeFailureReason.DIRTY_WORKTREE); + } return isDirty; } @@ -1029,14 +1520,12 @@ public class ResolveMerger extends ThreeWayMerger { * @param theirs * @param result * @param attributes - * @throws FileNotFoundException * @throws IOException */ private void updateIndex(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, MergeResult<RawText> result, Attributes attributes) - throws FileNotFoundException, - IOException { + throws IOException { TemporaryBuffer rawMerged = null; try { rawMerged = doMerge(result); @@ -1046,30 +1535,26 @@ public class ResolveMerger extends ThreeWayMerger { // A conflict occurred, the file will contain conflict markers // the index will be populated with the three stages and the // workdir (if used) contains the halfway merged content. - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + addConflict(base, ours, theirs); mergeResults.put(tw.getPathString(), result); return; } // No conflict occurred, the file will contain fully merged content. // The index will be populated with the new merged version. - DirCacheEntry dce = new DirCacheEntry(tw.getPathString()); - + Instant lastModified = mergedFile == null ? null + : nonNullRepo().getFS().lastModifiedInstant(mergedFile); // Set the mode for the new content. Fall back to REGULAR_FILE if // we can't merge modes of OURS and THEIRS. int newMode = mergeFileModes(tw.getRawMode(0), tw.getRawMode(1), tw.getRawMode(2)); - dce.setFileMode(newMode == FileMode.MISSING.getBits() - ? FileMode.REGULAR_FILE : FileMode.fromBits(newMode)); - if (mergedFile != null) { - dce.setLastModified( - nonNullRepo().getFS().lastModifiedInstant(mergedFile)); - dce.setLength((int) mergedFile.length()); - } - dce.setObjectId(insertMergeResult(rawMerged, attributes)); - builder.add(dce); + FileMode mode = newMode == FileMode.MISSING.getBits() + ? FileMode.REGULAR_FILE : FileMode.fromBits(newMode); + workTreeUpdater.insertToIndex(rawMerged.openInputStream(), + tw.getPathString().getBytes(UTF_8), mode, + DirCacheEntry.STAGE_0, lastModified, + (int) rawMerged.length(), + attributes.get(Constants.ATTR_MERGE)); } finally { if (rawMerged != null) { rawMerged.destroy(); @@ -1085,34 +1570,28 @@ public class ResolveMerger extends ThreeWayMerger { * @param attributes * the files .gitattributes entries * @return the working tree file to which the merged content was written. - * @throws FileNotFoundException * @throws IOException */ private File writeMergedFile(TemporaryBuffer rawMerged, Attributes attributes) - throws FileNotFoundException, IOException { + throws IOException { File workTree = nonNullRepo().getWorkTree(); FS fs = nonNullRepo().getFS(); File of = new File(workTree, tw.getPathString()); File parentFolder = of.getParentFile(); + EolStreamType eol = workTreeUpdater.detectCheckoutStreamType(attributes); if (!fs.exists(parentFolder)) { parentFolder.mkdirs(); } - EolStreamType streamType = EolStreamTypeUtil.detectStreamType( - OperationType.CHECKOUT_OP, workingTreeOptions, - attributes); - try (OutputStream os = EolStreamTypeUtil.wrapOutputStream( - new BufferedOutputStream(new FileOutputStream(of)), - streamType)) { - rawMerged.writeTo(os, null); - } + workTreeUpdater.updateFileWithContent(rawMerged::openInputStream, + eol, tw.getSmudgeCommand(attributes), of.getPath(), of); return of; } private TemporaryBuffer doMerge(MergeResult<RawText> result) throws IOException { TemporaryBuffer.LocalFile buf = new TemporaryBuffer.LocalFile( - db != null ? nonNullRepo().getDirectory() : null, inCoreLimit); + db != null ? nonNullRepo().getDirectory() : null, workTreeUpdater.getInCoreFileSizeLimit()); boolean success = false; try { new MergeFormatter().formatMerge(buf, result, @@ -1127,16 +1606,6 @@ public class ResolveMerger extends ThreeWayMerger { return buf; } - private ObjectId insertMergeResult(TemporaryBuffer buf, - Attributes attributes) throws IOException { - InputStream in = buf.openInputStream(); - try (LfsInputStream is = LfsFactory.getInstance().applyCleanFilter( - getRepository(), in, - buf.length(), attributes.get(Constants.ATTR_MERGE))) { - return getObjectInserter().insert(OBJ_BLOB, is.getLength(), is); - } - } - /** * Try to merge filemodes. If only ours or theirs have changed the mode * (compared to base) we choose that one. If ours and theirs have equal @@ -1154,22 +1623,26 @@ public class ResolveMerger extends ThreeWayMerger { * conflict */ private int mergeFileModes(int modeB, int modeO, int modeT) { - if (modeO == modeT) + if (modeO == modeT) { return modeO; - if (modeB == modeO) + } + if (modeB == modeO) { // Base equal to Ours -> chooses Theirs if that is not missing return (modeT == FileMode.MISSING.getBits()) ? modeO : modeT; - if (modeB == modeT) + } + if (modeB == modeT) { // Base equal to Theirs -> chooses Ours if that is not missing return (modeO == FileMode.MISSING.getBits()) ? modeT : modeO; + } return FileMode.MISSING.getBits(); } private RawText getRawText(ObjectId id, Attributes attributes) throws IOException, BinaryBlobException { - if (id.equals(ObjectId.zeroId())) - return new RawText(new byte[] {}); + if (id.equals(ObjectId.zeroId())) { + return new RawText(new byte[]{}); + } ObjectLoader loader = LfsFactory.getInstance().applySmudgeFilter( getRepository(), reader.open(id, OBJ_BLOB), @@ -1233,7 +1706,7 @@ public class ResolveMerger extends ThreeWayMerger { * superset of the files listed by {@link #getUnmergedPaths()}. */ public List<String> getModifiedFiles() { - return modifiedFiles; + return workTreeUpdater != null ? workTreeUpdater.getModifiedFiles() : modifiedFiles; } /** @@ -1247,7 +1720,7 @@ public class ResolveMerger extends ThreeWayMerger { * for this path. */ public Map<String, DirCacheEntry> getToBeCheckedOut() { - return toBeCheckedOut; + return workTreeUpdater.getToBeCheckedOut(); } /** @@ -1297,7 +1770,6 @@ public class ResolveMerger extends ThreeWayMerger { */ public void setDirCache(DirCache dc) { this.dircache = dc; - implicitDirCache = false; } /** @@ -1352,53 +1824,48 @@ public class ResolveMerger extends ThreeWayMerger { protected boolean mergeTrees(AbstractTreeIterator baseTree, RevTree headTree, RevTree mergeTree, boolean ignoreConflicts) throws IOException { + try { + workTreeUpdater = inCore ? + WorkTreeUpdater.createInCoreWorkTreeUpdater(db, dircache, getObjectInserter()) : + WorkTreeUpdater.createWorkTreeUpdater(db, dircache); + dircache = workTreeUpdater.getLockedDirCache(); + tw = new NameConflictTreeWalk(db, reader); + + tw.addTree(baseTree); + tw.setHead(tw.addTree(headTree)); + tw.addTree(mergeTree); + DirCacheBuildIterator buildIt = workTreeUpdater.createDirCacheBuildIterator(); + int dciPos = tw.addTree(buildIt); + if (workingTreeIterator != null) { + tw.addTree(workingTreeIterator); + workingTreeIterator.setDirCacheIterator(tw, dciPos); + } else { + tw.setFilter(TreeFilter.ANY_DIFF); + } - builder = dircache.builder(); - DirCacheBuildIterator buildIt = new DirCacheBuildIterator(builder); - - tw = new NameConflictTreeWalk(db, reader); - tw.addTree(baseTree); - tw.setHead(tw.addTree(headTree)); - tw.addTree(mergeTree); - int dciPos = tw.addTree(buildIt); - if (workingTreeIterator != null) { - tw.addTree(workingTreeIterator); - workingTreeIterator.setDirCacheIterator(tw, dciPos); - } else { - tw.setFilter(TreeFilter.ANY_DIFF); - } + if (!mergeTreeWalk(tw, ignoreConflicts)) { + return false; + } - if (!mergeTreeWalk(tw, ignoreConflicts)) { + workTreeUpdater.writeWorkTreeChanges(true); + if (getUnmergedPaths().isEmpty() && !failed()) { + WorkTreeUpdater.Result result = workTreeUpdater.writeIndexChanges(); + resultTree = result.getTreeId(); + modifiedFiles = result.getModifiedFiles(); + for (String f : result.getFailedToDelete()) { + failingPaths.put(f, MergeFailureReason.COULD_NOT_DELETE); + } + return result.getFailedToDelete().isEmpty(); + } + resultTree = null; return false; - } - - if (!inCore) { - // No problem found. The only thing left to be done is to - // checkout all files from "theirs" which have been selected to - // go into the new index. - checkout(); - - // All content-merges are successfully done. If we can now write the - // new index we are on quite safe ground. Even if the checkout of - // files coming from "theirs" fails the user can work around such - // failures by checking out the index again. - if (!builder.commit()) { - cleanUp(); - throw new IndexWriteException(); + } finally { + if(modifiedFiles.isEmpty()) { + modifiedFiles = workTreeUpdater.getModifiedFiles(); } - builder = null; - - } else { - builder.finish(); - builder = null; + workTreeUpdater.close(); + workTreeUpdater = null; } - - if (getUnmergedPaths().isEmpty() && !failed()) { - resultTree = dircache.writeTree(getObjectInserter()); - return true; - } - resultTree = null; - return false; } /** @@ -1419,8 +1886,8 @@ public class ResolveMerger extends ThreeWayMerger { boolean hasAttributeNodeProvider = treeWalk .getAttributesNodeProvider() != null; while (treeWalk.next()) { - Attributes[] attributes = { NO_ATTRIBUTES, NO_ATTRIBUTES, - NO_ATTRIBUTES }; + Attributes[] attributes = {NO_ATTRIBUTES, NO_ATTRIBUTES, + NO_ATTRIBUTES}; if (hasAttributeNodeProvider) { attributes[T_BASE] = treeWalk.getAttributes(T_BASE); attributes[T_OURS] = treeWalk.getAttributes(T_OURS); @@ -1434,11 +1901,12 @@ public class ResolveMerger extends ThreeWayMerger { hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, WorkingTreeIterator.class) : null, ignoreConflicts, attributes)) { - cleanUp(); + workTreeUpdater.revertModifiedFiles(); return false; } - if (treeWalk.isSubtree() && enterSubtree) + if (treeWalk.isSubtree() && enterSubtree) { treeWalk.enterSubtree(); + } } return true; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java index e0c7bdd396..dda108bc69 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java @@ -66,7 +66,7 @@ abstract class AbstractRevQueue extends Generator { * flag that controls admission to the queue. */ public final void addParents(RevCommit c, RevFlag queueControl) { - final RevCommit[] pList = c.parents; + final RevCommit[] pList = c.getParents(); if (pList == null) { return; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java index 8b78d062b5..bdf63ff10b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java @@ -76,11 +76,12 @@ class BoundaryGenerator extends Generator { IncorrectObjectTypeException, IOException { RevCommit c = source.next(); if (c != null) { - for (int i = 0; i < c.parents.length; i++) { + int n = c.getParentCount(); + for (int i = 0; i < n; i++) { if (firstParent && i > 0) { break; } - RevCommit p = c.parents[i]; + RevCommit p = c.getParent(i); if ((p.flags & UNINTERESTING) != 0) { held.add(p); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java index 3a2cb8b0f9..ec0824cb0b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java @@ -164,11 +164,12 @@ class DepthGenerator extends Generator { int newDepth = c.depth + 1; - for (int i = 0; i < c.parents.length; i++) { + int n = c.getParentCount(); + for (int i = 0; i < n; i++) { if (firstParent && i > 0) { break; } - RevCommit p = c.parents[i]; + RevCommit p = c.getParent(i); DepthWalk.Commit dp = (DepthWalk.Commit) p; // If no depth has been assigned to this commit, assign diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java index c0fea75777..a213dd47c6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java @@ -114,7 +114,7 @@ class MergeBaseGenerator extends Generator { return null; } - for (RevCommit p : c.parents) { + for (RevCommit p : c.getParents()) { if ((p.flags & IN_PENDING) != 0) continue; if ((p.flags & PARSED) == 0) @@ -180,7 +180,7 @@ class MergeBaseGenerator extends Generator { private void carryOntoHistoryInnerLoop(RevCommit c, int carry) { for (;;) { - RevCommit[] parents = c.parents; + RevCommit[] parents = c.getParents(); if (parents == null || parents.length == 0) { break; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java index add387de03..a49f787316 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java @@ -108,8 +108,9 @@ class PendingGenerator extends Generator { produce = filter.include(walker, c); } - for (int i = 0; i < c.parents.length; i++) { - RevCommit p = c.parents[i]; + int parentCount = c.getParentCount(); + for (int i = 0; i < parentCount; i++) { + RevCommit p = c.getParent(i); // If the commit is uninteresting, don't try to prune // parents because we want the maximal uninteresting set. if (firstParent && i > 0 && (c.flags & UNINTERESTING) == 0) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java index 82725f3db9..6b644cef90 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java @@ -36,6 +36,11 @@ import org.eclipse.jgit.util.StringUtils; /** * A commit reference to a commit in the DAG. + * + * The state of the RevCommit isn't populated until the commit is parsed. The + * newly created RevCommit is unparsed and only has an objectId reference. Other + * states like parents, trees, commit ident, commit message, etc. are + * populated/available when the commit is parsed. */ public class RevCommit extends RevObject { private static final int STACK_DEPTH = 500; @@ -102,7 +107,14 @@ public class RevCommit extends RevObject { private RevTree tree; - RevCommit[] parents; + /** + * Avoid accessing this field directly. Use method + * {@link RevCommit#getParents()} instead. RevCommit does not allow parents + * to be overridden and altering parent(s) is not supported. + * + * @since 6.3 + */ + protected RevCommit[] parents; int commitTime; // An int here for performance, overflows in 2038 @@ -146,7 +158,7 @@ public class RevCommit extends RevObject { tree = walk.lookupTree(idBuffer); int ptr = 46; - if (parents == null) { + if (getParents() == null) { RevCommit[] pList = new RevCommit[1]; int nParents = 0; for (;;) { @@ -210,8 +222,8 @@ public class RevCommit extends RevObject { } private static FIFORevQueue carryFlags1(RevCommit c, int carry, int depth) { - for(;;) { - RevCommit[] pList = c.parents; + for (;;) { + RevCommit[] pList = c.getParents(); if (pList == null || pList.length == 0) return null; if (pList.length != 1) { @@ -259,7 +271,7 @@ public class RevCommit extends RevObject { // Commits in q have non-null parent arrays and have set all // flags in carry. This loop finishes copying over the graph. for (RevCommit c; (c = q.next()) != null;) { - for (RevCommit p : c.parents) + for (RevCommit p : c.getParents()) carryOneStep(q, carry, p); } } @@ -267,7 +279,7 @@ public class RevCommit extends RevObject { private static void carryOneStep(FIFORevQueue q, int carry, RevCommit c) { if ((c.flags & carry) != carry) { c.flags |= carry; - if (c.parents != null) + if (c.getParents() != null) q.add(c); } } @@ -313,8 +325,8 @@ public class RevCommit extends RevObject { * * @return number of parents; always a positive value but can be 0. */ - public final int getParentCount() { - return parents.length; + public int getParentCount() { + return parents == null ? 0 : parents.length; } /** @@ -327,7 +339,7 @@ public class RevCommit extends RevObject { * @throws java.lang.ArrayIndexOutOfBoundsException * an invalid parent index was specified. */ - public final RevCommit getParent(int nth) { + public RevCommit getParent(int nth) { return parents[nth]; } @@ -341,7 +353,7 @@ public class RevCommit extends RevObject { * * @return the array of parents. */ - public final RevCommit[] getParents() { + public RevCommit[] getParents() { return parents; } @@ -353,9 +365,9 @@ public class RevCommit extends RevObject { * this buffer should be very careful to ensure they do not modify its * contents during their use of it. * - * @return the raw unparsed commit body. This is <b>NOT A COPY</b>. - * Altering the contents of this buffer may alter the walker's - * knowledge of this commit, and the results it produces. + * @return the raw unparsed commit body. This is <b>NOT A COPY</b>. Altering + * the contents of this buffer may alter the walker's knowledge of + * this commit, and the results it produces. */ public final byte[] getRawBuffer() { return buffer; @@ -380,7 +392,7 @@ public class RevCommit extends RevObject { */ public final byte[] getRawGpgSignature() { final byte[] raw = buffer; - final byte[] header = {'g', 'p', 'g', 's', 'i', 'g'}; + final byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' }; final int start = RawParseUtils.headerStart(header, raw, 0); if (start < 0) { return null; 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 a25948e50b..8da36c5243 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java @@ -154,7 +154,9 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { */ static final int TREE_REV_FILTER_APPLIED = 1 << 7; - /** Number of flag bits we keep internal for our own use. See above flags. */ + /** + * Number of flag bits we keep internal for our own use. See above flags. + */ static final int RESERVED_FLAGS = 8; private static final int APP_FLAGS = -1 & ~((1 << RESERVED_FLAGS) - 1); @@ -196,9 +198,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { boolean shallowCommitsInitialized; private enum GetMergedIntoStrategy { - RETURN_ON_FIRST_FOUND, - RETURN_ON_FIRST_NOT_FOUND, - EVALUATE_ALL + RETURN_ON_FIRST_FOUND, RETURN_ON_FIRST_NOT_FOUND, EVALUATE_ALL } /** @@ -219,8 +219,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * * @param or * the reader the walker will obtain data from. The reader is not - * closed when the walker is closed (but is closed by {@link - * #dispose()}. + * closed when the walker is closed (but is closed by + * {@link #dispose()}. */ public RevWalk(ObjectReader or) { this(or, false); @@ -381,9 +381,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * @throws java.io.IOException * a pack file or loose object could not be read. */ - public void markUninteresting(RevCommit c) - throws MissingObjectException, IncorrectObjectTypeException, - IOException { + public void markUninteresting(RevCommit c) throws MissingObjectException, + IncorrectObjectTypeException, IOException { c.flags |= UNINTERESTING; carryFlagsImpl(c); markStart(c); @@ -392,8 +391,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { /** * Determine if a commit is reachable from another commit. * <p> - * A commit <code>base</code> is an ancestor of <code>tip</code> if we - * can find a path of commits that leads from <code>tip</code> and ends at + * A commit <code>base</code> is an ancestor of <code>tip</code> if we can + * find a path of commits that leads from <code>tip</code> and ends at * <code>base</code>. * <p> * This utility function resets the walker, inserts the two supplied @@ -462,7 +461,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * @since 5.12 */ public List<Ref> getMergedInto(RevCommit commit, Collection<Ref> refs) - throws IOException{ + throws IOException { return getMergedInto(commit, refs, NullProgressMonitor.INSTANCE); } @@ -486,9 +485,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * @since 5.12 */ public List<Ref> getMergedInto(RevCommit commit, Collection<Ref> refs, - ProgressMonitor monitor) throws IOException{ - return getMergedInto(commit, refs, - GetMergedIntoStrategy.EVALUATE_ALL, + ProgressMonitor monitor) throws IOException { + return getMergedInto(commit, refs, GetMergedIntoStrategy.EVALUATE_ALL, monitor); } @@ -531,12 +529,11 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { throws IOException { return getMergedInto(commit, refs, GetMergedIntoStrategy.RETURN_ON_FIRST_NOT_FOUND, - NullProgressMonitor.INSTANCE).size() - == refs.size(); + NullProgressMonitor.INSTANCE).size() == refs.size(); } private List<Ref> getMergedInto(RevCommit needle, Collection<Ref> haystacks, - Enum returnStrategy, ProgressMonitor monitor) throws IOException { + Enum returnStrategy, ProgressMonitor monitor) throws IOException { List<Ref> result = new ArrayList<>(); List<RevCommit> uninteresting = new ArrayList<>(); List<RevCommit> marked = new ArrayList<>(); @@ -547,7 +544,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { reset(~freeFlags & APP_FLAGS); filter = RevFilter.ALL; treeFilter = TreeFilter.ALL; - for (Ref r: haystacks) { + for (Ref r : haystacks) { if (monitor.isCancelled()) { return result; } @@ -574,7 +571,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { break; } } - if(!commitFound){ + if (!commitFound) { markUninteresting(c); uninteresting.add(c); if (returnStrategy == GetMergedIntoStrategy.RETURN_ON_FIRST_NOT_FOUND) { @@ -990,9 +987,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * a pack file or loose object could not be read. */ @NonNull - public RevCommit parseCommit(AnyObjectId id) - throws MissingObjectException, IncorrectObjectTypeException, - IOException { + public RevCommit parseCommit(AnyObjectId id) throws MissingObjectException, + IncorrectObjectTypeException, IOException { RevObject c = peel(parseAny(id)); if (!(c instanceof RevCommit)) throw new IncorrectObjectTypeException(id.toObjectId(), @@ -1018,9 +1014,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * a pack file or loose object could not be read. */ @NonNull - public RevTree parseTree(AnyObjectId id) - throws MissingObjectException, IncorrectObjectTypeException, - IOException { + public RevTree parseTree(AnyObjectId id) throws MissingObjectException, + IncorrectObjectTypeException, IOException { RevObject c = peel(parseAny(id)); final RevTree t; @@ -1274,8 +1269,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * @throws java.io.IOException * a pack file or loose object could not be read. */ - public RevObject peel(RevObject obj) throws MissingObjectException, - IOException { + public RevObject peel(RevObject obj) + throws MissingObjectException, IOException { while (obj instanceof RevTag) { parseHeaders(obj); obj = ((RevTag) obj).getObject(); @@ -1304,9 +1299,9 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { int allocFlag() { if (freeFlags == 0) - throw new IllegalArgumentException(MessageFormat.format( - JGitText.get().flagsAlreadyCreated, - Integer.valueOf(32 - RESERVED_FLAGS))); + throw new IllegalArgumentException( + MessageFormat.format(JGitText.get().flagsAlreadyCreated, + Integer.valueOf(32 - RESERVED_FLAGS))); final int m = Integer.lowestOneBit(freeFlags); freeFlags &= ~m; return m; @@ -1323,9 +1318,11 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { */ public void carry(RevFlag flag) { if ((freeFlags & flag.mask) != 0) - throw new IllegalArgumentException(MessageFormat.format(JGitText.get().flagIsDisposed, flag.name)); + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().flagIsDisposed, flag.name)); if (flag.walker != this) - throw new IllegalArgumentException(MessageFormat.format(JGitText.get().flagNotFromThis, flag.name)); + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().flagNotFromThis, flag.name)); carryFlags |= flag.mask; } @@ -1359,9 +1356,11 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { */ public final void retainOnReset(RevFlag flag) { if ((freeFlags & flag.mask) != 0) - throw new IllegalArgumentException(MessageFormat.format(JGitText.get().flagIsDisposed, flag.name)); + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().flagIsDisposed, flag.name)); if (flag.walker != this) - throw new IllegalArgumentException(MessageFormat.format(JGitText.get().flagNotFromThis, flag.name)); + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().flagNotFromThis, flag.name)); retainOnReset |= flag.mask; } @@ -1496,9 +1495,9 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { final RevCommit c = q.next(); if (c == null) break; - if (c.parents == null) + if (c.getParents() == null) continue; - for (RevCommit p : c.parents) { + for (RevCommit p : c.getParents()) { if ((p.flags & clearFlags) == 0) continue; p.flags &= retainFlags; @@ -1538,7 +1537,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { * Like {@link #next()}, but if a checked exception is thrown during the * walk it is rethrown as a {@link RevWalkException}. * - * @throws RevWalkException if an {@link IOException} was thrown. + * @throws RevWalkException + * if an {@link IOException} was thrown. * @return next most recent commit; null if traversal is over. */ @Nullable @@ -1598,7 +1598,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { protected void assertNotStarted() { if (isNotStarted()) return; - throw new IllegalStateException(JGitText.get().outputHasAlreadyBeenStarted); + throw new IllegalStateException( + JGitText.get().outputHasAlreadyBeenStarted); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java index 1adef07ad9..2c88bb872e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java @@ -72,7 +72,7 @@ class RewriteGenerator extends Generator { applyFilterToParents(c); boolean rewrote = false; - final RevCommit[] pList = c.parents; + final RevCommit[] pList = c.getParents(); final int nParents = pList.length; for (int i = 0; i < nParents; i++) { final RevCommit oldp = pList[i]; @@ -108,7 +108,7 @@ class RewriteGenerator extends Generator { private void applyFilterToParents(RevCommit c) throws MissingObjectException, IncorrectObjectTypeException, IOException { - for (RevCommit parent : c.parents) { + for (RevCommit parent : c.getParents()) { while ((parent.flags & RevWalk.TREE_REV_FILTER_APPLIED) == 0) { RevCommit n = source.next(); @@ -130,7 +130,7 @@ class RewriteGenerator extends Generator { IncorrectObjectTypeException, IOException { for (;;) { - if (p.parents.length > 1) { + if (p.getParentCount() > 1) { // This parent is a merge, so keep it. // return p; @@ -150,15 +150,15 @@ class RewriteGenerator extends Generator { return p; } - if (p.parents.length == 0) { + if (p.getParentCount() == 0) { // We can't go back any further, other than to // just delete the parent entirely. // return null; } - applyFilterToParents(p.parents[0]); - p = p.parents[0]; + applyFilterToParents(p.getParent(0)); + p = p.getParent(0); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoNonIntermixSortGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoNonIntermixSortGenerator.java index 4f6d417ed1..452545a04e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoNonIntermixSortGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoNonIntermixSortGenerator.java @@ -48,7 +48,7 @@ class TopoNonIntermixSortGenerator extends Generator { break; } if ((c.flags & TOPO_QUEUED) == 0) { - for (RevCommit p : c.parents) { + for (RevCommit p : c.getParents()) { p.inDegree++; if (firstParent) { @@ -94,7 +94,7 @@ class TopoNonIntermixSortGenerator extends Generator { continue; } - for (RevCommit p : c.parents) { + for (RevCommit p : c.getParents()) { if (--p.inDegree == 0 && (p.flags & TOPO_QUEUED) != 0) { // The parent has no unproduced interesting children. unpop // the parent so it goes right behind this child. This means diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java index 7a5db43a7a..4739f78fc0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java @@ -47,7 +47,7 @@ class TopoSortGenerator extends Generator { if (c == null) { break; } - for (RevCommit p : c.parents) { + for (RevCommit p : c.getParents()) { p.inDegree++; if (firstParent) { break; @@ -86,7 +86,7 @@ class TopoSortGenerator extends Generator { // All of our children have already produced, // so it is OK for us to produce now as well. // - for (RevCommit p : c.parents) { + for (RevCommit p : c.getParents()) { if (--p.inDegree == 0 && (p.flags & TOPO_DELAY) != 0) { // This parent tried to come before us, but we are // his last child. unpop the parent so it goes right diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java index 92d72268d1..f921449ffa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java @@ -44,6 +44,7 @@ public class TreeRevFilter extends RevFilter { private static final int FILTER_APPLIED = RevWalk.TREE_REV_FILTER_APPLIED; private final int rewriteFlag; + private final TreeWalk pathFilter; /** @@ -62,10 +63,9 @@ public class TreeRevFilter extends RevFilter { this(walker, t, 0); } - /** - * Create a filter for the first phase of a parent-rewriting limited revision - * walk. + * Create a filter for the first phase of a parent-rewriting limited + * revision walk. * <p> * This filter is ANDed to evaluate before all other filters and ties the * configured {@link TreeFilter} into the revision walking process. @@ -79,8 +79,8 @@ public class TreeRevFilter extends RevFilter { * @param walker * walker used for reading trees. * @param t - * filter to compare against any changed paths in each commit. If a - * {@link FollowFilter}, will be replaced with a new filter + * filter to compare against any changed paths in each commit. If + * a {@link FollowFilter}, will be replaced with a new filter * following new paths after a rename. * @param rewriteFlag * flag to color commits to be removed from the simplified DAT. @@ -106,12 +106,12 @@ public class TreeRevFilter extends RevFilter { c.flags |= FILTER_APPLIED; // Reset the tree filter to scan this commit and parents. // - RevCommit[] pList = c.parents; + RevCommit[] pList = c.getParents(); int nParents = pList.length; TreeWalk tw = pathFilter; ObjectId[] trees = new ObjectId[nParents + 1]; for (int i = 0; i < nParents; i++) { - RevCommit p = c.parents[i]; + RevCommit p = c.getParent(i); if ((p.flags & PARSED) == 0) { p.parseHeaders(walker); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java index 3f167ccce2..be36d2b834 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java @@ -12,16 +12,30 @@ package org.eclipse.jgit.transport; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DELIM; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_NOT; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_SINCE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DONE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_END; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ERR; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_HAVE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_SHALLOW; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_UNSHALLOW; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_WANT; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.MessageFormat; +import java.time.Instant; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.eclipse.jgit.errors.PackProtocolException; @@ -32,6 +46,7 @@ import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectDatabase; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ProgressMonitor; @@ -76,7 +91,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection /** * Maximum number of 'have' lines to send before giving up. * <p> - * During {@link #negotiate(ProgressMonitor)} we send at most this many + * During {@link #negotiate(ProgressMonitor, boolean, Set)} we send at most this many * commits to the remote peer as 'have' lines without an ACK response before * we give up. */ @@ -210,6 +225,12 @@ public abstract class BasePackFetchConnection extends BasePackConnection private int maxHaves; + private Integer depth; + + private Instant deepenSince; + + private List<String> deepenNots; + /** * RPC state, if {@link BasePackConnection#statelessRPC} is true or protocol * V2 is used. @@ -246,6 +267,9 @@ public abstract class BasePackFetchConnection extends BasePackConnection includeTags = transport.getTagOpt() != TagOpt.NO_TAGS; thinPack = transport.isFetchThin(); filterSpec = transport.getFilterSpec(); + depth = transport.getDepth(); + deepenSince = transport.getDeepenSince(); + deepenNots = transport.getDeepenNots(); if (local != null) { walk = new RevWalk(local); @@ -385,9 +409,17 @@ public abstract class BasePackFetchConnection extends BasePackConnection } PacketLineOut output = statelessRPC ? pckState : pckOut; if (sendWants(want, output)) { + boolean mayHaveShallow = depth != null || deepenSince != null || !deepenNots.isEmpty(); + Set<ObjectId> shallowCommits = local.getObjectDatabase().getShallowCommits(); + if (isCapableOf(GitProtocolConstants.CAPABILITY_SHALLOW)) { + sendShallow(shallowCommits, output); + } else if (mayHaveShallow) { + throw new PackProtocolException(JGitText.get().shallowNotSupported); + } output.end(); outNeedsEnd = false; - negotiate(monitor); + + negotiate(monitor, mayHaveShallow, shallowCommits); clearState(); @@ -424,10 +456,18 @@ public abstract class BasePackFetchConnection extends BasePackConnection for (String capability : getCapabilitiesV2(capabilities)) { pckState.writeString(capability); } + if (!sendWants(want, pckState)) { // We already have everything we wanted. return; } + + Set<ObjectId> shallowCommits = local.getObjectDatabase().getShallowCommits(); + if (capabilities.contains(GitProtocolConstants.CAPABILITY_SHALLOW)) { + sendShallow(shallowCommits, pckState); + } else if (depth != null || deepenSince != null || !deepenNots.isEmpty()) { + throw new PackProtocolException(JGitText.get().shallowNotSupported); + } // If we send something, we always close it properly ourselves. outNeedsEnd = false; @@ -455,10 +495,21 @@ public abstract class BasePackFetchConnection extends BasePackConnection clearState(); String line = pckIn.readString(); // If we sent a done, we may have an error reply here. - if (sentDone && line.startsWith("ERR ")) { //$NON-NLS-1$ + if (sentDone && line.startsWith(PACKET_ERR)) { throw new RemoteRepositoryException(uri, line.substring(4)); } - // "shallow-info", "wanted-refs", and "packfile-uris" would have to be + + if (GitProtocolConstants.SECTION_SHALLOW_INFO.equals(line)) { + line = handleShallowUnshallow(shallowCommits, pckIn); + if (!PacketLineIn.isDelimiter(line)) { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().expectedGot, PACKET_DELIM, + line)); + } + line = pckIn.readString(); + } + + // "wanted-refs" and "packfile-uris" would have to be // handled here in that order. if (!GitProtocolConstants.SECTION_PACKFILE.equals(line)) { throw new PackProtocolException( @@ -494,7 +545,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection if (c == null) { break; } - output.writeString("have " + c.getId().name() + '\n'); //$NON-NLS-1$ + output.writeString(PACKET_HAVE + c.getId().name() + '\n'); n++; if (n % 10 == 0 && monitor.isCancelled()) { throw new CancelledException(); @@ -505,7 +556,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection || (fetchState.hadAcks && fetchState.havesWithoutAck > MAX_HAVES) || fetchState.havesTotal > maxHaves) { - output.writeString("done\n"); //$NON-NLS-1$ + output.writeString(PACKET_DONE + '\n'); output.end(); return true; } @@ -572,11 +623,12 @@ public abstract class BasePackFetchConnection extends BasePackConnection if (gotReady) { if (!PacketLineIn.isDelimiter(line)) { throw new PackProtocolException(MessageFormat - .format(JGitText.get().expectedGot, "0001", line)); //$NON-NLS-1$ + .format(JGitText.get().expectedGot, PACKET_DELIM, + line)); } } else if (!PacketLineIn.isEnd(line)) { throw new PackProtocolException(MessageFormat - .format(JGitText.get().expectedGot, "0000", line)); //$NON-NLS-1$ + .format(JGitText.get().expectedGot, PACKET_END, line)); } return gotReady; } @@ -672,21 +724,23 @@ public abstract class BasePackFetchConnection extends BasePackConnection if (objectId == null) { continue; } - try { - if (walk.parseAny(objectId).has(REACHABLE)) { - // We already have this object. Asking for it is - // not a very good idea. - // - continue; + // if depth is set we need to fetch the objects even if they are already available + if (transport.getDepth() == null) { + try { + if (walk.parseAny(objectId).has(REACHABLE)) { + // We already have this object. Asking for it is + // not a very good idea. + // + continue; + } + } catch (IOException err) { + // Its OK, we don't have it, but we want to fix that + // by fetching the object from the other side. } - } catch (IOException err) { - // Its OK, we don't have it, but we want to fix that - // by fetching the object from the other side. } final StringBuilder line = new StringBuilder(46); - line.append("want "); //$NON-NLS-1$ - line.append(objectId.name()); + line.append(PACKET_WANT).append(objectId.name()); if (first && TransferConfig.ProtocolVersion.V0 .equals(getProtocolVersion())) { line.append(enableCapabilities()); @@ -773,8 +827,8 @@ public abstract class BasePackFetchConnection extends BasePackConnection return line.toString(); } - private void negotiate(ProgressMonitor monitor) throws IOException, - CancelledException { + private void negotiate(ProgressMonitor monitor, boolean mayHaveShallow, Set<ObjectId> shallowCommits) + throws IOException, CancelledException { final MutableObjectId ackId = new MutableObjectId(); int resultsPending = 0; int havesSent = 0; @@ -795,7 +849,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection } ObjectId o = c.getId(); - pckOut.writeString("have " + o.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + pckOut.writeString(PACKET_HAVE + o.name() + '\n'); havesSent++; havesSinceLastContinue++; @@ -898,7 +952,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection // loop above while in the middle of a request. This allows us // to just write done immediately. // - pckOut.writeString("done\n"); //$NON-NLS-1$ + pckOut.writeString(PACKET_DONE + '\n'); pckOut.flush(); } @@ -911,6 +965,14 @@ public abstract class BasePackFetchConnection extends BasePackConnection resultsPending++; } + if (mayHaveShallow) { + String line = handleShallowUnshallow(shallowCommits, pckIn); + if (!PacketLineIn.isEnd(line)) { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().expectedGot, PACKET_END, line)); + } + } + READ_RESULT: while (resultsPending > 0 || multiAck != MultiAck.OFF) { final AckNackResult anr = pckIn.readACK(ackId); resultsPending--; @@ -992,7 +1054,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection private void markCommon(RevObject obj, AckNackResult anr, boolean useState) throws IOException { if (useState && anr == AckNackResult.ACK_COMMON && !obj.has(STATE)) { - pckState.writeString("have " + obj.name() + '\n'); //$NON-NLS-1$ + pckState.writeString(PACKET_HAVE + obj.name() + '\n'); obj.add(STATE); } obj.add(COMMON); @@ -1025,6 +1087,55 @@ public abstract class BasePackFetchConnection extends BasePackConnection } } + private void sendShallow(Set<ObjectId> shallowCommits, PacketLineOut output) + throws IOException { + for (ObjectId shallowCommit : shallowCommits) { + output.writeString(PACKET_SHALLOW + shallowCommit.name()); + } + + if (depth != null) { + output.writeString(PACKET_DEEPEN + depth); + } + + if (deepenSince != null) { + output.writeString( + PACKET_DEEPEN_SINCE + deepenSince.getEpochSecond()); + } + + if (deepenNots != null) { + for (String deepenNotRef : deepenNots) { + output.writeString(PACKET_DEEPEN_NOT + deepenNotRef); + } + } + } + + private String handleShallowUnshallow( + Set<ObjectId> advertisedShallowCommits, PacketLineIn input) + throws IOException { + String line = input.readString(); + ObjectDatabase objectDatabase = local.getObjectDatabase(); + HashSet<ObjectId> newShallowCommits = new HashSet<>( + advertisedShallowCommits); + while (!PacketLineIn.isDelimiter(line) && !PacketLineIn.isEnd(line)) { + if (line.startsWith(PACKET_SHALLOW)) { + newShallowCommits.add(ObjectId + .fromString(line.substring(PACKET_SHALLOW.length()))); + } else if (line.startsWith(PACKET_UNSHALLOW)) { + ObjectId unshallow = ObjectId + .fromString(line.substring(PACKET_UNSHALLOW.length())); + if (!advertisedShallowCommits.contains(unshallow)) { + throw new PackProtocolException(MessageFormat.format( + JGitText.get().notShallowedUnshallow, + unshallow.name())); + } + newShallowCommits.remove(unshallow); + } + line = input.readString(); + } + objectDatabase.setShallowCommits(newShallowCommits); + return line; + } + /** * Notification event delivered just before the pack is received from the * network. This event can be used by RPC such as {@link org.eclipse.jgit.transport.TransportHttp} to diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java index 7bface49d9..28c3b6a0fa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java @@ -1,6 +1,6 @@ /* * 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 @@ -393,7 +393,7 @@ class FetchProcess { ow.markUninteresting(ow.parseAny(ref.getObjectId())); ow.checkConnectivity(); } - return true; + return transport.getDepth() == null; // if depth is set we need to request objects that are already available } catch (MissingObjectException e) { return false; } catch (IOException e) { @@ -516,8 +516,10 @@ class FetchProcess { } if (spec.getDestination() != null) { final TrackingRefUpdate tru = createUpdate(spec, newId); - if (newId.equals(tru.getOldObjectId())) + // if depth is set we need to update the ref + if (newId.equals(tru.getOldObjectId()) && transport.getDepth() == null) { return; + } localUpdates.add(tru); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java index 9ebc722ffe..0663c5141c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Google LLC. and others + * Copyright (C) 2018, 2022 Google LLC. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -35,7 +35,7 @@ abstract class FetchRequest { final int deepenSince; - final List<String> deepenNotRefs; + final List<String> deepenNots; @Nullable final String agent; @@ -53,7 +53,7 @@ abstract class FetchRequest { * the filter spec * @param clientCapabilities * capabilities sent in the request - * @param deepenNotRefs + * @param deepenNots * Requests that the shallow clone/fetch should be cut at these * specific revisions instead of a depth. * @param deepenSince @@ -66,14 +66,14 @@ abstract class FetchRequest { @NonNull Set<ObjectId> clientShallowCommits, @NonNull FilterSpec filterSpec, @NonNull Set<String> clientCapabilities, int deepenSince, - @NonNull List<String> deepenNotRefs, @Nullable String agent) { + @NonNull List<String> deepenNots, @Nullable String agent) { this.wantIds = requireNonNull(wantIds); this.depth = depth; this.clientShallowCommits = requireNonNull(clientShallowCommits); this.filterSpec = requireNonNull(filterSpec); this.clientCapabilities = requireNonNull(clientCapabilities); this.deepenSince = deepenSince; - this.deepenNotRefs = requireNonNull(deepenNotRefs); + this.deepenNots = requireNonNull(deepenNots); this.agent = agent; } @@ -148,8 +148,8 @@ abstract class FetchRequest { * @return refs received in "deepen-not" lines. */ @NonNull - List<String> getDeepenNotRefs() { - return deepenNotRefs; + List<String> getDeepenNots() { + return deepenNots; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java index 91adb5e6ac..4decb79513 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Google LLC. and others + * Copyright (C) 2018, 2022 Google LLC. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -11,9 +11,10 @@ package org.eclipse.jgit.transport; import static java.util.Objects.requireNonNull; +import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.eclipse.jgit.annotations.NonNull; @@ -28,15 +29,20 @@ final class FetchV0Request extends FetchRequest { FetchV0Request(@NonNull Set<ObjectId> wantIds, int depth, @NonNull Set<ObjectId> clientShallowCommits, @NonNull FilterSpec filterSpec, - @NonNull Set<String> clientCapabilities, @Nullable String agent) { + @NonNull Set<String> clientCapabilities, int deepenSince, + @NonNull List<String> deepenNotRefs, @Nullable String agent) { super(wantIds, depth, clientShallowCommits, filterSpec, - clientCapabilities, 0, Collections.emptyList(), agent); + clientCapabilities, deepenSince, deepenNotRefs, agent); } static final class Builder { int depth; + final List<String> deepenNots = new ArrayList<>(); + + int deepenSince; + final Set<ObjectId> wantIds = new HashSet<>(); final Set<ObjectId> clientShallowCommits = new HashSet<>(); @@ -68,6 +74,50 @@ final class FetchV0Request extends FetchRequest { } /** + * @return depth set in the request (via a "deepen" line). Defaulting to + * 0 if not set. + */ + int getDepth() { + return depth; + } + + /** + * @return true if there has been at least one "deepen not" line in the + * request so far + */ + boolean hasDeepenNots() { + return !deepenNots.isEmpty(); + } + + /** + * @param deepenNot + * reference received in a "deepen not" line + * @return this builder + */ + Builder addDeepenNot(String deepenNot) { + deepenNots.add(deepenNot); + return this; + } + + /** + * @param value + * Unix timestamp received in a "deepen since" line + * @return this builder + */ + Builder setDeepenSince(int value) { + deepenSince = value; + return this; + } + + /** + * @return shallow since value, sent before in a "deepen since" line. 0 + * by default. + */ + int getDeepenSince() { + return deepenSince; + } + + /** * @param shallowOid * object id received in a "shallow" line * @return this builder @@ -110,7 +160,7 @@ final class FetchV0Request extends FetchRequest { FetchV0Request build() { return new FetchV0Request(wantIds, depth, clientShallowCommits, - filterSpec, clientCaps, agent); + filterSpec, clientCaps, deepenSince, deepenNots, agent); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java index 50fb9d2262..446a3bcc79 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Google LLC. and others + * Copyright (C) 2018, 2022 Google LLC. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -50,7 +50,7 @@ public final class FetchV2Request extends FetchRequest { @NonNull List<String> wantedRefs, @NonNull Set<ObjectId> wantIds, @NonNull Set<ObjectId> clientShallowCommits, int deepenSince, - @NonNull List<String> deepenNotRefs, int depth, + @NonNull List<String> deepenNots, int depth, @NonNull FilterSpec filterSpec, boolean doneReceived, boolean waitForDone, @NonNull Set<String> clientCapabilities, @@ -58,7 +58,7 @@ public final class FetchV2Request extends FetchRequest { boolean sidebandAll, @NonNull List<String> packfileUriProtocols) { super(wantIds, depth, clientShallowCommits, filterSpec, clientCapabilities, deepenSince, - deepenNotRefs, agent); + deepenNots, agent); this.peerHas = requireNonNull(peerHas); this.wantedRefs = requireNonNull(wantedRefs); this.doneReceived = doneReceived; @@ -140,7 +140,7 @@ public final class FetchV2Request extends FetchRequest { final Set<ObjectId> clientShallowCommits = new HashSet<>(); - final List<String> deepenNotRefs = new ArrayList<>(); + final List<String> deepenNots = new ArrayList<>(); final Set<String> clientCapabilities = new HashSet<>(); @@ -240,17 +240,17 @@ public final class FetchV2Request extends FetchRequest { * @return true if there has been at least one "deepen not" line in the * request so far */ - boolean hasDeepenNotRefs() { - return !deepenNotRefs.isEmpty(); + boolean hasDeepenNots() { + return !deepenNots.isEmpty(); } /** - * @param deepenNotRef + * @param deepenNot * reference received in a "deepen not" line * @return this builder */ - Builder addDeepenNotRef(String deepenNotRef) { - deepenNotRefs.add(deepenNotRef); + Builder addDeepenNot(String deepenNot) { + deepenNots.add(deepenNot); return this; } @@ -350,7 +350,7 @@ public final class FetchV2Request extends FetchRequest { */ FetchV2Request build() { return new FetchV2Request(peerHas, wantedRefs, wantIds, - clientShallowCommits, deepenSince, deepenNotRefs, + clientShallowCommits, deepenSince, deepenNots, depth, filterSpec, doneReceived, waitForDone, clientCapabilities, agent, Collections.unmodifiableList(serverOptions), sidebandAll, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java index aaa9308ac3..be14e92d07 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java @@ -1,7 +1,7 @@ /* * Copyright (C) 2008, 2013 Google Inc. * 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 @@ -234,6 +234,13 @@ public final class GitProtocolConstants { public static final String CAPABILITY_SERVER_OPTION = "server-option"; //$NON-NLS-1$ /** + * The server supports the receiving of shallow options. + * + * @since 6.3 + */ + public static final String CAPABILITY_SHALLOW = "shallow"; //$NON-NLS-1$ + + /** * Option for passing application-specific options to the server. * * @since 5.2 @@ -308,6 +315,13 @@ public final class GitProtocolConstants { public static final String SECTION_PACKFILE = "packfile"; //$NON-NLS-1$ /** + * Protocol V2 shallow-info section header. + * + * @since 6.3 + */ + public static final String SECTION_SHALLOW_INFO = "shallow-info"; //$NON-NLS-1$ + + /** * Protocol announcement for protocol version 1. This is the same as V0, * except for this initial line. * @@ -329,6 +343,106 @@ public final class GitProtocolConstants { */ public static final String VERSION_2_REQUEST = "version=2"; //$NON-NLS-1$ + /** + * The flush packet. + * + * @since 6.3 + */ + public static final String PACKET_FLUSH = "0000"; //$NON-NLS-1$ + + /** + * An alias for {@link #PACKET_FLUSH}. "Flush" is the name used in the C git + * documentation; the Java implementation calls this "end" in several + * places. + * + * @since 6.3 + */ + public static final String PACKET_END = PACKET_FLUSH; + + /** + * The delimiter packet in protocol V2. + * + * @since 6.3 + */ + public static final String PACKET_DELIM = "0001"; //$NON-NLS-1$ + + /** + * A "deepen" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_DEEPEN = "deepen "; //$NON-NLS-1$ + + /** + * A "deepen-not" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_DEEPEN_NOT = "deepen-not "; //$NON-NLS-1$ + + /** + * A "deepen-since" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_DEEPEN_SINCE = "deepen-since "; //$NON-NLS-1$ + + /** + * An "ACK" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_ACK = "ACK "; //$NON-NLS-1$ + + /** + * A "done" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_DONE = "done"; //$NON-NLS-1$ + + /** + * A "ERR" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_ERR = "ERR "; //$NON-NLS-1$ + + /** + * A "have" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_HAVE = "have "; //$NON-NLS-1$ + + /** + * A "shallow" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_SHALLOW = OPTION_SHALLOW + ' '; + + /** + * A "shallow" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_UNSHALLOW = "unshallow "; //$NON-NLS-1$ + + /** + * A "want" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_WANT = "want "; //$NON-NLS-1$ + + /** + * A "want-ref" packet beginning. + * + * @since 6.3 + */ + public static final String PACKET_WANT_REF = OPTION_WANT_REF + ' '; + enum MultiAck { OFF, CONTINUE, DETAILED; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java index f8c51c180f..21a492577f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Google LLC. and others + * Copyright (C) 2018, 2022 Google LLC. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -10,6 +10,11 @@ package org.eclipse.jgit.transport; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_FILTER; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_NOT; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_SINCE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_SHALLOW; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_WANT; import java.io.EOFException; import java.io.IOException; @@ -70,20 +75,56 @@ final class ProtocolV0Parser { break; } - if (line.startsWith("deepen ")) { //$NON-NLS-1$ - int depth = Integer.parseInt(line.substring(7)); + if (line.startsWith(PACKET_DEEPEN)) { + int depth = Integer + .parseInt(line.substring(PACKET_DEEPEN.length())); if (depth <= 0) { throw new PackProtocolException( MessageFormat.format(JGitText.get().invalidDepth, Integer.valueOf(depth))); } + if (reqBuilder.getDeepenSince() != 0) { + throw new PackProtocolException( + JGitText.get().deepenSinceWithDeepen); + } + if (reqBuilder.hasDeepenNots()) { + throw new PackProtocolException( + JGitText.get().deepenNotWithDeepen); + } reqBuilder.setDepth(depth); continue; } - if (line.startsWith("shallow ")) { //$NON-NLS-1$ + if (line.startsWith(PACKET_DEEPEN_NOT)) { + reqBuilder.addDeepenNot( + line.substring(PACKET_DEEPEN_NOT.length())); + if (reqBuilder.getDepth() != 0) { + throw new PackProtocolException( + JGitText.get().deepenNotWithDeepen); + } + continue; + } + + if (line.startsWith(PACKET_DEEPEN_SINCE)) { + // TODO: timestamps should be long + int ts = Integer + .parseInt(line.substring(PACKET_DEEPEN_SINCE.length())); + if (ts <= 0) { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().invalidTimestamp, line)); + } + if (reqBuilder.getDepth() != 0) { + throw new PackProtocolException( + JGitText.get().deepenSinceWithDeepen); + } + reqBuilder.setDeepenSince(ts); + continue; + } + + if (line.startsWith(PACKET_SHALLOW)) { reqBuilder.addClientShallowCommit( - ObjectId.fromString(line.substring(8))); + ObjectId.fromString( + line.substring(PACKET_SHALLOW.length()))); continue; } @@ -101,7 +142,7 @@ final class ProtocolV0Parser { continue; } - if (!line.startsWith("want ") || line.length() < 45) { //$NON-NLS-1$ + if (!line.startsWith(PACKET_WANT) || line.length() < 45) { throw new PackProtocolException(MessageFormat .format(JGitText.get().expectedGot, "want", line)); //$NON-NLS-1$ } @@ -115,7 +156,8 @@ final class ProtocolV0Parser { } } - reqBuilder.addWantId(ObjectId.fromString(line.substring(5))); + reqBuilder.addWantId( + ObjectId.fromString(line.substring(PACKET_WANT.length()))); isFirst = false; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java index 6cec4b9a3f..b38deb69c0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Google LLC. and others + * Copyright (C) 2018, 2022 Google LLC. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -20,7 +20,14 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDEBAND_AL import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND_64K; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_THIN_PACK; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_WAIT_FOR_DONE; -import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_WANT_REF; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_NOT; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_SINCE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DONE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_HAVE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_SHALLOW; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_WANT; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_WANT_REF; import java.io.IOException; import java.text.MessageFormat; @@ -115,15 +122,17 @@ final class ProtocolV2Parser { boolean filterReceived = false; for (String line2 : pckIn.readStrings()) { - if (line2.startsWith("want ")) { //$NON-NLS-1$ - reqBuilder.addWantId(ObjectId.fromString(line2.substring(5))); + if (line2.startsWith(PACKET_WANT)) { + reqBuilder.addWantId(ObjectId + .fromString(line2.substring(PACKET_WANT.length()))); } else if (transferConfig.isAllowRefInWant() - && line2.startsWith(OPTION_WANT_REF + " ")) { //$NON-NLS-1$ + && line2.startsWith(PACKET_WANT_REF)) { reqBuilder.addWantedRef( - line2.substring(OPTION_WANT_REF.length() + 1)); - } else if (line2.startsWith("have ")) { //$NON-NLS-1$ - reqBuilder.addPeerHas(ObjectId.fromString(line2.substring(5))); - } else if (line2.equals("done")) { //$NON-NLS-1$ + line2.substring(PACKET_WANT_REF.length())); + } else if (line2.startsWith(PACKET_HAVE)) { + reqBuilder.addPeerHas(ObjectId + .fromString(line2.substring(PACKET_HAVE.length()))); + } else if (line2.equals(PACKET_DONE)) { reqBuilder.setDoneReceived(); } else if (line2.equals(OPTION_WAIT_FOR_DONE)) { reqBuilder.setWaitForDone(); @@ -135,11 +144,13 @@ final class ProtocolV2Parser { reqBuilder.addClientCapability(OPTION_INCLUDE_TAG); } else if (line2.equals(OPTION_OFS_DELTA)) { reqBuilder.addClientCapability(OPTION_OFS_DELTA); - } else if (line2.startsWith("shallow ")) { //$NON-NLS-1$ + } else if (line2.startsWith(PACKET_SHALLOW)) { reqBuilder.addClientShallowCommit( - ObjectId.fromString(line2.substring(8))); - } else if (line2.startsWith("deepen ")) { //$NON-NLS-1$ - int parsedDepth = Integer.parseInt(line2.substring(7)); + ObjectId.fromString( + line2.substring(PACKET_SHALLOW.length()))); + } else if (line2.startsWith(PACKET_DEEPEN)) { + int parsedDepth = Integer + .parseInt(line2.substring(PACKET_DEEPEN.length())); if (parsedDepth <= 0) { throw new PackProtocolException( MessageFormat.format(JGitText.get().invalidDepth, @@ -149,21 +160,23 @@ final class ProtocolV2Parser { throw new PackProtocolException( JGitText.get().deepenSinceWithDeepen); } - if (reqBuilder.hasDeepenNotRefs()) { + if (reqBuilder.hasDeepenNots()) { throw new PackProtocolException( JGitText.get().deepenNotWithDeepen); } reqBuilder.setDepth(parsedDepth); - } else if (line2.startsWith("deepen-not ")) { //$NON-NLS-1$ - reqBuilder.addDeepenNotRef(line2.substring(11)); + } else if (line2.startsWith(PACKET_DEEPEN_NOT)) { + reqBuilder.addDeepenNot( + line2.substring(PACKET_DEEPEN_NOT.length())); if (reqBuilder.getDepth() != 0) { throw new PackProtocolException( JGitText.get().deepenNotWithDeepen); } } else if (line2.equals(OPTION_DEEPEN_RELATIVE)) { reqBuilder.addClientCapability(OPTION_DEEPEN_RELATIVE); - } else if (line2.startsWith("deepen-since ")) { //$NON-NLS-1$ - int ts = Integer.parseInt(line2.substring(13)); + } else if (line2.startsWith(PACKET_DEEPEN_SINCE)) { + int ts = Integer.parseInt( + line2.substring(PACKET_DEEPEN_SINCE.length())); if (ts <= 0) { throw new PackProtocolException(MessageFormat .format(JGitText.get().invalidTimestamp, line2)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java index 2542105c07..b70eedca63 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -20,6 +20,8 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_QUIET; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_REPORT_STATUS; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BAND_64K; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ERR; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_SHALLOW; import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA; import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR; import static org.eclipse.jgit.transport.SideBandOutputStream.CH_PROGRESS; @@ -1247,7 +1249,7 @@ public class ReceivePack { public void sendAdvertisedRefs(RefAdvertiser adv) throws IOException, ServiceMayNotContinueException { if (advertiseError != null) { - adv.writeOne("ERR " + advertiseError); //$NON-NLS-1$ + adv.writeOne(PACKET_ERR + advertiseError); return; } @@ -1255,7 +1257,7 @@ public class ReceivePack { advertiseRefsHook.advertiseRefs(this); } catch (ServiceMayNotContinueException fail) { if (fail.getMessage() != null) { - adv.writeOne("ERR " + fail.getMessage()); //$NON-NLS-1$ + adv.writeOne(PACKET_ERR + fail.getMessage()); fail.setOutput(); } throw fail; @@ -1339,8 +1341,9 @@ public class ReceivePack { break; } - if (line.length() >= 48 && line.startsWith("shallow ")) { //$NON-NLS-1$ - parseShallow(line.substring(8, 48)); + int len = PACKET_SHALLOW.length() + 40; + if (line.length() >= len && line.startsWith(PACKET_SHALLOW)) { + parseShallow(line.substring(PACKET_SHALLOW.length(), len)); continue; } @@ -1795,9 +1798,9 @@ public class ReceivePack { @Override void sendString(String s) throws IOException { if (reportStatus) { - pckOut.writeString(s + "\n"); //$NON-NLS-1$ + pckOut.writeString(s + '\n'); } else if (msgOut != null) { - msgOut.write(Constants.encode(s + "\n")); //$NON-NLS-1$ + msgOut.write(Constants.encode(s + '\n')); } } }; 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 e36eeccfb1..61d193593a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, 2013 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 @@ -154,7 +154,7 @@ public class RefSpec implements Serializable { this.allowMismatchedWildcards = mode; String s = spec; - if (s.startsWith("^+") || s.startsWith("+^")) { + if (s.startsWith("^+") || s.startsWith("+^")) { //$NON-NLS-1$ //$NON-NLS-2$ throw new IllegalArgumentException( JGitText.get().invalidNegativeAndForce); } @@ -164,7 +164,7 @@ public class RefSpec implements Serializable { s = s.substring(1); } - if(s.startsWith("^")) { + if (s.startsWith("^")) { //$NON-NLS-1$ negative = true; s = s.substring(1); } @@ -251,7 +251,7 @@ public class RefSpec implements Serializable { * the specification is invalid. */ public RefSpec(String spec) { - this(spec, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH + this(spec, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH //$NON-NLS-1$ : WildcardMode.REQUIRE_MATCH); } 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 3222d6330c..7cea998474 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -27,6 +27,7 @@ import java.lang.reflect.Modifier; import java.net.URISyntaxException; import java.net.URL; import java.text.MessageFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -784,6 +785,12 @@ public abstract class Transport implements AutoCloseable { private PrePushHook prePush; + private Integer depth; + + private Instant deepenSince; + + private List<String> deepenNots = new ArrayList<>(); + @Nullable TransferConfig.ProtocolVersion protocol; @@ -1086,6 +1093,83 @@ public abstract class Transport implements AutoCloseable { filterSpec = requireNonNull(filter); } + + /** + * Retrieves the depth for a shallow clone. + * + * @return the depth, or {@code null} if none set + * @since 6.3 + */ + public final Integer getDepth() { + return depth; + } + + /** + * Limits fetching to the specified number of commits from the tip of each + * remote branch history. + * + * @param depth + * the depth + * @since 6.3 + */ + public final void setDepth(int depth) { + if (depth < 1) { + throw new IllegalArgumentException(JGitText.get().depthMustBeAt1); + } + this.depth = Integer.valueOf(depth); + } + + /** + * Limits fetching to the specified number of commits from the tip of each + * remote branch history. + * + * @param depth + * the depth, or {@code null} to unset the depth + * @since 6.3 + */ + public final void setDepth(Integer depth) { + if (depth != null && depth.intValue() < 1) { + throw new IllegalArgumentException(JGitText.get().depthMustBeAt1); + } + this.depth = depth; + } + + /** + * @return the deepen-since for a shallow clone + * @since 6.3 + */ + public final Instant getDeepenSince() { + return deepenSince; + } + + /** + * Deepen or shorten the history of a shallow repository to include all reachable commits after a specified time. + * + * @param deepenSince the deepen-since. Must not be {@code null} + * @since 6.3 + */ + public final void setDeepenSince(@NonNull Instant deepenSince) { + this.deepenSince = deepenSince; + } + + /** + * @return the deepen-not for a shallow clone + * @since 6.3 + */ + public final List<String> getDeepenNots() { + return deepenNots; + } + + /** + * Deepen or shorten the history of a shallow repository to exclude commits reachable from a specified remote branch or tag. + * + * @param deepenNots the deepen-not. Must not be {@code null} + * @since 6.3 + */ + public final void setDeepenNots(@NonNull List<String> deepenNots) { + this.deepenNots = deepenNots; + } + /** * Apply provided remote configuration on this transport. * @@ -1230,7 +1314,7 @@ public abstract class Transport implements AutoCloseable { * @param toFetch * specification of refs to fetch locally. May be null or the * empty collection to use the specifications from the - * RemoteConfig. May contains regular and negative + * RemoteConfig. May contains regular and negative * {@link RefSpec}s. Source for each regular RefSpec can't * be null. * @return information describing the tracking refs updated. @@ -1266,7 +1350,7 @@ public abstract class Transport implements AutoCloseable { * @param toFetch * specification of refs to fetch locally. May be null or the * empty collection to use the specifications from the - * RemoteConfig. May contains regular and negative + * RemoteConfig. May contain regular and negative * {@link RefSpec}s. Source for each regular RefSpec can't * be null. * @param branch 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 1617c5063d..65dbf12b2f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, 2020 Google Inc. and others + * Copyright (C) 2008, 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 @@ -10,6 +10,7 @@ package org.eclipse.jgit.transport; +import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableMap; import static java.util.Objects.requireNonNull; import static org.eclipse.jgit.lib.Constants.R_TAGS; @@ -35,6 +36,12 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND_64K; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_THIN_PACK; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_WAIT_FOR_DONE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ACK; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DONE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ERR; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_HAVE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_SHALLOW; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_UNSHALLOW; import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2_REQUEST; import static org.eclipse.jgit.util.RefMap.toRefMap; @@ -1033,6 +1040,7 @@ public class UploadPack implements Closeable { // writing a response. Buffer the response until then. PackStatistics.Accumulator accumulator = new PackStatistics.Accumulator(); List<ObjectId> unshallowCommits = new ArrayList<>(); + List<ObjectId> deepenNots = emptyList(); FetchRequest req; try { if (biDirectionalPipe) @@ -1071,13 +1079,15 @@ public class UploadPack implements Closeable { verifyClientShallow(req.getClientShallowCommits()); } - if (req.getDepth() != 0 || req.getDeepenSince() != 0) { + deepenNots = parseDeepenNots(req.getDeepenNots()); + if (req.getDepth() != 0 || req.getDeepenSince() != 0 || !req.getDeepenNots().isEmpty()) { computeShallowsAndUnshallows(req, shallow -> { - pckOut.writeString("shallow " + shallow.name() + '\n'); //$NON-NLS-1$ + pckOut.writeString(PACKET_SHALLOW + shallow.name() + '\n'); }, unshallow -> { - pckOut.writeString("unshallow " + unshallow.name() + '\n'); //$NON-NLS-1$ + pckOut.writeString( + PACKET_UNSHALLOW + unshallow.name() + '\n'); unshallowCommits.add(unshallow); - }, Collections.emptyList()); + }, deepenNots); pckOut.end(); } @@ -1109,7 +1119,7 @@ public class UploadPack implements Closeable { if (sendPack) { sendPack(accumulator, req, refs == null ? null : refs.values(), - unshallowCommits, Collections.emptyList(), pckOut); + unshallowCommits, deepenNots, pckOut); } } @@ -1188,15 +1198,7 @@ public class UploadPack implements Closeable { // TODO(ifrade): Refactor to pass around the Request object, instead of // copying data back to class fields - List<ObjectId> deepenNots = new ArrayList<>(); - for (String s : req.getDeepenNotRefs()) { - Ref ref = findRef(s); - if (ref == null) { - throw new PackProtocolException(MessageFormat - .format(JGitText.get().invalidRefName, s)); - } - deepenNots.add(ref.getObjectId()); - } + List<ObjectId> deepenNots = parseDeepenNots(req.getDeepenNots()); Map<String, ObjectId> wantedRefs = wantedRefs(req); // TODO(ifrade): Avoid mutating the parsed request. @@ -1206,7 +1208,7 @@ public class UploadPack implements Closeable { boolean sectionSent = false; boolean mayHaveShallow = req.getDepth() != 0 || req.getDeepenSince() != 0 - || !req.getDeepenNotRefs().isEmpty(); + || !req.getDeepenNots().isEmpty(); List<ObjectId> shallowCommits = new ArrayList<>(); List<ObjectId> unshallowCommits = new ArrayList<>(); @@ -1232,7 +1234,7 @@ public class UploadPack implements Closeable { GitProtocolConstants.SECTION_ACKNOWLEDGMENTS + '\n'); for (ObjectId id : req.getPeerHas()) { if (walk.getObjectReader().has(id)) { - pckOut.writeString("ACK " + id.getName() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + pckOut.writeString(PACKET_ACK + id.getName() + '\n'); } } processHaveLines(req.getPeerHas(), ObjectId.zeroId(), @@ -1250,12 +1252,13 @@ public class UploadPack implements Closeable { if (mayHaveShallow) { if (sectionSent) pckOut.writeDelim(); - pckOut.writeString("shallow-info\n"); //$NON-NLS-1$ + pckOut.writeString( + GitProtocolConstants.SECTION_SHALLOW_INFO + '\n'); for (ObjectId o : shallowCommits) { - pckOut.writeString("shallow " + o.getName() + '\n'); //$NON-NLS-1$ + pckOut.writeString(PACKET_SHALLOW + o.getName() + '\n'); } for (ObjectId o : unshallowCommits) { - pckOut.writeString("unshallow " + o.getName() + '\n'); //$NON-NLS-1$ + pckOut.writeString(PACKET_UNSHALLOW + o.getName() + '\n'); } sectionSent = true; } @@ -1319,7 +1322,7 @@ public class UploadPack implements Closeable { .format(JGitText.get().missingObject, oid.name()), e); } - pckOut.writeString(oid.getName() + " " + size); //$NON-NLS-1$ + pckOut.writeString(oid.getName() + ' ' + size); } pckOut.end(); @@ -1391,7 +1394,7 @@ public class UploadPack implements Closeable { protocolV2Hook .onCapabilities(CapabilitiesV2Request.builder().build()); for (String s : getV2CapabilityAdvertisement()) { - pckOut.writeString(s + "\n"); //$NON-NLS-1$ + pckOut.writeString(s + '\n'); } pckOut.end(); @@ -1618,7 +1621,7 @@ public class UploadPack implements Closeable { */ public void sendMessage(String what) { try { - msgOut.write(Constants.encode(what + "\n")); //$NON-NLS-1$ + msgOut.write(Constants.encode(what + '\n')); } catch (IOException e) { // Ignore write failures. } @@ -1725,24 +1728,26 @@ public class UploadPack implements Closeable { if (commonBase.isEmpty() || multiAck != MultiAck.OFF) pckOut.writeString("NAK\n"); //$NON-NLS-1$ if (noDone && sentReady) { - pckOut.writeString("ACK " + last.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + pckOut.writeString(PACKET_ACK + last.name() + '\n'); return true; } if (!biDirectionalPipe) return false; pckOut.flush(); - } else if (line.startsWith("have ") && line.length() == 45) { //$NON-NLS-1$ - peerHas.add(ObjectId.fromString(line.substring(5))); + } else if (line.startsWith(PACKET_HAVE) + && line.length() == PACKET_HAVE.length() + 40) { + peerHas.add(ObjectId + .fromString(line.substring(PACKET_HAVE.length()))); accumulator.haves++; - } else if (line.equals("done")) { //$NON-NLS-1$ + } else if (line.equals(PACKET_DONE)) { last = processHaveLines(peerHas, last, pckOut, accumulator, Option.NONE); if (commonBase.isEmpty()) pckOut.writeString("NAK\n"); //$NON-NLS-1$ else if (multiAck != MultiAck.OFF) - pckOut.writeString("ACK " + last.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + pckOut.writeString(PACKET_ACK + last.name() + '\n'); return true; @@ -1803,14 +1808,15 @@ public class UploadPack implements Closeable { // switch (multiAck) { case OFF: - if (commonBase.size() == 1) - out.writeString("ACK " + obj.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + if (commonBase.size() == 1) { + out.writeString(PACKET_ACK + obj.name() + '\n'); + } break; case CONTINUE: - out.writeString("ACK " + obj.name() + " continue\n"); //$NON-NLS-1$ //$NON-NLS-2$ + out.writeString(PACKET_ACK + obj.name() + " continue\n"); //$NON-NLS-1$ break; case DETAILED: - out.writeString("ACK " + obj.name() + " common\n"); //$NON-NLS-1$ //$NON-NLS-2$ + out.writeString(PACKET_ACK + obj.name() + " common\n"); //$NON-NLS-1$ break; } } @@ -1849,11 +1855,11 @@ public class UploadPack implements Closeable { break; case CONTINUE: out.writeString( - "ACK " + id.name() + " continue\n"); //$NON-NLS-1$ //$NON-NLS-2$ + PACKET_ACK + id.name() + " continue\n"); //$NON-NLS-1$ break; case DETAILED: out.writeString( - "ACK " + id.name() + " ready\n"); //$NON-NLS-1$ //$NON-NLS-2$ + PACKET_ACK + id.name() + " ready\n"); //$NON-NLS-1$ readySent = true; break; } @@ -1866,7 +1872,7 @@ public class UploadPack implements Closeable { if (multiAck == MultiAck.DETAILED && !didOkToGiveUp && okToGiveUp()) { ObjectId id = peerHas.get(peerHas.size() - 1); - out.writeString("ACK " + id.name() + " ready\n"); //$NON-NLS-1$ //$NON-NLS-2$ + out.writeString(PACKET_ACK + id.name() + " ready\n"); //$NON-NLS-1$ readySent = true; } @@ -2038,7 +2044,8 @@ public class UploadPack implements Closeable { .filter(obj -> !(obj instanceof RevCommit)) .limit(1) .collect(Collectors.toList()).get(0); - throw new WantNotValidException(nonCommit); + throw new WantNotValidException(nonCommit, + new Exception("Cannot walk without bitmaps")); //$NON-NLS-1$ } try (ObjectWalk objWalk = walk.toObjectWalkWithSameObjects()) { @@ -2052,6 +2059,11 @@ public class UploadPack implements Closeable { Optional<RevObject> unreachable = reachabilityChecker .areAllReachable(wantsAsObjs, startersAsObjs); if (unreachable.isPresent()) { + if (!repoHasBitmaps) { + throw new WantNotValidException( + unreachable.get(), new Exception( + "Retry with bitmaps enabled")); //$NON-NLS-1$ + } throw new WantNotValidException(unreachable.get()); } } @@ -2470,6 +2482,24 @@ public class UploadPack implements Closeable { } } + private List<ObjectId> parseDeepenNots(List<String> deepenNots) + throws IOException { + List<ObjectId> result = new ArrayList<>(); + for (String s : deepenNots) { + if (ObjectId.isId(s)) { + result.add(ObjectId.fromString(s)); + } else { + Ref ref = findRef(s); + if (ref == null) { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().invalidRefName, s)); + } + result.add(ref.getObjectId()); + } + } + return result; + } + private static class ResponseBufferedOutputStream extends OutputStream { private final OutputStream rawOut; @@ -2533,7 +2563,7 @@ public class UploadPack implements Closeable { @Override public void writeError(String message) throws IOException { new PacketLineOut(requireNonNull(rawOut)) - .writeString("ERR " + message + '\n'); //$NON-NLS-1$ + .writeString(PACKET_ERR + message + '\n'); } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java index 28b7423817..2fd945b03f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java @@ -56,7 +56,7 @@ import org.eclipse.jgit.lib.Repository; public class NameConflictTreeWalk extends TreeWalk { private static final int TREE_MODE = FileMode.TREE.getBits(); - private boolean fastMinHasMatch; + private boolean allTreesNamesMatchFastMinRef; private AbstractTreeIterator dfConflict; @@ -97,7 +97,7 @@ public class NameConflictTreeWalk extends TreeWalk { AbstractTreeIterator min() throws CorruptObjectException { for (;;) { final AbstractTreeIterator minRef = fastMin(); - if (fastMinHasMatch) + if (allTreesNamesMatchFastMinRef) return minRef; if (isTree(minRef)) { @@ -118,15 +118,14 @@ public class NameConflictTreeWalk extends TreeWalk { } private AbstractTreeIterator fastMin() { - fastMinHasMatch = true; - - int i = 0; + int i = getFirstNonEofTreeIndex(); + if (i == -1) { + // All trees reached the end. + allTreesNamesMatchFastMinRef = true; + return trees[trees.length - 1]; + } AbstractTreeIterator minRef = trees[i]; - while (minRef.eof() && ++i < trees.length) - minRef = trees[i]; - if (minRef.eof()) - return minRef; - + allTreesNamesMatchFastMinRef = true; boolean hasConflict = false; minRef.matches = minRef; while (++i < trees.length) { @@ -136,7 +135,7 @@ public class NameConflictTreeWalk extends TreeWalk { final int cmp = t.pathCompare(minRef); if (cmp < 0) { - if (fastMinHasMatch && isTree(minRef) && !isTree(t) + if (allTreesNamesMatchFastMinRef && isTree(minRef) && !isTree(t) && nameEqual(minRef, t)) { // We used to be at a tree, but now we are at a file // with the same name. Allow the file to match the @@ -145,7 +144,7 @@ public class NameConflictTreeWalk extends TreeWalk { t.matches = minRef; hasConflict = true; } else { - fastMinHasMatch = false; + allTreesNamesMatchFastMinRef = false; t.matches = t; minRef = t; } @@ -153,7 +152,7 @@ public class NameConflictTreeWalk extends TreeWalk { // Exact name/mode match is best. // t.matches = minRef; - } else if (fastMinHasMatch && isTree(t) && !isTree(minRef) + } else if (allTreesNamesMatchFastMinRef && isTree(t) && !isTree(minRef) && !isGitlink(minRef) && nameEqual(t, minRef)) { // The minimum is a file (non-tree) but the next entry // of this iterator is a tree whose name matches our file. @@ -172,14 +171,23 @@ public class NameConflictTreeWalk extends TreeWalk { minRef = t; hasConflict = true; } else - fastMinHasMatch = false; + allTreesNamesMatchFastMinRef = false; } - if (hasConflict && fastMinHasMatch && dfConflict == null) + if (hasConflict && allTreesNamesMatchFastMinRef && dfConflict == null) dfConflict = minRef; return minRef; } + private int getFirstNonEofTreeIndex() { + for (int i = 0; i < trees.length; i++) { + if (!trees[i].eof()) { + return i; + } + } + return -1; + } + private static boolean nameEqual(final AbstractTreeIterator a, final AbstractTreeIterator b) { return a.pathCompare(b, TREE_MODE) == 0; @@ -268,6 +276,28 @@ public class NameConflictTreeWalk extends TreeWalk { } } + // When the combineDF is called, the t.matches field stores other + // entry (i.e. tree iterator) with an equal path. However, the + // previous loop moves each iterator independently. As a result, + // iterators which have had equals path at the start of the + // method can have different paths at this point. + // Reevaluate existing matches. + // The NameConflictTreeWalkTest.testDF_specialFileNames test + // cover this situation. + for (AbstractTreeIterator t : trees) { + // The previous loop doesn't touch tree iterator if it matches + // minRef. Skip it here + if (t.eof() || t.matches == null || t.matches == minRef) + continue; + // The t.pathCompare takes into account the entry type (file + // or directory) and returns non-zero value if names match + // but entry type don't match. + // We want to keep such matches (file/directory conflict), + // so reset matches only if names are not equal. + if (!nameEqual(t, t.matches)) + t.matches = null; + } + if (treeMatch != null) { // If we do have a conflict use one of the directory // matching iterators instead of the file iterator. 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 8269666d26..ece945232e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -1375,6 +1375,14 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { System.arraycopy(tmp, 0, trees, 0, trees.length); } + /** + * Returns an AbstractTreeIterator from {@code trees} with the smallest name, and sets its + * {@code matches} field. This may clobber {@code matches} in other {@code tree}s. Other iterators + * at the same name will have their {@code matches} pointing to the same {@code min()} value. + * + * @return the smallest tree iterator available. + * @throws CorruptObjectException + */ @SuppressWarnings("unused") AbstractTreeIterator min() throws CorruptObjectException { int i = 0; 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 427eac5b53..d8a61ec97a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -1004,6 +1004,12 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { return wtMode; } final FileMode iMode = indexIter.getEntryFileMode(); + if (iMode == FileMode.SYMLINK + && getOptions().getSymLinks() == SymLinks.FALSE + && (wtMode == FileMode.REGULAR_FILE + || wtMode == FileMode.EXECUTABLE_FILE)) { + return iMode; + } if (getOptions().isFileMode() && iMode != FileMode.GITLINK && iMode != FileMode.TREE) { return wtMode; } @@ -1274,11 +1280,15 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } IgnoreNode load() throws IOException { - IgnoreNode r = new IgnoreNode(); + return load(null); + } + + IgnoreNode load(IgnoreNode parent) throws IOException { + IgnoreNodeWithParent r = new IgnoreNodeWithParent(parent); try (InputStream in = entry.openInputStream()) { r.parse(name, in); } - return r.getRules().isEmpty() ? null : r; + return r.getRules().isEmpty() && parent == null ? null : r; } } @@ -1292,29 +1302,41 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } @Override - IgnoreNode load() throws IOException { - IgnoreNode r; - if (entry != null) { - r = super.load(); - if (r == null) - r = new IgnoreNode(); - } else { - r = new IgnoreNode(); - } - + IgnoreNode load(IgnoreNode parent) throws IOException { + IgnoreNode coreExclude = new IgnoreNodeWithParent(parent); FS fs = repository.getFS(); Path path = repository.getConfig().getPath( ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_EXCLUDESFILE, fs, null, null); if (path != null) { - loadRulesFromFile(r, path.toFile()); + loadRulesFromFile(coreExclude, path.toFile()); + } + if (coreExclude.getRules().isEmpty()) { + coreExclude = parent; } + IgnoreNode infoExclude = new IgnoreNodeWithParent( + coreExclude); File exclude = fs.resolve(repository.getDirectory(), Constants.INFO_EXCLUDE); - loadRulesFromFile(r, exclude); + loadRulesFromFile(infoExclude, exclude); + if (infoExclude.getRules().isEmpty()) { + infoExclude = null; + } - return r.getRules().isEmpty() ? null : r; + IgnoreNode parentNode = infoExclude != null ? infoExclude + : coreExclude; + + IgnoreNode r; + if (entry != null) { + r = super.load(parentNode); + if (r == null) { + return null; + } + } else { + return parentNode; + } + return r.getRules().isEmpty() ? parentNode : r; } private static void loadRulesFromFile(IgnoreNode r, File exclude) @@ -1327,6 +1349,24 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } } + private static class IgnoreNodeWithParent extends IgnoreNode { + + private final IgnoreNode parent; + + IgnoreNodeWithParent(IgnoreNode parent) { + this.parent = parent; + } + + @Override + public Boolean checkIgnored(String path, boolean isDirectory) { + Boolean result = super.checkIgnored(path, isDirectory); + if (result == null && parent != null) { + return parent.checkIgnored(path, isDirectory); + } + return result; + } + } + /** Magic type indicating we know rules exist, but they aren't loaded. */ private static class PerDirectoryAttributesNode extends AttributesNode { final Entry entry; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java index 5a39f95b2b..ae13ef7f3c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java @@ -18,7 +18,8 @@ import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; * * @since 4.2 */ -public class Paths { +public final class Paths { + /** * Remove trailing {@code '/'} if present. * @@ -43,6 +44,33 @@ public class Paths { } /** + * Determines whether a git path {@code folder} is a prefix of another git + * path {@code path}, or the same as {@code path}. An empty {@code folder} + * is <em>not</em> not considered a prefix and matches only if {@code path} + * is also empty. + * + * @param folder + * a git path for a directory, without trailing slash + * @param path + * a git path + * @return {@code true} if {@code folder} is a directory prefix of + * {@code path}, or is equal to {@code path}, {@code false} + * otherwise + * @since 6.3 + */ + public static boolean isEqualOrPrefix(String folder, String path) { + if (folder.isEmpty()) { + return path.isEmpty(); + } + boolean isPrefix = path.startsWith(folder); + if (isPrefix) { + int length = folder.length(); + return path.length() == length || path.charAt(length) == '/'; + } + return false; + } + + /** * Compare two paths according to Git path sort ordering rules. * * @param aPath @@ -63,9 +91,8 @@ public class Paths { * @param bMode * mode of the second file. Trees are sorted as though * {@code bPath[bEnd] == '/'}, even if bEnd does not exist. - * @return <0 if {@code aPath} sorts before {@code bPath}; - * 0 if the paths are the same; - * >0 if {@code aPath} sorts after {@code bPath}. + * @return <0 if {@code aPath} sorts before {@code bPath}; 0 if the paths + * are the same; >0 if {@code aPath} sorts after {@code bPath}. */ public static int compare(byte[] aPath, int aPos, int aEnd, int aMode, byte[] bPath, int bPos, int bEnd, int bMode) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java index 16e2577911..5ced0713e0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java @@ -63,6 +63,8 @@ public abstract class SystemReader { private static volatile Boolean isWindows; + private static volatile Boolean isLinux; + static { SystemReader r = new Default(); r.init(); @@ -185,6 +187,7 @@ public abstract class SystemReader { public static void setInstance(SystemReader newReader) { isMacOS = null; isWindows = null; + isLinux = null; if (newReader == null) INSTANCE = DEFAULT; else { @@ -543,6 +546,20 @@ public abstract class SystemReader { return isMacOS.booleanValue(); } + /** + * Whether we are running on Linux. + * + * @return true if we are running on Linux. + * @since 6.3 + */ + public boolean isLinux() { + if (isLinux == null) { + String osname = getOsName(); + isLinux = Boolean.valueOf(osname.toLowerCase().startsWith("linux")); //$NON-NLS-1$ + } + return isLinux.booleanValue(); + } + private String getOsName() { return AccessController.doPrivileged( (PrivilegedAction<String>) () -> getProperty("os.name") //$NON-NLS-1$ |