diff options
Diffstat (limited to 'org.eclipse.jgit')
112 files changed, 4225 insertions, 1729 deletions
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 7aa7301b03..2c9b3fe422 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -3,8 +3,8 @@ <resource path="META-INF/MANIFEST.MF"> <filter id="924844039"> <message_arguments> - <message_argument value="5.1.9"/> - <message_argument value="5.1.0"/> + <message_argument value="5.2.3"/> + <message_argument value="5.2.0"/> </message_arguments> </filter> </resource> @@ -62,22 +62,6 @@ </message_arguments> </filter> </resource> - <resource path="src/org/eclipse/jgit/lib/GitmoduleEntry.java" type="org.eclipse.jgit.lib.GitmoduleEntry"> - <filter id="1109393411"> - <message_arguments> - <message_argument value="4.7.5"/> - <message_argument value="org.eclipse.jgit.lib.GitmoduleEntry"/> - </message_arguments> - </filter> - </resource> - <resource path="src/org/eclipse/jgit/lib/ObjectChecker.java" type="org.eclipse.jgit.lib.ObjectChecker"> - <filter id="1142947843"> - <message_arguments> - <message_argument value="4.7.5"/> - <message_argument value="getGitsubmodules()"/> - </message_arguments> - </filter> - </resource> <resource path="src/org/eclipse/jgit/storage/file/FileBasedConfig.java" type="org.eclipse.jgit.storage.file.FileBasedConfig"> <filter id="1142947843"> <message_arguments> diff --git a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs index 13c32a6d94..ef6f5e732f 100644 --- a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs +++ b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs @@ -91,7 +91,7 @@ org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=error org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore diff --git a/org.eclipse.jgit/.settings/org.eclipse.mylyn.team.ui.prefs b/org.eclipse.jgit/.settings/org.eclipse.mylyn.team.ui.prefs index 0cba949fb7..2fca432276 100644 --- a/org.eclipse.jgit/.settings/org.eclipse.mylyn.team.ui.prefs +++ b/org.eclipse.jgit/.settings/org.eclipse.mylyn.team.ui.prefs @@ -1,3 +1,3 @@ #Tue Jul 19 20:11:28 CEST 2011 -commit.comment.template=${task.description} \n\nBug\: ${task.key} +commit.comment.template=${task.description}\n\nBug\: ${task.key} eclipse.preferences.version=1 diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 470871c503..66701b0b38 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: %plugin_name Automatic-Module-Name: org.eclipse.jgit Bundle-SymbolicName: org.eclipse.jgit -Bundle-Version: 5.1.9.qualifier +Bundle-Version: 5.2.3.qualifier Bundle-Localization: plugin Bundle-Vendor: %provider_name Bundle-ActivationPolicy: lazy -Export-Package: org.eclipse.jgit.annotations;version="5.1.9", - org.eclipse.jgit.api;version="5.1.9"; +Export-Package: org.eclipse.jgit.annotations;version="5.2.3", + org.eclipse.jgit.api;version="5.2.3"; uses:="org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.diff, @@ -22,65 +22,73 @@ Export-Package: org.eclipse.jgit.annotations;version="5.1.9", org.eclipse.jgit.submodule, org.eclipse.jgit.transport, org.eclipse.jgit.merge", - org.eclipse.jgit.api.errors;version="5.1.9";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.errors", - org.eclipse.jgit.attributes;version="5.1.9", - org.eclipse.jgit.blame;version="5.1.9"; + org.eclipse.jgit.api.errors;version="5.2.3";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.errors", + org.eclipse.jgit.attributes;version="5.2.3", + org.eclipse.jgit.blame;version="5.2.3"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.diff", - org.eclipse.jgit.diff;version="5.1.9"; + org.eclipse.jgit.diff;version="5.2.3"; uses:="org.eclipse.jgit.patch, org.eclipse.jgit.lib, org.eclipse.jgit.treewalk, org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.util", - org.eclipse.jgit.dircache;version="5.1.9"; + org.eclipse.jgit.dircache;version="5.2.3"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.treewalk, org.eclipse.jgit.util, org.eclipse.jgit.events, org.eclipse.jgit.attributes", - org.eclipse.jgit.errors;version="5.1.9"; + org.eclipse.jgit.errors;version="5.2.3"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.internal.storage.pack, org.eclipse.jgit.transport, org.eclipse.jgit.dircache", - org.eclipse.jgit.events;version="5.1.9";uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.fnmatch;version="5.1.9", - org.eclipse.jgit.gitrepo;version="5.1.9"; + org.eclipse.jgit.events;version="5.2.3";uses:="org.eclipse.jgit.lib", + org.eclipse.jgit.fnmatch;version="5.2.3", + org.eclipse.jgit.gitrepo;version="5.2.3"; uses:="org.eclipse.jgit.api, org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, org.xml.sax.helpers, org.xml.sax", - org.eclipse.jgit.gitrepo.internal;version="5.1.9";x-internal:=true, - org.eclipse.jgit.hooks;version="5.1.9";uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.ignore;version="5.1.9", - org.eclipse.jgit.ignore.internal;version="5.1.9";x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal;version="5.1.9";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test", - org.eclipse.jgit.internal.fsck;version="5.1.9";x-friends:="org.eclipse.jgit.test", - org.eclipse.jgit.internal.ketch;version="5.1.9";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.dfs;version="5.1.9"; + org.eclipse.jgit.gitrepo.internal;version="5.2.3";x-internal:=true, + org.eclipse.jgit.hooks;version="5.2.3";uses:="org.eclipse.jgit.lib", + org.eclipse.jgit.ignore;version="5.2.3", + org.eclipse.jgit.ignore.internal;version="5.2.3";x-friends:="org.eclipse.jgit.test", + org.eclipse.jgit.internal;version="5.2.3";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test", + org.eclipse.jgit.internal.fsck;version="5.2.3";x-friends:="org.eclipse.jgit.test", + org.eclipse.jgit.internal.ketch;version="5.2.3";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", + org.eclipse.jgit.internal.revwalk;version="5.2.3";x-internal:=true, + org.eclipse.jgit.internal.storage.dfs;version="5.2.3"; 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="5.1.9"; + org.eclipse.jgit.internal.storage.file;version="5.2.3"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.junit, org.eclipse.jgit.junit.http, org.eclipse.jgit.http.server, org.eclipse.jgit.lfs, org.eclipse.jgit.pgm, - org.eclipse.jgit.pgm.test", - org.eclipse.jgit.internal.storage.io;version="5.1.9";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.pack;version="5.1.9";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.reftable;version="5.1.9"; - x-friends:="org.eclipse.jgit.http.test,org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", - org.eclipse.jgit.internal.storage.reftree;version="5.1.9";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", - org.eclipse.jgit.lib;version="5.1.9"; + org.eclipse.jgit.pgm.test, + org.eclipse.jgit.ssh.apache", + org.eclipse.jgit.internal.storage.io;version="5.2.3";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", + org.eclipse.jgit.internal.storage.pack;version="5.2.3";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", + org.eclipse.jgit.internal.storage.reftable;version="5.2.3"; + x-friends:="org.eclipse.jgit.http.test, + org.eclipse.jgit.junit, + org.eclipse.jgit.test, + org.eclipse.jgit.pgm", + org.eclipse.jgit.internal.storage.reftree;version="5.2.3";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", + org.eclipse.jgit.internal.submodule;version="5.2.3";x-internal:=true, + org.eclipse.jgit.internal.transport.parser;version="5.2.3";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.server", + org.eclipse.jgit.internal.transport.ssh;version="5.2.3";x-friends:="org.eclipse.jgit.ssh.apache", + org.eclipse.jgit.lib;version="5.2.3"; uses:="org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.util, @@ -90,33 +98,33 @@ Export-Package: org.eclipse.jgit.annotations;version="5.1.9", org.eclipse.jgit.treewalk, org.eclipse.jgit.transport, org.eclipse.jgit.submodule", - org.eclipse.jgit.lib.internal;version="5.1.9";x-internal:=true, - org.eclipse.jgit.merge;version="5.1.9"; + org.eclipse.jgit.lib.internal;version="5.2.3";x-internal:=true, + org.eclipse.jgit.merge;version="5.2.3"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.treewalk, org.eclipse.jgit.revwalk, org.eclipse.jgit.diff, org.eclipse.jgit.dircache, org.eclipse.jgit.api", - org.eclipse.jgit.nls;version="5.1.9", - org.eclipse.jgit.notes;version="5.1.9"; + org.eclipse.jgit.nls;version="5.2.3", + org.eclipse.jgit.notes;version="5.2.3"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.treewalk, org.eclipse.jgit.revwalk, org.eclipse.jgit.merge", - org.eclipse.jgit.patch;version="5.1.9";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.diff", - org.eclipse.jgit.revplot;version="5.1.9";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.revwalk", - org.eclipse.jgit.revwalk;version="5.1.9"; + org.eclipse.jgit.patch;version="5.2.3";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.diff", + org.eclipse.jgit.revplot;version="5.2.3";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.revwalk", + org.eclipse.jgit.revwalk;version="5.2.3"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.treewalk, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.diff, org.eclipse.jgit.revwalk.filter", - org.eclipse.jgit.revwalk.filter;version="5.1.9";uses:="org.eclipse.jgit.revwalk,org.eclipse.jgit.lib,org.eclipse.jgit.util", - org.eclipse.jgit.storage.file;version="5.1.9";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.util", - org.eclipse.jgit.storage.pack;version="5.1.9";uses:="org.eclipse.jgit.lib", - org.eclipse.jgit.submodule;version="5.1.9";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.treewalk.filter,org.eclipse.jgit.treewalk", - org.eclipse.jgit.transport;version="5.1.9"; + org.eclipse.jgit.revwalk.filter;version="5.2.3";uses:="org.eclipse.jgit.revwalk,org.eclipse.jgit.lib,org.eclipse.jgit.util", + org.eclipse.jgit.storage.file;version="5.2.3";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.util", + org.eclipse.jgit.storage.pack;version="5.2.3";uses:="org.eclipse.jgit.lib", + org.eclipse.jgit.submodule;version="5.2.3";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.treewalk.filter,org.eclipse.jgit.treewalk", + org.eclipse.jgit.transport;version="5.2.3"; uses:="org.eclipse.jgit.transport.resolver, org.eclipse.jgit.revwalk, org.eclipse.jgit.internal.storage.pack, @@ -128,24 +136,24 @@ Export-Package: org.eclipse.jgit.annotations;version="5.1.9", org.eclipse.jgit.transport.http, org.eclipse.jgit.errors, org.eclipse.jgit.storage.pack", - org.eclipse.jgit.transport.http;version="5.1.9";uses:="javax.net.ssl", - org.eclipse.jgit.transport.resolver;version="5.1.9";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.transport", - org.eclipse.jgit.treewalk;version="5.1.9"; + org.eclipse.jgit.transport.http;version="5.2.3";uses:="javax.net.ssl", + org.eclipse.jgit.transport.resolver;version="5.2.3";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.transport", + org.eclipse.jgit.treewalk;version="5.2.3"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.revwalk, org.eclipse.jgit.attributes, org.eclipse.jgit.treewalk.filter, org.eclipse.jgit.util, org.eclipse.jgit.dircache", - org.eclipse.jgit.treewalk.filter;version="5.1.9";uses:="org.eclipse.jgit.treewalk", - org.eclipse.jgit.util;version="5.1.9"; + org.eclipse.jgit.treewalk.filter;version="5.2.3";uses:="org.eclipse.jgit.treewalk", + org.eclipse.jgit.util;version="5.2.3"; uses:="org.eclipse.jgit.lib, org.eclipse.jgit.transport.http, org.eclipse.jgit.storage.file, org.ietf.jgss", - org.eclipse.jgit.util.io;version="5.1.9", - org.eclipse.jgit.util.sha1;version="5.1.9", - org.eclipse.jgit.util.time;version="5.1.9" + org.eclipse.jgit.util.io;version="5.2.3", + org.eclipse.jgit.util.sha1;version="5.2.3", + org.eclipse.jgit.util.time;version="5.2.3" Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", com.jcraft.jsch;version="[0.1.37,0.2.0)", diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF index 8885e64cb9..8e22086cce 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: 5.1.9.qualifier -Eclipse-SourceBundle: org.eclipse.jgit;version="5.1.9.qualifier";roots="." +Bundle-Version: 5.2.3.qualifier +Eclipse-SourceBundle: org.eclipse.jgit;version="5.2.3.qualifier";roots="." diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml index cb35c2fb66..c30d648c69 100644 --- a/org.eclipse.jgit/pom.xml +++ b/org.eclipse.jgit/pom.xml @@ -53,7 +53,7 @@ <parent> <groupId>org.eclipse.jgit</groupId> <artifactId>org.eclipse.jgit-parent</artifactId> - <version>5.1.9-SNAPSHOT</version> + <version>5.2.3-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 290a0a28ef..35f3dabe53 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -303,7 +303,7 @@ expectedPktLineWithService=expected pkt-line with ''# service=-'', got ''{0}'' expectedReceivedContentType=expected Content-Type {0}; received Content-Type {1} expectedReportForRefNotReceived={0}: expected report for ref {1} not received failedAtomicFileCreation=Atomic file creation failed, number of hard links to file {0} was not 2 but {1}" -failedToDetermineFilterDefinition=An exception occured while determining filter definitions +failedToDetermineFilterDefinition=An exception occurred while determining filter definitions failedUpdatingRefs=failed updating refs failureDueToOneOfTheFollowing=Failure due to one of the following: failureUpdatingFETCH_HEAD=Failure updating FETCH_HEAD: {0} @@ -470,6 +470,7 @@ newIdMustNotBeNull=New ID must not be null newlineInQuotesNotAllowed=Newline in quotes not allowed noApplyInDelete=No apply in delete noClosingBracket=No closing {0} found for {1} at index {2}. +noCommitsSelectedForShallow=No commits selected for shallow request noCredentialsProvider=Authentication is required but no CredentialsProvider has been registered noHEADExistsAndNoExplicitStartingRevisionWasSpecified=No HEAD exists and no explicit starting revision was specified noHMACsupport=No {0} support: {1} @@ -642,8 +643,8 @@ sourceRefNotSpecifiedForRefspec=Source ref not specified for refspec: {0} squashCommitNotUpdatingHEAD=Squash commit -- not updating HEAD sshCommandFailed=Execution of ssh command ''{0}'' failed with error ''{1}'' sshUserNameError=Jsch error: failed to set SSH user name correctly to ''{0}''; using ''{1}'' picked up from SSH config file. -sslFailureExceptionMessage=Secure connection to {0} could not be stablished because of SSL problems -sslFailureInfo=A secure connection to {0}\ncould not be established because the server''s certificate could not be validated. +sslFailureExceptionMessage=Secure connection to {0} could not be established because of SSL problems +sslFailureInfo=A secure connection to {0} could not be established because the server''s certificate could not be validated. sslFailureCause=SSL reported: {0} sslFailureTrustExplanation=Do you want to skip SSL verification for this server? sslTrustAlways=Always skip SSL verification for this server from now on @@ -675,7 +676,6 @@ submodulePathInvalid=Invalid submodule path ''{0}'' submoduleUrlInvalid=Invalid submodule URL ''{0}'' submodulesNotSupported=Submodules are not supported supportOnlyPackIndexVersion2=Only support index version 2 -symlinkCannotBeWrittenAsTheLinkTarget=Symlink "{0}" cannot be written as the link target cannot be read from within Java. systemConfigFileInvalid=System wide config file {0} is invalid {1} tagAlreadyExists=tag ''{0}'' already exists tagNameInvalid=tag name {0} is invalid @@ -776,7 +776,9 @@ uriNotFound={0} not found uriNotFoundWithMessage={0} not found: {1} URINotSupported=URI not supported: {0} userConfigFileInvalid=User config file {0} invalid {1} +validatingGitModules=Validating .gitmodules files walkFailure=Walk failure. +wantNoSpaceWithCapabilities=No space between oid and first capability in first want line wantNotValid=want {0} not valid weeksAgo={0} weeks ago windowSizeMustBeLesserThanLimit=Window size must be < limit 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 5b84032b15..c6f3c671a9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java @@ -42,11 +42,14 @@ */ package org.eclipse.jgit.api; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.File; import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.util.ArrayList; @@ -258,7 +261,8 @@ public class ApplyCommand extends GitCommand<ApplyResult> { if (sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } - try (FileWriter fw = new FileWriter(f)) { + try (Writer fw = new OutputStreamWriter(new FileOutputStream(f), + UTF_8)) { fw.write(sb.toString()); } 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 11edb10894..455a2e665f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java @@ -269,7 +269,7 @@ public class CheckoutCommand extends GitCommand<Ref> { try { dco = new DirCacheCheckout(repo, headTree, dc, newCommit.getTree()); - dco.setFailOnConflict(true); + dco.setFailOnConflict(!force); dco.setProgressMonitor(monitor); try { dco.checkout(); 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 5c06bac1f2..73af8ba16d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java @@ -283,12 +283,11 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { config.addURI(u); final String dst = (bare ? Constants.R_HEADS : Constants.R_REMOTES - + config.getName() + "/") + "*"; //$NON-NLS-1$//$NON-NLS-2$ - RefSpec refSpec = new RefSpec(); - refSpec = refSpec.setForceUpdate(true); - refSpec = refSpec.setSourceDestination(Constants.R_HEADS + "*", dst); //$NON-NLS-1$ + + config.getName() + '/') + '*'; + boolean fetchAll = cloneAllBranches || branchesToClone == null + || branchesToClone.isEmpty(); - config.addFetchRefSpec(refSpec); + config.setFetchRefSpecs(calculateRefSpecs(fetchAll, dst)); config.update(clonedRepo.getConfig()); clonedRepo.getConfig().save(); @@ -297,27 +296,25 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { FetchCommand command = new FetchCommand(clonedRepo); command.setRemote(remote); command.setProgressMonitor(monitor); - command.setTagOpt(TagOpt.FETCH_TAGS); + command.setTagOpt(fetchAll ? TagOpt.FETCH_TAGS : TagOpt.AUTO_FOLLOW); configure(command); - List<RefSpec> specs = calculateRefSpecs(dst); - command.setRefSpecs(specs); - return command.call(); } - private List<RefSpec> calculateRefSpecs(String dst) { + private List<RefSpec> calculateRefSpecs(boolean fetchAll, String dst) { RefSpec wcrs = new RefSpec(); wcrs = wcrs.setForceUpdate(true); - wcrs = wcrs.setSourceDestination(Constants.R_HEADS + "*", dst); //$NON-NLS-1$ + wcrs = wcrs.setSourceDestination(Constants.R_HEADS + '*', dst); List<RefSpec> specs = new ArrayList<>(); - if (cloneAllBranches) - specs.add(wcrs); - else if (branchesToClone != null - && branchesToClone.size() > 0) { - for (String selectedRef : branchesToClone) - if (wcrs.matchSource(selectedRef)) + if (!fetchAll) { + for (String selectedRef : branchesToClone) { + if (wcrs.matchSource(selectedRef)) { specs.add(wcrs.expandFromSource(selectedRef)); + } + } + } else { + specs.add(wcrs); } return specs; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ListBranchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ListBranchCommand.java index 28a27a90e0..29a51a0f02 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ListBranchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ListBranchCommand.java @@ -44,6 +44,10 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Constants.R_REMOTES; + import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -56,7 +60,6 @@ import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; @@ -113,17 +116,18 @@ public class ListBranchCommand extends GitCommand<List<Ref>> { Collection<Ref> refs = new ArrayList<>(); // Also return HEAD if it's detached - Ref head = repo.exactRef(Constants.HEAD); - if (head != null && head.getLeaf().getName().equals(Constants.HEAD)) + Ref head = repo.exactRef(HEAD); + if (head != null && head.getLeaf().getName().equals(HEAD)) { refs.add(head); + } if (listMode == null) { - refs.addAll(getRefs(Constants.R_HEADS)); + refs.addAll(repo.getRefDatabase().getRefsByPrefix(R_HEADS)); } else if (listMode == ListMode.REMOTE) { - refs.addAll(getRefs(Constants.R_REMOTES)); + refs.addAll(repo.getRefDatabase().getRefsByPrefix(R_REMOTES)); } else { - refs.addAll(getRefs(Constants.R_HEADS)); - refs.addAll(getRefs(Constants.R_REMOTES)); + refs.addAll(repo.getRefDatabase().getRefsByPrefix(R_HEADS, + R_REMOTES)); } resultRefs = new ArrayList<>(filterRefs(refs)); } catch (IOException e) { @@ -185,8 +189,4 @@ public class ListBranchCommand extends GitCommand<List<Ref>> { this.containsCommitish = containsCommitish; return this; } - - private Collection<Ref> getRefs(String prefix) throws IOException { - return repo.getRefDatabase().getRefsByPrefix(prefix); - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java index 244a15686f..f92455a96a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java @@ -179,21 +179,6 @@ public class SubmoduleAddCommand extends // Use the path as the default. name = path; } - if (name.contains("/../") || name.contains("\\..\\") //$NON-NLS-1$ //$NON-NLS-2$ - || name.startsWith("../") || name.startsWith("..\\") //$NON-NLS-1$ //$NON-NLS-2$ - || name.endsWith("/..") || name.endsWith("\\..")) { //$NON-NLS-1$ //$NON-NLS-2$ - // Submodule names are used to store the submodule repositories - // under $GIT_DIR/modules. Having ".." in submodule names makes a - // vulnerability (CVE-2018-11235 - // https://bugs.eclipse.org/bugs/show_bug.cgi?id=535027#c0) - // Reject the names with them. The callers need to make sure the - // names free from these. We don't automatically replace these - // characters or canonicalize by regarding the name as a file path. - // Since Path class is platform dependent, we manually check '/' and - // '\\' patterns here. - throw new IllegalArgumentException(MessageFormat - .format(JGitText.get().invalidNameContainsDotDot, name)); - } try { SubmoduleValidator.assertValidSubmoduleName(name); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportConfigCallback.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportConfigCallback.java index f60926c562..d73453c5af 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportConfigCallback.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportConfigCallback.java @@ -68,5 +68,5 @@ public interface TransportConfigCallback { * @param transport * a {@link org.eclipse.jgit.transport.Transport} object. */ - public void configure(Transport transport); + void configure(Transport transport); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNodeProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNodeProvider.java index f1d7d7be0e..2d1cde12e0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNodeProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesNodeProvider.java @@ -63,7 +63,7 @@ public interface AttributesNodeProvider { * @throws java.io.IOException * if an error is raised while parsing the attributes file */ - public AttributesNode getInfoAttributesNode() throws IOException; + AttributesNode getInfoAttributesNode() throws IOException; /** * Retrieve the {@link org.eclipse.jgit.attributes.AttributesNode} that @@ -76,6 +76,6 @@ public interface AttributesNodeProvider { * attributes file * @see CoreConfig#getAttributesFile() */ - public AttributesNode getGlobalAttributesNode() throws IOException; + AttributesNode getGlobalAttributesNode() throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesProvider.java index 1545e3523d..7b51f6dd32 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesProvider.java @@ -53,5 +53,5 @@ public interface AttributesProvider { * * @return the currently active attributes */ - public Attributes getAttributes(); + Attributes getAttributes(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommand.java index c4357d1297..0bb4516297 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommand.java @@ -95,7 +95,7 @@ public abstract class FilterCommand { * -1. -1 means that the {@link java.io.InputStream} is completely * processed. * @throws java.io.IOException - * when {@link java.io.IOException} occured while reading from + * when {@link java.io.IOException} occurred while reading from * {@link #in} or writing to {@link #out} */ public abstract int run() throws IOException; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandFactory.java index 11b76b0d90..78573d2a63 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandFactory.java @@ -69,7 +69,7 @@ public interface FilterCommandFactory { * thrown when the command constructor throws an * java.io.IOException */ - public FilterCommand create(Repository db, InputStream in, - OutputStream out) throws IOException; + FilterCommand create(Repository db, InputStream in, OutputStream out) + throws IOException; } 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 5110d77dc9..8aa97df777 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -525,7 +525,7 @@ public class DirCacheCheckout { builder.finish(); // init progress reporting - int numTotal = removed.size() + updated.size(); + int numTotal = removed.size() + updated.size() + conflicts.size(); monitor.beginTask(JGitText.get().checkingOutFiles, numTotal); performingCheckout = true; @@ -600,6 +600,33 @@ public class DirCacheCheckout { } throw ex; } + for (String conflict : conflicts) { + // the conflicts are likely to have multiple entries in the + // dircache, we only want to check out the one for the "theirs" + // tree + int entryIdx = dc.findEntry(conflict); + if (entryIdx >= 0) { + while (entryIdx < dc.getEntryCount()) { + DirCacheEntry entry = dc.getEntry(entryIdx); + if (!entry.getPathString().equals(conflict)) { + break; + } + if (entry.getStage() == DirCacheEntry.STAGE_3) { + checkoutEntry(repo, entry, objectReader, false, + null); + break; + } + ++entryIdx; + } + } + + monitor.update(1); + if (monitor.isCancelled()) { + throw new CanceledException(MessageFormat.format( + JGitText.get().operationCanceled, + JGitText.get().checkingOutFiles)); + } + } monitor.endTask(); // commit the index builder - a new index is persisted diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java index 19c916f810..6196e758a9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java @@ -44,6 +44,8 @@ package org.eclipse.jgit.dircache; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.IOException; import java.io.InputStream; import java.util.Collections; @@ -75,7 +77,7 @@ import org.eclipse.jgit.util.RawParseUtils; public class DirCacheIterator extends AbstractTreeIterator { /** Byte array holding ".gitattributes" string */ private static final byte[] DOT_GIT_ATTRIBUTES_BYTES = Constants.DOT_GIT_ATTRIBUTES - .getBytes(); + .getBytes(UTF_8); /** The cache this iterator was created to walk. */ protected final DirCache cache; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java index 9fbcc4dd50..afd7889f8a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/WorkingTreeModifiedEvent.java @@ -94,7 +94,8 @@ public class WorkingTreeModifiedEvent * * @return the set */ - public @NonNull Collection<String> getModified() { + @NonNull + public Collection<String> getModified() { Collection<String> result = modified; if (result == null) { result = Collections.emptyList(); @@ -109,7 +110,8 @@ public class WorkingTreeModifiedEvent * * @return the set */ - public @NonNull Collection<String> getDeleted() { + @NonNull + public Collection<String> getDeleted() { Collection<String> result = deleted; if (result == null) { result = Collections.emptyList(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/Head.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/Head.java index 49839f8e6e..e8f6844dda 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/Head.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/Head.java @@ -54,5 +54,5 @@ interface Head { * the character which decides which heads are returned. * @return a list of heads based on the input. */ - public abstract List<Head> getNextHeads(char c); + List<Head> getNextHeads(char c); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java index 26e783ddd7..8e463415b8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java @@ -358,7 +358,8 @@ public class ManifestParser extends DefaultHandler { * * @return filtered projects list reference, never null */ - public @NonNull List<RepoProject> getFilteredProjects() { + @NonNull + public List<RepoProject> getFilteredProjects() { return filteredProjects; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java index 5a73cdc067..e9d86dfa83 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java @@ -59,12 +59,14 @@ import java.util.Objects; import java.util.StringJoiner; import java.util.TreeMap; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.GitCommand; import org.eclipse.jgit.api.SubmoduleAddCommand; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; @@ -80,7 +82,6 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; @@ -90,6 +91,7 @@ import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.FileUtils; /** @@ -144,7 +146,9 @@ public class RepoCommand extends GitCommand<RevCommit> { * @param uri * The URI of the remote repository * @param ref - * The ref (branch/tag/etc.) to read + * Name of the ref to lookup. May be a short-hand form, e.g. + * "master" which is is automatically expanded to + * "refs/heads/master" if "refs/heads/master" already exists. * @return the sha1 of the remote repository, or null if the ref does * not exist. * @throws GitAPIException @@ -165,13 +169,93 @@ public class RepoCommand extends GitCommand<RevCommit> { * @throws GitAPIException * @throws IOException * @since 3.5 + * + * @deprecated Use {@link #readFileWithMode(String, String, String)} + * instead + */ + @Deprecated + public default byte[] readFile(String uri, String ref, String path) + throws GitAPIException, IOException { + return readFileWithMode(uri, ref, path).getContents(); + } + + /** + * Read contents and mode (i.e. permissions) of the file from a remote + * repository. + * + * @param uri + * The URI of the remote repository + * @param ref + * Name of the ref to lookup. May be a short-hand form, e.g. + * "master" which is is automatically expanded to + * "refs/heads/master" if "refs/heads/master" already exists. + * @param path + * The relative path (inside the repo) to the file to read + * @return The contents and file mode of the file in the given + * repository and branch. Never null. + * @throws GitAPIException + * If the ref have an invalid or ambiguous name, or it does + * not exist in the repository, + * @throws IOException + * If the object does not exist or is too large + * @since 5.2 */ - public byte[] readFile(String uri, String ref, String path) + @NonNull + public RemoteFile readFileWithMode(String uri, String ref, String path) throws GitAPIException, IOException; } + /** + * Read-only view of contents and file mode (i.e. permissions) for a file in + * a remote repository. + * + * @since 5.2 + */ + public static final class RemoteFile { + @NonNull + private final byte[] contents; + + @NonNull + private final FileMode fileMode; + + /** + * @param contents + * Raw contents of the file. + * @param fileMode + * Git file mode for this file (e.g. executable or regular) + */ + public RemoteFile(@NonNull byte[] contents, + @NonNull FileMode fileMode) { + this.contents = Objects.requireNonNull(contents); + this.fileMode = Objects.requireNonNull(fileMode); + } + + /** + * Contents of the file. + * <p> + * Callers who receive this reference must not modify its contents (as + * it can point to internal cached data). + * + * @return Raw contents of the file. Do not modify it. + */ + @NonNull + public byte[] getContents() { + return contents; + } + + /** + * @return Git file mode for this file (e.g. executable or regular) + */ + @NonNull + public FileMode getFileMode() { + return fileMode; + } + + } + /** A default implementation of {@link RemoteReader} callback. */ public static class DefaultRemoteReader implements RemoteReader { + @Override public ObjectId sha1(String uri, String ref) throws GitAPIException { Map<String, Ref> map = Git @@ -183,38 +267,30 @@ public class RepoCommand extends GitCommand<RevCommit> { } @Override - public byte[] readFile(String uri, String ref, String path) + public RemoteFile readFileWithMode(String uri, String ref, String path) throws GitAPIException, IOException { File dir = FileUtils.createTempDir("jgit_", ".git", null); //$NON-NLS-1$ //$NON-NLS-2$ try (Git git = Git.cloneRepository().setBare(true).setDirectory(dir) .setURI(uri).call()) { - return readFileFromRepo(git.getRepository(), ref, path); + Repository repo = git.getRepository(); + ObjectId refCommitId = sha1(uri, ref); + if (refCommitId == null) { + throw new InvalidRefNameException(MessageFormat + .format(JGitText.get().refNotResolved, ref)); + } + RevCommit commit = repo.parseCommit(refCommitId); + TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree()); + + // TODO(ifrade): Cope better with big files (e.g. using + // InputStream instead of byte[]) + return new RemoteFile( + tw.getObjectReader().open(tw.getObjectId(0)) + .getCachedBytes(Integer.MAX_VALUE), + tw.getFileMode(0)); } finally { FileUtils.delete(dir, FileUtils.RECURSIVE); } } - - /** - * Read a file from the repository - * - * @param repo - * The repository containing the file - * @param ref - * The ref (branch/tag/etc.) to read - * @param path - * The relative path (inside the repo) to the file to read - * @return the file's content - * @throws GitAPIException - * @throws IOException - * @since 3.5 - */ - protected byte[] readFileFromRepo(Repository repo, - String ref, String path) throws GitAPIException, IOException { - try (ObjectReader reader = repo.newObjectReader()) { - ObjectId oid = repo.resolve(ref + ":" + path); //$NON-NLS-1$ - return reader.open(oid).getBytes(Integer.MAX_VALUE); - } - } } @SuppressWarnings("serial") @@ -587,12 +663,13 @@ public class RepoCommand extends GitCommand<RevCommit> { builder.add(dcEntry); for (CopyFile copyfile : proj.getCopyFiles()) { - byte[] src = callback.readFile( + RemoteFile rf = callback.readFileWithMode( url, proj.getRevision(), copyfile.src); - objectId = inserter.insert(Constants.OBJ_BLOB, src); + objectId = inserter.insert(Constants.OBJ_BLOB, + rf.getContents()); dcEntry = new DirCacheEntry(copyfile.dest); dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.REGULAR_FILE); + dcEntry.setFileMode(rf.getFileMode()); builder.add(dcEntry); } for (LinkFile linkfile : proj.getLinkFiles()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java index 7ba83c7cbf..d79dfa8b2f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java @@ -136,6 +136,7 @@ public class RepoProject implements Comparable<RepoProject> { FileChannel channel = input.getChannel(); output.getChannel().transferFrom(channel, 0, channel.size()); } + destFile.setExecutable(srcFile.canExecute()); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java index 051a1d1c81..431944f9d4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java @@ -164,12 +164,7 @@ public class PrePushHook extends GitHook<String> { */ public void setRefs(Collection<RemoteRefUpdate> toRefs) { StringBuilder b = new StringBuilder(); - boolean first = true; for (RemoteRefUpdate u : toRefs) { - if (!first) - b.append("\n"); //$NON-NLS-1$ - else - first = false; b.append(u.getSrcRef()); b.append(" "); //$NON-NLS-1$ b.append(u.getNewObjectId().getName()); @@ -179,6 +174,7 @@ public class PrePushHook extends GitHook<String> { ObjectId ooid = u.getExpectedOldObjectId(); b.append((ooid == null) ? ObjectId.zeroId().getName() : ooid .getName()); + b.append("\n"); //$NON-NLS-1$ } refs = b.toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java index 9b255b41c3..41923eed18 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java @@ -443,7 +443,7 @@ public class Strings { if (in_brackets > 0) throw new InvalidPatternException("Not closed bracket?", pattern); //$NON-NLS-1$ try { - return Pattern.compile(sb.toString()); + return Pattern.compile(sb.toString(), Pattern.DOTALL); } catch (PatternSyntaxException e) { throw new InvalidPatternException( MessageFormat.format(JGitText.get().invalidIgnoreRule, 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 00aaa42b47..4914927ad5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -531,6 +531,7 @@ public class JGitText extends TranslationBundle { /***/ public String newlineInQuotesNotAllowed; /***/ public String noApplyInDelete; /***/ public String noClosingBracket; + /***/ public String noCommitsSelectedForShallow; /***/ public String noCredentialsProvider; /***/ public String noHEADExistsAndNoExplicitStartingRevisionWasSpecified; /***/ public String noHMACsupport; @@ -733,10 +734,8 @@ public class JGitText extends TranslationBundle { /***/ public String submoduleNameInvalid; /***/ public String submoduleParentRemoteUrlInvalid; /***/ public String submodulePathInvalid; - /***/ public String submodulesNotSupported; /***/ public String submoduleUrlInvalid; /***/ public String supportOnlyPackIndexVersion2; - /***/ public String symlinkCannotBeWrittenAsTheLinkTarget; /***/ public String systemConfigFileInvalid; /***/ public String tagAlreadyExists; /***/ public String tagNameInvalid; @@ -837,7 +836,9 @@ public class JGitText extends TranslationBundle { /***/ public String uriNotFoundWithMessage; /***/ public String URINotSupported; /***/ public String userConfigFileInvalid; + /***/ public String validatingGitModules; /***/ public String walkFailure; + /***/ public String wantNoSpaceWithCapabilities; /***/ public String wantNotValid; /***/ public String weeksAgo; /***/ public String windowSizeMustBeLesserThanLimit; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java index 131b0048ae..335ac667cc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java @@ -61,20 +61,21 @@ public class FsckError { final int type; - ObjectChecker.ErrorType errorType; + @Nullable + final ObjectChecker.ErrorType errorType; /** * @param id * the object identifier. * @param type * type of the object. + * @param errorType + * kind of error */ - public CorruptObject(ObjectId id, int type) { + public CorruptObject(ObjectId id, int type, + @Nullable ObjectChecker.ErrorType errorType) { this.id = id; this.type = type; - } - - void setErrorType(ObjectChecker.ErrorType errorType) { this.errorType = errorType; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java index 5397ba4798..50594df1dd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java @@ -174,12 +174,8 @@ public class FsckPackParser extends PackParser { try { super.verifySafeObject(id, type, data); } catch (CorruptObjectException e) { - // catch the exception and continue parse the pack file - CorruptObject o = new CorruptObject(id.toObjectId(), type); - if (e.getErrorType() != null) { - o.setErrorType(e.getErrorType()); - } - corruptObjects.add(o); + corruptObjects.add( + new CorruptObject(id.toObjectId(), type, e.getErrorType())); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsFsck.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsFsck.java index 3f96d0919b..c0e24c02cf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsFsck.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsFsck.java @@ -43,6 +43,7 @@ package org.eclipse.jgit.internal.storage.dfs; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; @@ -54,12 +55,18 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.fsck.FsckError; import org.eclipse.jgit.internal.fsck.FsckError.CorruptIndex; +import org.eclipse.jgit.internal.fsck.FsckError.CorruptObject; import org.eclipse.jgit.internal.fsck.FsckPackParser; import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; +import org.eclipse.jgit.internal.submodule.SubmoduleValidator; +import org.eclipse.jgit.internal.submodule.SubmoduleValidator.SubmoduleValidationException; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.GitmoduleEntry; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectChecker; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.ObjectWalk; @@ -102,6 +109,7 @@ public class DfsFsck { FsckError errors = new FsckError(); if (!connectivityOnly) { + objChecker.reset(); checkPacks(pm, errors); } checkConnectivity(pm, errors); @@ -128,6 +136,8 @@ public class DfsFsck { } } } + + checkGitModules(pm, errors); } private void verifyPack(ProgressMonitor pm, FsckError errors, DfsReader ctx, @@ -142,6 +152,28 @@ public class DfsFsck { fpp.verifyIndex(pack.getPackIndex(ctx)); } + private void checkGitModules(ProgressMonitor pm, FsckError errors) + throws IOException { + pm.beginTask(JGitText.get().validatingGitModules, + objChecker.getGitsubmodules().size()); + for (GitmoduleEntry entry : objChecker.getGitsubmodules()) { + AnyObjectId blobId = entry.getBlobId(); + ObjectLoader blob = objdb.open(blobId, Constants.OBJ_BLOB); + + try { + SubmoduleValidator.assertValidGitModulesFile( + new String(blob.getBytes(), UTF_8)); + } catch (SubmoduleValidationException e) { + CorruptObject co = new FsckError.CorruptObject( + blobId.toObjectId(), Constants.OBJ_BLOB, + e.getFsckMessageId()); + errors.getCorruptObjects().add(co); + } + pm.update(1); + } + pm.endTask(); + } + private void checkConnectivity(ProgressMonitor pm, FsckError errors) throws IOException { pm.beginTask(JGitText.get().countingObjects, ProgressMonitor.UNKNOWN); @@ -179,6 +211,9 @@ public class DfsFsck { * Use a customized object checker instead of the default one. Caller can * specify a skip list to ignore some errors. * + * It will be reset at the start of each {{@link #check(ProgressMonitor)} + * call. + * * @param objChecker * A customized object checker. */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReadableChannel.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReadableChannel.java index 9b98250884..d5e17224cf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReadableChannel.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReadableChannel.java @@ -57,7 +57,7 @@ public interface ReadableChannel extends ReadableByteChannel { * @throws java.io.IOException * the channel's current position cannot be obtained. */ - public long position() throws IOException; + long position() throws IOException; /** * Seek the current position of the channel to a new offset. @@ -70,7 +70,7 @@ public interface ReadableChannel extends ReadableByteChannel { * channel only supports block aligned IO and the current * position is not block aligned. */ - public void position(long newPosition) throws IOException; + void position(long newPosition) throws IOException; /** * Get the total size of the channel. @@ -83,7 +83,7 @@ public interface ReadableChannel extends ReadableByteChannel { * @throws java.io.IOException * the size cannot be determined. */ - public long size() throws IOException; + long size() throws IOException; /** * Get the recommended alignment for reads. @@ -102,7 +102,7 @@ public interface ReadableChannel extends ReadableByteChannel { * @return recommended alignment size for randomly positioned reads. Does * not need to be a power of 2. */ - public int blockSize(); + int blockSize(); /** * Recommend the channel maintain a read-ahead buffer. @@ -131,5 +131,5 @@ public interface ReadableChannel extends ReadableByteChannel { * @throws java.io.IOException * if the read ahead cannot be adjusted. */ - public void setReadAheadBytes(int bufferSize) throws IOException; + void setReadAheadBytes(int bufferSize) 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 e35b9c9e4a..35522667e0 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 @@ -43,6 +43,7 @@ package org.eclipse.jgit.internal.storage.file; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; @@ -50,7 +51,6 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; @@ -1036,8 +1036,8 @@ public class ObjectDirectory extends FileObjectDatabase { } private static BufferedReader open(File f) - throws FileNotFoundException { - return new BufferedReader(new FileReader(f)); + throws IOException, FileNotFoundException { + return Files.newBufferedReader(f.toPath(), UTF_8); } private AlternateHandle openAlternate(String location) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectReuseAsIs.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectReuseAsIs.java index f759e23ef4..2c806235a6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectReuseAsIs.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectReuseAsIs.java @@ -79,7 +79,7 @@ public interface ObjectReuseAsIs { * the Git type of the object that will be packed. * @return a new instance for this object. */ - public ObjectToPack newObjectToPack(AnyObjectId objectId, int type); + ObjectToPack newObjectToPack(AnyObjectId objectId, int type); /** * Select the best object representation for a packer. @@ -114,7 +114,7 @@ public interface ObjectReuseAsIs { * @throws java.io.IOException * the repository cannot be accessed. Packing will abort. */ - public void selectObjectRepresentation(PackWriter packer, + void selectObjectRepresentation(PackWriter packer, ProgressMonitor monitor, Iterable<ObjectToPack> objects) throws IOException, MissingObjectException; @@ -155,7 +155,7 @@ public interface ObjectReuseAsIs { * the stream cannot be written to, or one or more required * objects cannot be accessed from the object database. */ - public void writeObjects(PackOutputStream out, List<ObjectToPack> list) + void writeObjects(PackOutputStream out, List<ObjectToPack> list) throws IOException; /** @@ -200,7 +200,7 @@ public interface ObjectReuseAsIs { * the stream's write method threw an exception. Packing will * abort. */ - public void copyObjectAsIs(PackOutputStream out, ObjectToPack otp, + void copyObjectAsIs(PackOutputStream out, ObjectToPack otp, boolean validate) throws IOException, StoredObjectRepresentationNotAvailableException; @@ -216,7 +216,7 @@ public interface ObjectReuseAsIs { * @throws java.io.IOException * the pack cannot be read, or stream did not accept a write. */ - public abstract void copyPackAsIs(PackOutputStream out, CachedPack pack) + void copyPackAsIs(PackOutputStream out, CachedPack pack) throws IOException; /** @@ -234,6 +234,6 @@ public interface ObjectReuseAsIs { * Callers may choose to ignore this and continue as-if there * were no cached packs. */ - public Collection<CachedPack> getCachedPacksAndUpdate( + Collection<CachedPack> getCachedPacksAndUpdate( BitmapBuilder needBitmap) throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java index 7f38a7b51a..eb777be809 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java @@ -187,6 +187,7 @@ public final class PackOutputStream extends OutputStream { * @throws java.io.IOException * the underlying stream refused to accept the header. */ + @SuppressWarnings("ShortCircuitBoolean") public final void writeHeader(ObjectToPack otp, long rawLength) throws IOException { ObjectToPack b = otp.getDeltaBase(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/submodule/SubmoduleValidator.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/submodule/SubmoduleValidator.java index 3651631573..7b872b1860 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/submodule/SubmoduleValidator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/submodule/SubmoduleValidator.java @@ -45,13 +45,17 @@ package org.eclipse.jgit.internal.submodule; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_URL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_SUBMODULE_SECTION; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.GITMODULES_NAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.GITMODULES_PARSE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.GITMODULES_PATH; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.GITMODULES_URL; -import java.io.IOException; import java.text.MessageFormat; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectChecker; /** * Validations for the git submodule fields (name, path, uri). @@ -66,15 +70,30 @@ public class SubmoduleValidator { */ public static class SubmoduleValidationException extends Exception { + private static final long serialVersionUID = 1L; + + private final ObjectChecker.ErrorType fsckMessageId; + /** * @param message * Description of the problem + * @param fsckMessageId + * Error identifier, following the git fsck fsck.<msg-id> + * format */ - public SubmoduleValidationException(String message) { + SubmoduleValidationException(String message, + ObjectChecker.ErrorType fsckMessageId) { super(message); + this.fsckMessageId = fsckMessageId; } - private static final long serialVersionUID = 1L; + + /** + * @return the error identifier + */ + public ObjectChecker.ErrorType getFsckMessageId() { + return fsckMessageId; + } } /** @@ -100,13 +119,15 @@ public class SubmoduleValidator { // Since Path class is platform dependent, we manually check '/' and // '\\' patterns here. throw new SubmoduleValidationException(MessageFormat - .format(JGitText.get().invalidNameContainsDotDot, name)); + .format(JGitText.get().invalidNameContainsDotDot, name), + GITMODULES_NAME); } if (name.startsWith("-")) { //$NON-NLS-1$ throw new SubmoduleValidationException( MessageFormat.format( - JGitText.get().submoduleNameInvalid, name)); + JGitText.get().submoduleNameInvalid, name), + GITMODULES_NAME); } } @@ -123,7 +144,8 @@ public class SubmoduleValidator { if (uri.startsWith("-")) { //$NON-NLS-1$ throw new SubmoduleValidationException( MessageFormat.format( - JGitText.get().submoduleUrlInvalid, uri)); + JGitText.get().submoduleUrlInvalid, uri), + GITMODULES_URL); } } @@ -140,19 +162,22 @@ public class SubmoduleValidator { if (path.startsWith("-")) { //$NON-NLS-1$ throw new SubmoduleValidationException( MessageFormat.format( - JGitText.get().submodulePathInvalid, path)); + JGitText.get().submodulePathInvalid, path), + GITMODULES_PATH); } } /** + * Validate a .gitmodules file + * * @param gitModulesContents * Contents of a .gitmodule file. They will be parsed internally. - * @throws IOException - * If the contents + * @throws SubmoduleValidationException + * if the contents don't look like a configuration file or field + * values are not valid */ public static void assertValidGitModulesFile(String gitModulesContents) - throws IOException { - // Validate .gitmodules file + throws SubmoduleValidationException { Config c = new Config(); try { c.fromText(gitModulesContents); @@ -173,12 +198,9 @@ public class SubmoduleValidator { } } } catch (ConfigInvalidException e) { - throw new IOException( - MessageFormat.format( - JGitText.get().invalidGitModules, - e)); - } catch (SubmoduleValidationException e) { - throw new IOException(e.getMessage(), e); + throw new SubmoduleValidationException( + JGitText.get().invalidGitModules, + GITMODULES_PARSE); } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstWant.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstWant.java new file mode 100644 index 0000000000..2dae021702 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/parser/FirstWant.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2018, Google LLC. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.parser; + +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.internal.JGitText; + +/** + * In the pack negotiation phase (protocol v0/v1), the client sends a list of + * wants. The first "want" line is special, as it (can) have a list of + * capabilities appended. + * + * E.g. "want oid cap1 cap2 cap3" + * + * Do not confuse this line with the first one in the reference advertisement, + * which is sent by the server, looks like + * "b8f7c471373b8583ced0025cfad8c9916c484b76 HEAD\0 cap1 cap2 cap3" and is + * parsed by the BasePackConnection.readAdvertisedRefs method. + * + * This class parses the input want line and holds the results: the actual want + * line and the capabilities. + * + * @since 5.2 + */ +public class FirstWant { + private final String line; + + private final Set<String> capabilities; + + @Nullable + private final String agent; + + private static final String AGENT_PREFIX = OPTION_AGENT + '='; + + /** + * Parse the first want line in the protocol v0/v1 pack negotiation. + * + * @param line + * line from the client. + * @return an instance of FirstWant + * @throws PackProtocolException + * if the line doesn't follow the protocol format. + */ + public static FirstWant fromLine(String line) throws PackProtocolException { + String wantLine; + Set<String> capabilities; + String agent = null; + + if (line.length() > 45) { + String opt = line.substring(45); + if (!opt.startsWith(" ")) { //$NON-NLS-1$ + throw new PackProtocolException(JGitText.get().wantNoSpaceWithCapabilities); + } + opt = opt.substring(1); + + HashSet<String> opts = new HashSet<>(); + for (String clientCapability : opt.split(" ")) { //$NON-NLS-1$ + if (clientCapability.startsWith(AGENT_PREFIX)) { + agent = clientCapability.substring(AGENT_PREFIX.length()); + } else { + opts.add(clientCapability); + } + } + wantLine = line.substring(0, 45); + capabilities = Collections.unmodifiableSet(opts); + } else { + wantLine = line; + capabilities = Collections.emptySet(); + } + + return new FirstWant(wantLine, capabilities, agent); + } + + private FirstWant(String line, Set<String> capabilities, + @Nullable String agent) { + this.line = line; + this.capabilities = capabilities; + this.agent = agent; + } + + /** @return non-capabilities part of the line. */ + public String getLine() { + return line; + } + + /** + * @return capabilities parsed from the line as an immutable set (excluding + * agent). + */ + public Set<String> getCapabilities() { + return capabilities; + } + + /** @return client user agent parsed from the line. */ + @Nullable + public String getAgent() { + return agent; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java new file mode 100644 index 0000000000..c1e94a0a3e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java @@ -0,0 +1,924 @@ +/* + * Copyright (C) 2008, 2017, Google Inc. + * Copyright (C) 2017, 2018, Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.transport.ssh; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.errors.InvalidPatternException; +import org.eclipse.jgit.fnmatch.FileNameMatcher; +import org.eclipse.jgit.transport.SshConstants; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.SystemReader; + +/** + * Fairly complete configuration parser for the openssh ~/.ssh/config file. + * <p> + * Both JSch 0.1.54 and Apache MINA sshd 2.1.0 have parsers for this, but both + * are buggy. Therefore we implement our own parser to read an openssh + * configuration file. + * </p> + * <p> + * Limitations compared to the full openssh 7.5 parser: + * </p> + * <ul> + * <li>This parser does not handle Match or Include keywords. + * <li>This parser does not do host name canonicalization. + * </ul> + * <p> + * Note that openssh's readconf.c is a validating parser; this parser does not + * validate entries. + * </p> + * <p> + * This config does %-substitutions for the following tokens: + * </p> + * <ul> + * <li>%% - single % + * <li>%C - short-hand for %l%h%p%r. + * <li>%d - home directory path + * <li>%h - remote host name + * <li>%L - local host name without domain + * <li>%l - FQDN of the local host + * <li>%n - host name as specified in {@link #lookup(String, int, String)} + * <li>%p - port number; if not given in {@link #lookup(String, int, String)} + * replaced only if set in the config + * <li>%r - remote user name; if not given in + * {@link #lookup(String, int, String)} replaced only if set in the config + * <li>%u - local user name + * </ul> + * <p> + * %i is not handled; Java has no concept of a "user ID". %T is always replaced + * by NONE. + * </p> + * + * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man + * ssh-config</a> + */ +public class OpenSshConfigFile { + + /** + * "Host" name of the HostEntry for the default options before the first + * host block in a config file. + */ + private static final String DEFAULT_NAME = ""; //$NON-NLS-1$ + + /** The user's home directory, as key files may be relative to here. */ + private final File home; + + /** The .ssh/config file we read and monitor for updates. */ + private final File configFile; + + /** User name of the user on the host OS. */ + private final String localUserName; + + /** Modification time of {@link #configFile} when it was last loaded. */ + private Instant lastModified; + + /** + * Encapsulates entries read out of the configuration file, and a cache of + * fully resolved entries created from that. + */ + private static class State { + // Keyed by pattern; if a "Host" line has multiple patterns, we generate + // duplicate HostEntry objects + Map<String, HostEntry> entries = new LinkedHashMap<>(); + + // Keyed by user@hostname:port + Map<String, HostEntry> hosts = new HashMap<>(); + + @Override + @SuppressWarnings("nls") + public String toString() { + return "State [entries=" + entries + ", hosts=" + hosts + "]"; + } + } + + /** State read from the config file, plus the cache. */ + private State state; + + /** + * Creates a new {@link OpenSshConfigFile} that will read the config from + * file {@code config} use the given file {@code home} as "home" directory. + * + * @param home + * user's home directory for the purpose of ~ replacement + * @param config + * file to load. + * @param localUserName + * user name of the current user on the local host OS + */ + public OpenSshConfigFile(@NonNull File home, @NonNull File config, + @NonNull String localUserName) { + this.home = home; + this.configFile = config; + this.localUserName = localUserName; + state = new State(); + } + + /** + * Locate the configuration for a specific host request. + * + * @param hostName + * the name the user has supplied to the SSH tool. This may be a + * real host name, or it may just be a "Host" block in the + * configuration file. + * @param port + * the user supplied; <= 0 if none + * @param userName + * the user supplied, may be {@code null} or empty if none given + * @return r configuration for the requested name. + */ + @NonNull + public HostEntry lookup(@NonNull String hostName, int port, + String userName) { + final State cache = refresh(); + String cacheKey = toCacheKey(hostName, port, userName); + HostEntry h = cache.hosts.get(cacheKey); + if (h != null) { + return h; + } + HostEntry fullConfig = new HostEntry(); + // Initialize with default entries at the top of the file, before the + // first Host block. + fullConfig.merge(cache.entries.get(DEFAULT_NAME)); + for (Map.Entry<String, HostEntry> e : cache.entries.entrySet()) { + String pattern = e.getKey(); + if (isHostMatch(pattern, hostName)) { + fullConfig.merge(e.getValue()); + } + } + fullConfig.substitute(hostName, port, userName, localUserName, home); + cache.hosts.put(cacheKey, fullConfig); + return fullConfig; + } + + @NonNull + private String toCacheKey(@NonNull String hostName, int port, + String userName) { + String key = hostName; + if (port > 0) { + key = key + ':' + Integer.toString(port); + } + if (userName != null && !userName.isEmpty()) { + key = userName + '@' + key; + } + return key; + } + + private synchronized State refresh() { + final Instant mtime = FS.DETECTED.lastModifiedInstant(configFile); + if (!mtime.equals(lastModified)) { + State newState = new State(); + try (BufferedReader br = Files + .newBufferedReader(configFile.toPath(), UTF_8)) { + newState.entries = parse(br); + } catch (IOException | RuntimeException none) { + // Ignore -- we'll set and return an empty state + } + lastModified = mtime; + state = newState; + } + return state; + } + + private Map<String, HostEntry> parse(BufferedReader reader) + throws IOException { + final Map<String, HostEntry> entries = new LinkedHashMap<>(); + final List<HostEntry> current = new ArrayList<>(4); + String line; + + // The man page doesn't say so, but the openssh parser (readconf.c) + // starts out in active mode and thus always applies any lines that + // occur before the first host block. We gather those options in a + // HostEntry for DEFAULT_NAME. + HostEntry defaults = new HostEntry(); + current.add(defaults); + entries.put(DEFAULT_NAME, defaults); + + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) { //$NON-NLS-1$ + continue; + } + String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$ + // Although the ssh-config man page doesn't say so, the openssh + // parser does allow quoted keywords. + String keyword = dequote(parts[0].trim()); + // man 5 ssh-config says lines had the format "keyword arguments", + // with no indication that arguments were optional. However, let's + // not crap out on missing arguments. See bug 444319. + String argValue = parts.length > 1 ? parts[1].trim() : ""; //$NON-NLS-1$ + + if (StringUtils.equalsIgnoreCase(SshConstants.HOST, keyword)) { + current.clear(); + for (String name : parseList(argValue)) { + if (name == null || name.isEmpty()) { + // null should not occur, but better be safe than sorry. + continue; + } + HostEntry c = entries.get(name); + if (c == null) { + c = new HostEntry(); + entries.put(name, c); + } + current.add(c); + } + continue; + } + + if (current.isEmpty()) { + // We received an option outside of a Host block. We + // don't know who this should match against, so skip. + continue; + } + + if (HostEntry.isListKey(keyword)) { + List<String> args = validate(keyword, parseList(argValue)); + for (HostEntry entry : current) { + entry.setValue(keyword, args); + } + } else if (!argValue.isEmpty()) { + argValue = validate(keyword, dequote(argValue)); + for (HostEntry entry : current) { + entry.setValue(keyword, argValue); + } + } + } + + return entries; + } + + /** + * Splits the argument into a list of whitespace-separated elements. + * Elements containing whitespace must be quoted and will be de-quoted. + * + * @param argument + * argument part of the configuration line as read from the + * config file + * @return a {@link List} of elements, possibly empty and possibly + * containing empty elements, but not containing {@code null} + */ + private List<String> parseList(String argument) { + List<String> result = new ArrayList<>(4); + int start = 0; + int length = argument.length(); + while (start < length) { + // Skip whitespace + if (Character.isSpaceChar(argument.charAt(start))) { + start++; + continue; + } + if (argument.charAt(start) == '"') { + int stop = argument.indexOf('"', ++start); + if (stop < start) { + // No closing double quote: skip + break; + } + result.add(argument.substring(start, stop)); + start = stop + 1; + } else { + int stop = start + 1; + while (stop < length + && !Character.isSpaceChar(argument.charAt(stop))) { + stop++; + } + result.add(argument.substring(start, stop)); + start = stop + 1; + } + } + return result; + } + + /** + * Hook to perform validation on a single value, or to sanitize it. If this + * throws an (unchecked) exception, parsing of the file is abandoned. + * + * @param key + * of the entry + * @param value + * as read from the config file + * @return the validated and possibly sanitized value + */ + protected String validate(String key, String value) { + if (String.CASE_INSENSITIVE_ORDER.compare(key, + SshConstants.PREFERRED_AUTHENTICATIONS) == 0) { + return stripWhitespace(value); + } + return value; + } + + /** + * Hook to perform validation on values, or to sanitize them. If this throws + * an (unchecked) exception, parsing of the file is abandoned. + * + * @param key + * of the entry + * @param value + * list of arguments as read from the config file + * @return a {@link List} of values, possibly empty and possibly containing + * empty elements, but not containing {@code null} + */ + protected List<String> validate(String key, List<String> value) { + return value; + } + + private static boolean isHostMatch(String pattern, String name) { + if (pattern.startsWith("!")) { //$NON-NLS-1$ + return !patternMatchesHost(pattern.substring(1), name); + } else { + return patternMatchesHost(pattern, name); + } + } + + private static boolean patternMatchesHost(String pattern, String name) { + if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) { + final FileNameMatcher fn; + try { + fn = new FileNameMatcher(pattern, null); + } catch (InvalidPatternException e) { + return false; + } + fn.append(name); + return fn.isMatch(); + } else { + // Not a pattern but a full host name + return pattern.equals(name); + } + } + + private static String dequote(String value) { + if (value.startsWith("\"") && value.endsWith("\"") //$NON-NLS-1$ //$NON-NLS-2$ + && value.length() > 1) + return value.substring(1, value.length() - 1); + return value; + } + + private static String stripWhitespace(String value) { + final StringBuilder b = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + if (!Character.isSpaceChar(value.charAt(i))) + b.append(value.charAt(i)); + } + return b.toString(); + } + + private static File toFile(String path, File home) { + if (path.startsWith("~/") || path.startsWith("~" + File.separator)) { //$NON-NLS-1$ //$NON-NLS-2$ + return new File(home, path.substring(2)); + } + File ret = new File(path); + if (ret.isAbsolute()) { + return ret; + } + return new File(home, path); + } + + /** + * Converts a positive value into an {@code int}. + * + * @param value + * to convert + * @return the value, or -1 if it wasn't a positive integral value + */ + public static int positive(String value) { + if (value != null) { + try { + return Integer.parseUnsignedInt(value); + } catch (NumberFormatException e) { + // Ignore + } + } + return -1; + } + + /** + * Converts a ssh config flag value (yes/true/on - no/false/off) into an + * {@code boolean}. + * + * @param value + * to convert + * @return {@code true} if {@code value}Â is "yes", "on", or "true"; + * {@code false} otherwise + */ + public static boolean flag(String value) { + if (value == null) { + return false; + } + return SshConstants.YES.equals(value) || SshConstants.ON.equals(value) + || SshConstants.TRUE.equals(value); + } + + /** + * Retrieves the local user name as given in the constructor. + * + * @return the user name + */ + public String getLocalUserName() { + return localUserName; + } + + /** + * A host entry from the ssh config file. Any merging of global values and + * of several matching host entries, %-substitutions, and ~ replacement have + * all been done. + */ + public static class HostEntry { + + /** + * Keys that can be specified multiple times, building up a list. (I.e., + * those are the keys that do not follow the general rule of "first + * occurrence wins".) + */ + private static final Set<String> MULTI_KEYS = new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + + static { + MULTI_KEYS.add(SshConstants.CERTIFICATE_FILE); + MULTI_KEYS.add(SshConstants.IDENTITY_FILE); + MULTI_KEYS.add(SshConstants.LOCAL_FORWARD); + MULTI_KEYS.add(SshConstants.REMOTE_FORWARD); + MULTI_KEYS.add(SshConstants.SEND_ENV); + } + + /** + * Keys that take a whitespace-separated list of elements as argument. + * Because the dequote-handling is different, we must handle those in + * the parser. There are a few other keys that take comma-separated + * lists as arguments, but for the parser those are single arguments + * that must be quoted if they contain whitespace, and taking them apart + * is the responsibility of the user of those keys. + */ + private static final Set<String> LIST_KEYS = new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + + static { + LIST_KEYS.add(SshConstants.CANONICAL_DOMAINS); + LIST_KEYS.add(SshConstants.GLOBAL_KNOWN_HOSTS_FILE); + LIST_KEYS.add(SshConstants.SEND_ENV); + LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE); + } + + private Map<String, String> options; + + private Map<String, List<String>> multiOptions; + + private Map<String, List<String>> listOptions; + + /** + * Retrieves the value of a single-valued key, or the first is the key + * has multiple values. Keys are case-insensitive, so + * {@code getValue("HostName") == getValue("HOSTNAME")}. + * + * @param key + * to get the value of + * @return the value, or {@code null} if none + */ + public String getValue(String key) { + String result = options != null ? options.get(key) : null; + if (result == null) { + // Let's be lenient and return at least the first value from + // a list-valued or multi-valued key. + List<String> values = listOptions != null ? listOptions.get(key) + : null; + if (values == null) { + values = multiOptions != null ? multiOptions.get(key) + : null; + } + if (values != null && !values.isEmpty()) { + result = values.get(0); + } + } + return result; + } + + /** + * Retrieves the values of a multi or list-valued key. Keys are + * case-insensitive, so + * {@code getValue("HostName") == getValue("HOSTNAME")}. + * + * @param key + * to get the values of + * @return a possibly empty list of values + */ + public List<String> getValues(String key) { + List<String> values = listOptions != null ? listOptions.get(key) + : null; + if (values == null) { + values = multiOptions != null ? multiOptions.get(key) : null; + } + if (values == null || values.isEmpty()) { + return new ArrayList<>(); + } + return new ArrayList<>(values); + } + + /** + * Sets the value of a single-valued key if it not set yet, or adds a + * value to a multi-valued key. If the value is {@code null}, the key is + * removed altogether, whether it is single-, list-, or multi-valued. + * + * @param key + * to modify + * @param value + * to set or add + */ + public void setValue(String key, String value) { + if (value == null) { + if (multiOptions != null) { + multiOptions.remove(key); + } + if (listOptions != null) { + listOptions.remove(key); + } + if (options != null) { + options.remove(key); + } + return; + } + if (MULTI_KEYS.contains(key)) { + if (multiOptions == null) { + multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + List<String> values = multiOptions.get(key); + if (values == null) { + values = new ArrayList<>(4); + multiOptions.put(key, values); + } + values.add(value); + } else { + if (options == null) { + options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + if (!options.containsKey(key)) { + options.put(key, value); + } + } + } + + /** + * Sets the values of a multi- or list-valued key. + * + * @param key + * to set + * @param values + * a non-empty list of values + */ + public void setValue(String key, List<String> values) { + if (values.isEmpty()) { + return; + } + // Check multi-valued keys first; because of the replacement + // strategy, they must take precedence over list-valued keys + // which always follow the "first occurrence wins" strategy. + // + // Note that SendEnv is a multi-valued list-valued key. (It's + // rather immaterial for JGit, though.) + if (MULTI_KEYS.contains(key)) { + if (multiOptions == null) { + multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + List<String> items = multiOptions.get(key); + if (items == null) { + items = new ArrayList<>(values); + multiOptions.put(key, items); + } else { + items.addAll(values); + } + } else { + if (listOptions == null) { + listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + if (!listOptions.containsKey(key)) { + listOptions.put(key, values); + } + } + } + + /** + * Does the key take a whitespace-separated list of values? + * + * @param key + * to check + * @return {@code true} if the key is a list-valued key. + */ + public static boolean isListKey(String key) { + return LIST_KEYS.contains(key.toUpperCase(Locale.ROOT)); + } + + void merge(HostEntry entry) { + if (entry == null) { + // Can occur if we could not read the config file + return; + } + if (entry.options != null) { + if (options == null) { + options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + for (Map.Entry<String, String> item : entry.options + .entrySet()) { + if (!options.containsKey(item.getKey())) { + options.put(item.getKey(), item.getValue()); + } + } + } + if (entry.listOptions != null) { + if (listOptions == null) { + listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + for (Map.Entry<String, List<String>> item : entry.listOptions + .entrySet()) { + if (!listOptions.containsKey(item.getKey())) { + listOptions.put(item.getKey(), item.getValue()); + } + } + + } + if (entry.multiOptions != null) { + if (multiOptions == null) { + multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + for (Map.Entry<String, List<String>> item : entry.multiOptions + .entrySet()) { + List<String> values = multiOptions.get(item.getKey()); + if (values == null) { + values = new ArrayList<>(item.getValue()); + multiOptions.put(item.getKey(), values); + } else { + values.addAll(item.getValue()); + } + } + } + } + + private List<String> substitute(List<String> values, String allowed, + Replacer r) { + List<String> result = new ArrayList<>(values.size()); + for (String value : values) { + result.add(r.substitute(value, allowed)); + } + return result; + } + + private List<String> replaceTilde(List<String> values, File home) { + List<String> result = new ArrayList<>(values.size()); + for (String value : values) { + result.add(toFile(value, home).getPath()); + } + return result; + } + + void substitute(String originalHostName, int port, String userName, + String localUserName, File home) { + int p = port >= 0 ? port : positive(getValue(SshConstants.PORT)); + if (p < 0) { + p = SshConstants.SSH_DEFAULT_PORT; + } + String u = userName != null && !userName.isEmpty() ? userName + : getValue(SshConstants.USER); + if (u == null || u.isEmpty()) { + u = localUserName; + } + Replacer r = new Replacer(originalHostName, p, u, localUserName, + home); + if (options != null) { + // HOSTNAME first + String hostName = options.get(SshConstants.HOST_NAME); + if (hostName == null || hostName.isEmpty()) { + options.put(SshConstants.HOST_NAME, originalHostName); + } else { + hostName = r.substitute(hostName, "h"); //$NON-NLS-1$ + options.put(SshConstants.HOST_NAME, hostName); + r.update('h', hostName); + } + } + if (multiOptions != null) { + List<String> values = multiOptions + .get(SshConstants.IDENTITY_FILE); + if (values != null) { + values = substitute(values, "dhlru", r); //$NON-NLS-1$ + values = replaceTilde(values, home); + multiOptions.put(SshConstants.IDENTITY_FILE, values); + } + values = multiOptions.get(SshConstants.CERTIFICATE_FILE); + if (values != null) { + values = substitute(values, "dhlru", r); //$NON-NLS-1$ + values = replaceTilde(values, home); + multiOptions.put(SshConstants.CERTIFICATE_FILE, values); + } + } + if (listOptions != null) { + List<String> values = listOptions + .get(SshConstants.USER_KNOWN_HOSTS_FILE); + if (values != null) { + values = replaceTilde(values, home); + listOptions.put(SshConstants.USER_KNOWN_HOSTS_FILE, values); + } + } + if (options != null) { + // HOSTNAME already done above + String value = options.get(SshConstants.IDENTITY_AGENT); + if (value != null) { + value = r.substitute(value, "dhlru"); //$NON-NLS-1$ + value = toFile(value, home).getPath(); + options.put(SshConstants.IDENTITY_AGENT, value); + } + value = options.get(SshConstants.CONTROL_PATH); + if (value != null) { + value = r.substitute(value, "ChLlnpru"); //$NON-NLS-1$ + value = toFile(value, home).getPath(); + options.put(SshConstants.CONTROL_PATH, value); + } + value = options.get(SshConstants.LOCAL_COMMAND); + if (value != null) { + value = r.substitute(value, "CdhlnprTu"); //$NON-NLS-1$ + options.put(SshConstants.LOCAL_COMMAND, value); + } + value = options.get(SshConstants.REMOTE_COMMAND); + if (value != null) { + value = r.substitute(value, "Cdhlnpru"); //$NON-NLS-1$ + options.put(SshConstants.REMOTE_COMMAND, value); + } + value = options.get(SshConstants.PROXY_COMMAND); + if (value != null) { + value = r.substitute(value, "hpr"); //$NON-NLS-1$ + options.put(SshConstants.PROXY_COMMAND, value); + } + } + // Match is not implemented and would need to be done elsewhere + // anyway. + } + + /** + * Retrieves an unmodifiable map of all single-valued options, with + * case-insensitive lookup by keys. + * + * @return all single-valued options + */ + @NonNull + public Map<String, String> getOptions() { + if (options == null) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(options); + } + + /** + * Retrieves an unmodifiable map of all multi-valued options, with + * case-insensitive lookup by keys. + * + * @return all multi-valued options + */ + @NonNull + public Map<String, List<String>> getMultiValuedOptions() { + if (listOptions == null && multiOptions == null) { + return Collections.emptyMap(); + } + Map<String, List<String>> allValues = new TreeMap<>( + String.CASE_INSENSITIVE_ORDER); + if (multiOptions != null) { + allValues.putAll(multiOptions); + } + if (listOptions != null) { + allValues.putAll(listOptions); + } + return Collections.unmodifiableMap(allValues); + } + + @Override + @SuppressWarnings("nls") + public String toString() { + return "HostEntry [options=" + options + ", multiOptions=" + + multiOptions + ", listOptions=" + listOptions + "]"; + } + } + + private static class Replacer { + private final Map<Character, String> replacements = new HashMap<>(); + + public Replacer(String host, int port, String user, + String localUserName, File home) { + replacements.put(Character.valueOf('%'), "%"); //$NON-NLS-1$ + replacements.put(Character.valueOf('d'), home.getPath()); + replacements.put(Character.valueOf('h'), host); + String localhost = SystemReader.getInstance().getHostname(); + replacements.put(Character.valueOf('l'), localhost); + int period = localhost.indexOf('.'); + if (period > 0) { + localhost = localhost.substring(0, period); + } + replacements.put(Character.valueOf('L'), localhost); + replacements.put(Character.valueOf('n'), host); + replacements.put(Character.valueOf('p'), Integer.toString(port)); + replacements.put(Character.valueOf('r'), user == null ? "" : user); //$NON-NLS-1$ + replacements.put(Character.valueOf('u'), localUserName); + replacements.put(Character.valueOf('C'), + substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$ + replacements.put(Character.valueOf('T'), "NONE"); //$NON-NLS-1$ + } + + public void update(char key, String value) { + replacements.put(Character.valueOf(key), value); + if ("lhpr".indexOf(key) >= 0) { //$NON-NLS-1$ + replacements.put(Character.valueOf('C'), + substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + public String substitute(String input, String allowed) { + if (input == null || input.length() <= 1 + || input.indexOf('%') < 0) { + return input; + } + StringBuilder builder = new StringBuilder(); + int start = 0; + int length = input.length(); + while (start < length) { + int percent = input.indexOf('%', start); + if (percent < 0 || percent + 1 >= length) { + builder.append(input.substring(start)); + break; + } + String replacement = null; + char ch = input.charAt(percent + 1); + if (ch == '%' || allowed.indexOf(ch) >= 0) { + replacement = replacements.get(Character.valueOf(ch)); + } + if (replacement == null) { + builder.append(input.substring(start, percent + 2)); + } else { + builder.append(input.substring(start, percent)) + .append(replacement); + } + start = percent + 2; + } + return builder.toString(); + } + } + + /** {@inheritDoc} */ + @Override + @SuppressWarnings("nls") + public String toString() { + return "OpenSshConfig [home=" + home + ", configFile=" + configFile + + ", lastModified=" + lastModified + ", state=" + state + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncObjectLoaderQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncObjectLoaderQueue.java index b4ea0e907f..659c67c5ab 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncObjectLoaderQueue.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncObjectLoaderQueue.java @@ -78,7 +78,7 @@ public interface AsyncObjectLoaderQueue<T extends ObjectId> extends * @throws java.io.IOException * the object store cannot be accessed. */ - public boolean next() throws MissingObjectException, IOException; + boolean next() throws MissingObjectException, IOException; /** * Get the current object, null if the implementation lost track. @@ -87,14 +87,14 @@ public interface AsyncObjectLoaderQueue<T extends ObjectId> extends * Implementations may for performance reasons discard the caller's * ObjectId and provider their own through {@link #getObjectId()}. */ - public T getCurrent(); + T getCurrent(); /** * Get the ObjectId of the current object. Never null. * * @return the ObjectId of the current object. Never null. */ - public ObjectId getObjectId(); + ObjectId getObjectId(); /** * Obtain a loader to read the object. @@ -115,5 +115,5 @@ public interface AsyncObjectLoaderQueue<T extends ObjectId> extends * @throws java.io.IOException * the object store cannot be accessed. */ - public ObjectLoader open() throws IOException; + ObjectLoader open() throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncObjectSizeQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncObjectSizeQueue.java index 03efcd295e..6b8642f119 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncObjectSizeQueue.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncObjectSizeQueue.java @@ -73,7 +73,7 @@ public interface AsyncObjectSizeQueue<T extends ObjectId> extends * @throws java.io.IOException * the object store cannot be accessed. */ - public boolean next() throws MissingObjectException, IOException; + boolean next() throws MissingObjectException, IOException; /** * <p>getCurrent.</p> @@ -82,19 +82,19 @@ public interface AsyncObjectSizeQueue<T extends ObjectId> extends * Implementations may for performance reasons discard the caller's * ObjectId and provider their own through {@link #getObjectId()}. */ - public T getCurrent(); + T getCurrent(); /** * Get the ObjectId of the current object. Never null. * * @return the ObjectId of the current object. Never null. */ - public ObjectId getObjectId(); + ObjectId getObjectId(); /** * Get the size of the current object. * * @return the size of the current object. */ - public long getSize(); + long getSize(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncOperation.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncOperation.java index 00555b0907..27b9c2038a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncOperation.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AsyncOperation.java @@ -68,10 +68,10 @@ public interface AsyncOperation { * @return false if the task could not be cancelled, typically because it * has already completed normally; true otherwise */ - public boolean cancel(boolean mayInterruptIfRunning); + boolean cancel(boolean mayInterruptIfRunning); /** * Release resources used by the operation, including cancellation. */ - public void release(); + void release(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobObjectChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobObjectChecker.java index 3fa3168327..7878351ce8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobObjectChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobObjectChecker.java @@ -55,7 +55,7 @@ import org.eclipse.jgit.errors.CorruptObjectException; */ public interface BlobObjectChecker { /** No-op implementation of {@link BlobObjectChecker}. */ - public static final BlobObjectChecker NULL_CHECKER = + BlobObjectChecker NULL_CHECKER = new BlobObjectChecker() { @Override public void update(byte[] in, int p, int len) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CheckoutEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CheckoutEntry.java index cfc0cc86d1..84ff0a8936 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CheckoutEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CheckoutEntry.java @@ -12,13 +12,13 @@ public interface CheckoutEntry { * * @return the name of the branch before checkout */ - public abstract String getFromBranch(); + String getFromBranch(); /** * Get the name of the branch after checkout * * @return the name of the branch after checkout */ - public abstract String getToBranch(); + String getToBranch(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java index b666f21d0b..4726975d07 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -62,7 +62,6 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.events.ConfigChangedEvent; import org.eclipse.jgit.events.ConfigChangedListener; @@ -868,7 +867,7 @@ public class Config { boolean lastWasMatch = false; for (ConfigLine e : srcState.entryList) { - if (e.match(section, subsection)) { + if (e.includedFrom == null && e.match(section, subsection)) { // Skip this record, it's for the section we are removing. lastWasMatch = true; continue; @@ -923,7 +922,7 @@ public class Config { // while (entryIndex < entries.size() && valueIndex < values.size()) { final ConfigLine e = entries.get(entryIndex); - if (e.match(section, subsection, name)) { + if (e.includedFrom == null && e.match(section, subsection, name)) { entries.set(entryIndex, e.forValue(values.get(valueIndex++))); insertPosition = entryIndex + 1; } @@ -935,7 +934,8 @@ public class Config { if (valueIndex == values.size() && entryIndex < entries.size()) { while (entryIndex < entries.size()) { final ConfigLine e = entries.get(entryIndex++); - if (e.match(section, subsection, name)) + if (e.includedFrom == null + && e.match(section, subsection, name)) entries.remove(--entryIndex); } } @@ -948,7 +948,8 @@ public class Config { // is already a section available that matches. Insert // after the last key of that section. // - insertPosition = findSectionEnd(entries, section, subsection); + insertPosition = findSectionEnd(entries, section, subsection, + true); } if (insertPosition < 0) { // We didn't find any matching section header for this key, @@ -985,9 +986,14 @@ public class Config { } private static int findSectionEnd(final List<ConfigLine> entries, - final String section, final String subsection) { + final String section, final String subsection, + boolean skipIncludedLines) { for (int i = 0; i < entries.size(); i++) { ConfigLine e = entries.get(i); + if (e.includedFrom != null && skipIncludedLines) { + continue; + } + if (e.match(section, subsection, null)) { i++; while (i < entries.size()) { @@ -1011,6 +1017,8 @@ public class Config { public String toText() { final StringBuilder out = new StringBuilder(); for (ConfigLine e : state.get().entryList) { + if (e.includedFrom != null) + continue; if (e.prefix != null) out.append(e.prefix); if (e.section != null && e.name == null) { @@ -1060,11 +1068,11 @@ public class Config { * made to {@code this}. */ public void fromText(String text) throws ConfigInvalidException { - state.set(newState(fromTextRecurse(text, 1))); + state.set(newState(fromTextRecurse(text, 1, null))); } - private List<ConfigLine> fromTextRecurse(String text, int depth) - throws ConfigInvalidException { + private List<ConfigLine> fromTextRecurse(String text, int depth, + String includedFrom) throws ConfigInvalidException { if (depth > MAX_DEPTH) { throw new ConfigInvalidException( JGitText.get().tooManyIncludeRecursions); @@ -1073,6 +1081,7 @@ public class Config { final StringReader in = new StringReader(text); ConfigLine last = null; ConfigLine e = new ConfigLine(); + e.includedFrom = includedFrom; for (;;) { int input = in.read(); if (-1 == input) { @@ -1088,7 +1097,7 @@ public class Config { if (e.section != null) last = e; e = new ConfigLine(); - + e.includedFrom = includedFrom; } else if (e.suffix != null) { // Everything up until the end-of-line is in the suffix. e.suffix += c; @@ -1148,7 +1157,6 @@ public class Config { * if something went wrong while reading the config * @since 4.10 */ - @Nullable protected byte[] readIncludedConfig(String relPath) throws ConfigInvalidException { return null; @@ -1173,7 +1181,7 @@ public class Config { decoded = RawParseUtils.decode(bytes); } try { - newEntries.addAll(fromTextRecurse(decoded, depth + 1)); + newEntries.addAll(fromTextRecurse(decoded, depth + 1, line.value)); } catch (ConfigInvalidException e) { throw new ConfigInvalidException(MessageFormat .format(JGitText.get().cannotReadFile, line.value), e); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 82ccd7b034..5ae9d41db2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -119,6 +119,36 @@ public final class ConfigConstants { */ public static final String CONFIG_FILTER_SECTION = "filter"; + /** + * The "gpg" section + * @since 5.2 + */ + public static final String CONFIG_GPG_SECTION = "gpg"; + + /** + * The "format" key + * @since 5.2 + */ + public static final String CONFIG_KEY_FORMAT = "format"; + + /** + * The "signingKey" key + * @since 5.2 + */ + public static final String CONFIG_KEY_SIGNINGKEY = "signingKey"; + + /** + * The "commit" section + * @since 5.2 + */ + public static final String CONFIG_COMMIT_SECTION = "commit"; + + /** + * The "gpgSign" key + * @since 5.2 + */ + public static final String CONFIG_KEY_GPGSIGN = "gpgSign"; + /** The "algorithm" key */ public static final String CONFIG_KEY_ALGORITHM = "algorithm"; @@ -434,6 +464,20 @@ public final class ConfigConstants { public static final String CONFIG_SECTION_LFS = "lfs"; /** + * The "i18n" section + * + * @since 5.2 + */ + public static final String CONFIG_SECTION_I18N = "i18n"; + + /** + * The "logOutputEncoding" key + * + * @since 5.2 + */ + public static final String CONFIG_KEY_LOG_OUTPUT_ENCODING = "logOutputEncoding"; + + /** * The "filesystem" section * @since 5.1.9 */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigLine.java index 937ba925c5..e623a8cebc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigLine.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigLine.java @@ -73,6 +73,9 @@ class ConfigLine { /** The text content after entry. */ String suffix; + /** The source from which this line was included from. */ + String includedFrom; + ConfigLine forValue(String newValue) { final ConfigLine e = new ConfigLine(); e.prefix = prefix; @@ -81,6 +84,7 @@ class ConfigLine { e.name = name; e.value = newValue; e.suffix = suffix; + e.includedFrom = includedFrom; return e; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java index 001ae93a0c..fb239399ed 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java @@ -301,7 +301,8 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { /** {@inheritDoc} */ @Override - public @NonNull List<RefSpec> getRefSpecs(Config config, String section, + @NonNull + public List<RefSpec> getRefSpecs(Config config, String section, String subsection, String name) { String[] values = config.getStringList(section, subsection, name); List<RefSpec> result = new ArrayList<>(values.length); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java new file mode 100644 index 0000000000..a09bc00786 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2018, Salesforce. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.lib; + +/** + * Typed access to GPG related configuration options. + * + * @since 5.2 + */ +public class GpgConfig { + + /** + * Config values for gpg.format. + */ + public enum GpgFormat implements Config.ConfigEnum { + + /** Value for openpgp */ + OPENPGP("openpgp"), //$NON-NLS-1$ + /** Value for x509 */ + X509("x509"); //$NON-NLS-1$ + + private final String configValue; + + private GpgFormat(String configValue) { + this.configValue = configValue; + } + + @Override + public boolean matchConfigValue(String s) { + return configValue.equals(s); + } + + @Override + public String toConfigValue() { + return configValue; + } + } + + private final Config config; + + /** + * Create a new GPG config, which will read configuration from config. + * + * @param config + * the config to read from + */ + public GpgConfig(Config config) { + this.config = config; + } + + /** + * Retrieves the config value of gpg.format. + * + * @return the {@link org.eclipse.jgit.lib.GpgConfig.GpgFormat} + */ + public GpgFormat getKeyFormat() { + return config.getEnum(GpgFormat.values(), + ConfigConstants.CONFIG_GPG_SECTION, null, + ConfigConstants.CONFIG_KEY_FORMAT, GpgFormat.OPENPGP); + } + + /** + * Retrieves the config value of user.signingKey. + * + * @return the value of user.signingKey (may be <code>null</code>) + */ + public String getSigningKey() { + return config.getString(ConfigConstants.CONFIG_USER_SECTION, null, + ConfigConstants.CONFIG_KEY_SIGNINGKEY); + } + + /** + * Retrieves the config value of commit.gpgSign. + * + * @return the value of commit.gpgSign (defaults to <code>false</code>) + */ + public boolean isSignCommits() { + return config.getBoolean(ConfigConstants.CONFIG_COMMIT_SECTION, + ConfigConstants.CONFIG_KEY_GPGSIGN, false); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java index 94b9ddc188..f37c310752 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java @@ -47,7 +47,11 @@ package org.eclipse.jgit.lib; +import java.io.File; import java.io.IOException; +import java.nio.file.DirectoryIteratorException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; @@ -261,6 +265,8 @@ public class IndexDiff { private Set<String> missing = new HashSet<>(); + private Set<String> missingSubmodules = new HashSet<>(); + private Set<String> modified = new HashSet<>(); private Set<String> untracked = new HashSet<>(); @@ -501,9 +507,15 @@ public class IndexDiff { if (dirCacheIterator != null) { if (workingTreeIterator == null) { // in index, not in workdir => missing - if (!isEntryGitLink(dirCacheIterator) - || ignoreSubmoduleMode != IgnoreSubmoduleMode.ALL) - missing.add(treeWalk.getPathString()); + boolean isGitLink = isEntryGitLink(dirCacheIterator); + if (!isGitLink + || ignoreSubmoduleMode != IgnoreSubmoduleMode.ALL) { + String path = treeWalk.getPathString(); + missing.add(path); + if (isGitLink) { + missingSubmodules.add(path); + } + } } else { if (workingTreeIterator.isModified( dirCacheIterator.getDirCacheEntry(), true, @@ -543,8 +555,8 @@ public class IndexDiff { smw.getPath()), e); } try (Repository subRepo = smw.getRepository()) { + String subRepoPath = smw.getPath(); if (subRepo != null) { - String subRepoPath = smw.getPath(); ObjectId subHead = subRepo.resolve("HEAD"); //$NON-NLS-1$ if (subHead != null && !subHead.equals(smw.getObjectId())) { @@ -573,6 +585,21 @@ public class IndexDiff { recordFileMode(subRepoPath, FileMode.GITLINK); } } + } else if (missingSubmodules.remove(subRepoPath)) { + // If the directory is there and empty but the submodule + // repository in .git/modules doesn't exist yet it isn't + // "missing". + File gitDir = new File( + new File(repository.getDirectory(), + Constants.MODULES), + subRepoPath); + if (!gitDir.isDirectory()) { + File dir = SubmoduleWalk.getSubmoduleDirectory( + repository, subRepoPath); + if (dir.isDirectory() && !hasFiles(dir)) { + missing.remove(subRepoPath); + } + } } } } @@ -592,6 +619,15 @@ public class IndexDiff { return true; } + private boolean hasFiles(File directory) { + try (DirectoryStream<java.nio.file.Path> dir = Files + .newDirectoryStream(directory.toPath())) { + return dir.iterator().hasNext(); + } catch (DirectoryIteratorException | IOException e) { + return false; + } + } + private void recordFileMode(String path, FileMode mode) { Set<String> values = fileModes.get(mode); if (path != null) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java index d37fb21c93..127f019c46 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java @@ -109,7 +109,8 @@ import org.eclipse.jgit.util.StringUtils; * the caller can provide both of these validations on its own. * <p> * Instances of this class are not thread safe, but they may be reused to - * perform multiple object validations. + * perform multiple object validations, calling {@link #reset()} between them to + * clear the internal state (e.g. {@link #getGitsubmodules()}) */ public class ObjectChecker { /** Header "tree " */ @@ -173,6 +174,13 @@ public class ObjectChecker { /***/ BAD_TIMEZONE, /***/ MISSING_EMAIL, /***/ MISSING_SPACE_BEFORE_DATE, + /** @since 5.2 */ GITMODULES_BLOB, + /** @since 5.2 */ GITMODULES_LARGE, + /** @since 5.2 */ GITMODULES_NAME, + /** @since 5.2 */ GITMODULES_PARSE, + /** @since 5.2 */ GITMODULES_PATH, + /** @since 5.2 */ GITMODULES_SYMLINK, + /** @since 5.2 */ GITMODULES_URL, /***/ UNKNOWN_TYPE, // These are unique to JGit. @@ -1251,4 +1259,19 @@ public class ObjectChecker { public List<GitmoduleEntry> getGitsubmodules() { return gitsubmodules; } + + /** + * Reset the invocation-specific state from this instance. Specifically this + * clears the list of .gitmodules files encountered (see + * {@link #getGitsubmodules()}) + * + * Configurations like errors to filter, skip lists or the specified O.S. + * (set via {@link #setSafeForMacOS(boolean)} or + * {@link #setSafeForWindows(boolean)}) are NOT cleared. + * + * @since 5.2 + */ + public void reset() { + gitsubmodules.clear(); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ProgressMonitor.java index d81ee45c9e..9d8d71a0be 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ProgressMonitor.java @@ -49,7 +49,7 @@ package org.eclipse.jgit.lib; */ public interface ProgressMonitor { /** Constant indicating the total work units cannot be predicted. */ - public static final int UNKNOWN = 0; + int UNKNOWN = 0; /** * Advise the monitor of the total number of subtasks. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java index b000558944..faabbf892f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java @@ -61,7 +61,7 @@ import org.eclipse.jgit.annotations.Nullable; */ public interface Ref { /** Location where a {@link Ref} is stored. */ - public static enum Storage { + enum Storage { /** * The ref does not exist yet, updating it may create it. * <p> @@ -131,7 +131,7 @@ public interface Ref { * @return name of this ref. */ @NonNull - public String getName(); + String getName(); /** * Test if this reference is a symbolic reference. @@ -144,7 +144,7 @@ public interface Ref { * @return true if this is a symbolic reference; false if this reference * contains its own ObjectId. */ - public abstract boolean isSymbolic(); + boolean isSymbolic(); /** * Traverse target references until {@link #isSymbolic()} is false. @@ -163,7 +163,7 @@ public interface Ref { * @return the reference that actually stores the ObjectId value. */ @NonNull - public abstract Ref getLeaf(); + Ref getLeaf(); /** * Get the reference this reference points to, or {@code this}. @@ -178,7 +178,7 @@ public interface Ref { * @return the target reference, or {@code this}. */ @NonNull - public abstract Ref getTarget(); + Ref getTarget(); /** * Cached value of this ref. @@ -188,7 +188,7 @@ public interface Ref { * symbolic ref pointing to an unborn branch. */ @Nullable - public abstract ObjectId getObjectId(); + ObjectId getObjectId(); /** * Cached value of <code>ref^{}</code> (the ref peeled to commit). @@ -198,14 +198,14 @@ public interface Ref { * does not refer to an annotated tag. */ @Nullable - public abstract ObjectId getPeeledObjectId(); + ObjectId getPeeledObjectId(); /** * Whether the Ref represents a peeled tag. * * @return whether the Ref represents a peeled tag. */ - public abstract boolean isPeeled(); + boolean isPeeled(); /** * How was this ref obtained? @@ -216,5 +216,5 @@ public interface Ref { * @return type of ref. */ @NonNull - public abstract Storage getStorage(); + Storage getStorage(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java index 3170787dd9..68929b4220 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java @@ -415,6 +415,31 @@ public abstract class RefDatabase { } /** + * Returns refs whose names start with one of the given prefixes. + * <p> + * The default implementation uses {@link #getRefsByPrefix(String)}. + * Implementors of {@link RefDatabase} should override this method directly + * if a better implementation is possible. + * + * @param prefixes + * strings that names of refs should start with. + * @return immutable list of refs whose names start with one of + * {@code prefixes}. Refs can be unsorted and may contain duplicates + * if the prefixes overlap. + * @throws java.io.IOException + * the reference space cannot be accessed. + * @since 5.2 + */ + @NonNull + public List<Ref> getRefsByPrefix(String... prefixes) throws IOException { + List<Ref> result = new ArrayList<>(); + for (String prefix : prefixes) { + result.addAll(getRefsByPrefix(prefix)); + } + return Collections.unmodifiableList(result); + } + + /** * Check if any refs exist in the ref database. * <p> * This uses the same definition of refs as {@link #getRefs()}. In diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogEntry.java index 51f2ea0ab7..824bbc4201 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogEntry.java @@ -57,7 +57,7 @@ public interface ReflogEntry { * * @since 4.9 */ - public static final String PREFIX_CREATED = "created"; //$NON-NLS-1$ + String PREFIX_CREATED = "created"; //$NON-NLS-1$ /** * Prefix used in reflog messages when the ref was updated with a fast @@ -69,7 +69,7 @@ public interface ReflogEntry { * * @since 4.9 */ - public static final String PREFIX_FAST_FORWARD = "fast-forward"; //$NON-NLS-1$ + String PREFIX_FAST_FORWARD = "fast-forward"; //$NON-NLS-1$ /** * Prefix used in reflog messages when the ref was force updated. @@ -80,35 +80,35 @@ public interface ReflogEntry { * * @since 4.9 */ - public static final String PREFIX_FORCED_UPDATE = "forced-update"; //$NON-NLS-1$ + String PREFIX_FORCED_UPDATE = "forced-update"; //$NON-NLS-1$ /** * Get the commit id before the change * * @return the commit id before the change */ - public abstract ObjectId getOldId(); + ObjectId getOldId(); /** * Get the commit id after the change * * @return the commit id after the change */ - public abstract ObjectId getNewId(); + ObjectId getNewId(); /** * Get user performing the change * * @return user performing the change */ - public abstract PersonIdent getWho(); + PersonIdent getWho(); /** * Get textual description of the change * * @return textual description of the change */ - public abstract String getComment(); + String getComment(); /** * Parse checkout @@ -117,6 +117,6 @@ public interface ReflogEntry { * information about a branch switch, or null if the entry is not a * checkout */ - public abstract CheckoutEntry parseCheckout(); + CheckoutEntry parseCheckout(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java index f97b07e08c..4f104d2d7c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java @@ -59,7 +59,7 @@ public interface ReflogReader { * @return the latest reflog entry, or null if no log * @throws java.io.IOException */ - public abstract ReflogEntry getLastEntry() throws IOException; + ReflogEntry getLastEntry() throws IOException; /** * Get all reflog entries in reverse order @@ -67,7 +67,7 @@ public interface ReflogReader { * @return all reflog entries in reverse order * @throws java.io.IOException */ - public abstract List<ReflogEntry> getReverseEntries() throws IOException; + List<ReflogEntry> getReverseEntries() throws IOException; /** * Get specific entry in the reflog relative to the last entry which is @@ -77,7 +77,7 @@ public interface ReflogReader { * @return reflog entry or null if not found * @throws java.io.IOException */ - public abstract ReflogEntry getReverseEntry(int number) throws IOException; + ReflogEntry getReverseEntry(int number) throws IOException; /** * Get all reflog entries in reverse order @@ -87,7 +87,5 @@ public interface ReflogReader { * @return all reflog entries in reverse order * @throws java.io.IOException */ - public abstract List<ReflogEntry> getReverseEntries(int max) - throws IOException; - + List<ReflogEntry> getReverseEntries(int max) throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index 2a2699f906..77d268a3bd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -1981,7 +1981,6 @@ public abstract class Repository implements AutoCloseable { * empty * @throws IOException */ - @Nullable private byte[] readGitDirectoryFile(String filename) throws IOException { File file = new File(getDirectory(), filename); try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java index 036917e62a..479670873d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java @@ -45,6 +45,7 @@ package org.eclipse.jgit.merge; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; @@ -63,7 +64,7 @@ public class MergeFormatter { * that are LF-separated lines. * * @param out - * the outputstream where to write the textual presentation + * the output stream where to write the textual presentation * @param res * the merge result which should be presented * @param seqName @@ -72,13 +73,44 @@ public class MergeFormatter { * " or ">>>>>>> " conflict markers. The * names for the sequences are given in this list * @param charsetName - * the name of the characterSet used when writing conflict + * the name of the character set used when writing conflict * metadata * @throws java.io.IOException + * @deprecated Use + * {@link #formatMerge(OutputStream, MergeResult, List, Charset)} + * instead. */ + @Deprecated public void formatMerge(OutputStream out, MergeResult<RawText> res, List<String> seqName, String charsetName) throws IOException { - new MergeFormatterPass(out, res, seqName, charsetName).formatMerge(); + formatMerge(out, res, seqName, Charset.forName(charsetName)); + } + + /** + * Formats the results of a merge of {@link org.eclipse.jgit.diff.RawText} + * objects in a Git conformant way. This method also assumes that the + * {@link org.eclipse.jgit.diff.RawText} objects being merged are line + * oriented files which use LF as delimiter. This method will also use LF to + * separate chunks and conflict metadata, therefore it fits only to texts + * that are LF-separated lines. + * + * @param out + * the output stream where to write the textual presentation + * @param res + * the merge result which should be presented + * @param seqName + * When a conflict is reported each conflicting range will get a + * name. This name is following the "<<<<<<< + * " or ">>>>>>> " conflict markers. The + * names for the sequences are given in this list + * @param charset + * the character set used when writing conflict metadata + * @throws java.io.IOException + * @since 5.2 + */ + public void formatMerge(OutputStream out, MergeResult<RawText> res, + List<String> seqName, Charset charset) throws IOException { + new MergeFormatterPass(out, res, seqName, charset).formatMerge(); } /** @@ -100,17 +132,51 @@ public class MergeFormatter { * @param theirsName * the name ranges from theirs should get * @param charsetName - * the name of the characterSet used when writing conflict + * the name of the character set used when writing conflict * metadata * @throws java.io.IOException + * @deprecated use + * {@link #formatMerge(OutputStream, MergeResult, String, String, String, Charset)} + * instead. */ - @SuppressWarnings("unchecked") + @Deprecated public void formatMerge(OutputStream out, MergeResult res, String baseName, String oursName, String theirsName, String charsetName) throws IOException { + formatMerge(out, res, baseName, oursName, theirsName, + Charset.forName(charsetName)); + } + + /** + * Formats the results of a merge of exactly two + * {@link org.eclipse.jgit.diff.RawText} objects in a Git conformant way. + * This convenience method accepts the names for the three sequences (base + * and the two merged sequences) as explicit parameters and doesn't require + * the caller to specify a List + * + * @param out + * the {@link java.io.OutputStream} where to write the textual + * presentation + * @param res + * the merge result which should be presented + * @param baseName + * the name ranges from the base should get + * @param oursName + * the name ranges from ours should get + * @param theirsName + * the name ranges from theirs should get + * @param charset + * the character set used when writing conflict metadata + * @throws java.io.IOException + * @since 5.2 + */ + @SuppressWarnings("unchecked") + public void formatMerge(OutputStream out, MergeResult res, String baseName, + String oursName, String theirsName, Charset charset) + throws IOException { List<String> names = new ArrayList<>(3); names.add(baseName); names.add(oursName); names.add(theirsName); - formatMerge(out, res, names, charsetName); + formatMerge(out, res, names, charset); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java index 060f06884a..e1a8d3110e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java @@ -46,6 +46,7 @@ package org.eclipse.jgit.merge; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.Charset; import java.util.List; import org.eclipse.jgit.diff.RawText; @@ -59,19 +60,33 @@ class MergeFormatterPass { private final List<String> seqName; - private final String charsetName; + private final Charset charset; private final boolean threeWayMerge; private String lastConflictingName; // is set to non-null whenever we are in // a conflict - MergeFormatterPass(OutputStream out, MergeResult<RawText> res, List<String> seqName, - String charsetName) { + /** + * @param out + * the {@link java.io.OutputStream} where to write the textual + * presentation + * @param res + * the merge result which should be presented + * @param seqName + * When a conflict is reported each conflicting range will get a + * name. This name is following the "<<<<<<< + * " or ">>>>>>> " conflict markers. The + * names for the sequences are given in this list + * @param charset + * the character set used when writing conflict metadata + */ + MergeFormatterPass(OutputStream out, MergeResult<RawText> res, + List<String> seqName, Charset charset) { this.out = new EolAwareOutputStream(out); this.res = res; this.seqName = seqName; - this.charsetName = charsetName; + this.charset = charset; this.threeWayMerge = (res.getSequences().size() == 3); } @@ -133,7 +148,7 @@ class MergeFormatterPass { private void writeln(String s) throws IOException { out.beginln(); - out.write((s + "\n").getBytes(charsetName)); //$NON-NLS-1$ + out.write((s + "\n").getBytes(charset)); //$NON-NLS-1$ } private void writeLine(RawText seq, int i) throws IOException { 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 d4282e0b85..909f3b15d8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -46,11 +46,11 @@ */ package org.eclipse.jgit.merge; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.time.Instant.EPOCH; import static org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm.HISTOGRAM; 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.CHARACTER_ENCODING; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import java.io.BufferedOutputStream; @@ -1029,7 +1029,7 @@ public class ResolveMerger extends ThreeWayMerger { db != null ? nonNullRepo().getDirectory() : null, inCoreLimit); try { new MergeFormatter().formatMerge(buf, result, - Arrays.asList(commitNames), CHARACTER_ENCODING); + Arrays.asList(commitNames), UTF_8); buf.close(); } catch (IOException e) { buf.destroy(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AsyncRevObjectQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AsyncRevObjectQueue.java index d263184622..98654f14c2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AsyncRevObjectQueue.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AsyncRevObjectQueue.java @@ -66,5 +66,5 @@ public interface AsyncRevObjectQueue extends AsyncOperation { * @throws java.io.IOException * the object store cannot be accessed. */ - public RevObject next() throws MissingObjectException, IOException; + RevObject next() throws MissingObjectException, IOException; } 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 eaec305b47..5154920393 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java @@ -48,6 +48,7 @@ import java.io.IOException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.ObjectId; /** * Only produce commits which are below a specified depth. @@ -59,6 +60,8 @@ class DepthGenerator extends Generator { private final int depth; + private final int deepenSince; + private final RevWalk walk; /** @@ -79,6 +82,11 @@ class DepthGenerator extends Generator { private final RevFlag REINTERESTING; /** + * Commits reachable from commits that the client specified using --shallow-exclude. + */ + private final RevFlag DEEPEN_NOT; + + /** * @param w * @param s Parent generator * @throws MissingObjectException @@ -91,8 +99,10 @@ class DepthGenerator extends Generator { walk = (RevWalk)w; this.depth = w.getDepth(); + this.deepenSince = w.getDeepenSince(); this.UNSHALLOW = w.getUnshallowFlag(); this.REINTERESTING = w.getReinterestingFlag(); + this.DEEPEN_NOT = w.getDeepenNotFlag(); s.shareFreeList(pending); @@ -105,6 +115,37 @@ class DepthGenerator extends Generator { if (((DepthWalk.Commit) c).getDepth() == 0) pending.add(c); } + + // Mark DEEPEN_NOT on all deepen-not commits and their ancestors. + // TODO(jonathantanmy): This implementation is somewhat + // inefficient in that any "deepen-not <ref>" in the request + // results in all commits reachable from that ref being parsed + // and marked, even if the commit topology is such that it is + // not necessary. + for (ObjectId oid : w.getDeepenNots()) { + RevCommit c; + try { + c = walk.parseCommit(oid); + } catch (IncorrectObjectTypeException notCommit) { + // The C Git implementation silently tolerates + // non-commits, so do the same here. + continue; + } + + FIFORevQueue queue = new FIFORevQueue(); + queue.add(c); + while ((c = queue.next()) != null) { + if (c.has(DEEPEN_NOT)) { + continue; + } + + walk.parseHeaders(c); + c.add(DEEPEN_NOT); + for (RevCommit p : c.getParents()) { + queue.add(p); + } + } + } } @Override @@ -132,6 +173,14 @@ class DepthGenerator extends Generator { if ((c.flags & RevWalk.PARSED) == 0) c.parseHeaders(walk); + if (c.getCommitTime() < deepenSince) { + continue; + } + + if (c.has(DEEPEN_NOT)) { + continue; + } + int newDepth = c.depth + 1; for (RevCommit p : c.parents) { @@ -142,12 +191,29 @@ class DepthGenerator extends Generator { // this depth is guaranteed to be the smallest value that // any path could produce. if (dp.depth == -1) { + boolean failsDeepenSince = false; + if (deepenSince != 0) { + if ((p.flags & RevWalk.PARSED) == 0) { + p.parseHeaders(walk); + } + failsDeepenSince = + p.getCommitTime() < deepenSince; + } + dp.depth = newDepth; - // If the parent is not too deep, add it to the queue - // so that we can produce it later - if (newDepth <= depth) + // If the parent is not too deep and was not excluded, add + // it to the queue so that we can produce it later + if (newDepth <= depth && !failsDeepenSince && + !p.has(DEEPEN_NOT)) { pending.add(p); + } else { + dp.makesChildBoundary = true; + } + } + + if (dp.makesChildBoundary) { + c.isBoundary = true; } // If the current commit has become unshallowed, everything @@ -160,8 +226,7 @@ class DepthGenerator extends Generator { } } - // Produce all commits less than the depth cutoff - boolean produce = c.depth <= depth; + boolean produce = true; // Unshallow commits are uninteresting, but still need to be sent // up to the PackWriter so that it will exclude objects correctly. @@ -169,6 +234,10 @@ class DepthGenerator extends Generator { if ((c.flags & RevWalk.UNINTERESTING) != 0 && !c.has(UNSHALLOW)) produce = false; + if (c.getCommitTime() < deepenSince) { + produce = false; + } + if (produce) return c; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthWalk.java index 06a5272b98..0201f0b602 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthWalk.java @@ -45,10 +45,14 @@ package org.eclipse.jgit.revwalk; import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; @@ -61,7 +65,24 @@ public interface DepthWalk { * * @return Depth to filter to. */ - public int getDepth(); + int getDepth(); + + /** + * @return the deepen-since value; if not 0, this walk only returns commits + * whose commit time is at or after this limit + * @since 5.2 + */ + default int getDeepenSince() { + return 0; + } + + /** + * @return the objects specified by the client using --shallow-exclude + * @since 5.2 + */ + default List<ObjectId> getDeepenNots() { + return Collections.emptyList(); + } /** @return flag marking commits that should become unshallow. */ /** @@ -69,26 +90,49 @@ public interface DepthWalk { * * @return flag marking commits that should become unshallow. */ - public RevFlag getUnshallowFlag(); + RevFlag getUnshallowFlag(); /** * Get flag marking commits that are interesting again. * * @return flag marking commits that are interesting again. */ - public RevFlag getReinterestingFlag(); + RevFlag getReinterestingFlag(); + + /** + * @return flag marking commits that are to be excluded because of --shallow-exclude + * @since 5.2 + */ + RevFlag getDeepenNotFlag(); /** RevCommit with a depth (in commits) from a root. */ public static class Commit extends RevCommit { /** Depth of this commit in the graph, via shortest path. */ int depth; + boolean isBoundary; + + /** + * True if this commit was excluded due to a shallow fetch + * setting. All its children are thus boundary commits. + */ + boolean makesChildBoundary; + /** @return depth of this commit, as found by the shortest path. */ public int getDepth() { return depth; } /** + * @return true if at least one of this commit's parents was excluded + * due to a shallow fetch setting, false otherwise + * @since 5.2 + */ + public boolean isBoundary() { + return isBoundary; + } + + /** * Initialize a new commit. * * @param id @@ -104,10 +148,16 @@ public interface DepthWalk { public class RevWalk extends org.eclipse.jgit.revwalk.RevWalk implements DepthWalk { private final int depth; + private int deepenSince; + + private List<ObjectId> deepenNots; + private final RevFlag UNSHALLOW; private final RevFlag REINTERESTING; + private final RevFlag DEEPEN_NOT; + /** * @param repo Repository to walk * @param depth Maximum depth to return @@ -116,8 +166,10 @@ public interface DepthWalk { super(repo); this.depth = depth; + this.deepenNots = Collections.emptyList(); this.UNSHALLOW = newFlag("UNSHALLOW"); //$NON-NLS-1$ this.REINTERESTING = newFlag("REINTERESTING"); //$NON-NLS-1$ + this.DEEPEN_NOT = newFlag("DEEPEN_NOT"); //$NON-NLS-1$ } /** @@ -128,8 +180,10 @@ public interface DepthWalk { super(or); this.depth = depth; + this.deepenNots = Collections.emptyList(); this.UNSHALLOW = newFlag("UNSHALLOW"); //$NON-NLS-1$ this.REINTERESTING = newFlag("REINTERESTING"); //$NON-NLS-1$ + this.DEEPEN_NOT = newFlag("DEEPEN_NOT"); //$NON-NLS-1$ } /** @@ -159,6 +213,39 @@ public interface DepthWalk { } @Override + public int getDeepenSince() { + return deepenSince; + } + + /** + * Sets the deepen-since value. + * + * @param limit + * new deepen-since value + * @since 5.2 + */ + public void setDeepenSince(int limit) { + deepenSince = limit; + } + + @Override + public List<ObjectId> getDeepenNots() { + return deepenNots; + } + + /** + * Mark objects that the client specified using + * --shallow-exclude. Objects that are not commits have no + * effect. + * + * @param deepenNots specified objects + * @since 5.2 + */ + public void setDeepenNots(List<ObjectId> deepenNots) { + this.deepenNots = Objects.requireNonNull(deepenNots); + } + + @Override public RevFlag getUnshallowFlag() { return UNSHALLOW; } @@ -168,12 +255,19 @@ public interface DepthWalk { return REINTERESTING; } + @Override + public RevFlag getDeepenNotFlag() { + return DEEPEN_NOT; + } + /** * @since 4.5 */ @Override public ObjectWalk toObjectWalkWithSameObjects() { ObjectWalk ow = new ObjectWalk(reader, depth); + ow.deepenSince = deepenSince; + ow.deepenNots = deepenNots; ow.objects = objects; ow.freeFlags = freeFlags; return ow; @@ -184,10 +278,16 @@ public interface DepthWalk { public class ObjectWalk extends org.eclipse.jgit.revwalk.ObjectWalk implements DepthWalk { private final int depth; + private int deepenSince; + + private List<ObjectId> deepenNots; + private final RevFlag UNSHALLOW; private final RevFlag REINTERESTING; + private final RevFlag DEEPEN_NOT; + /** * @param repo Repository to walk * @param depth Maximum depth to return @@ -196,8 +296,10 @@ public interface DepthWalk { super(repo); this.depth = depth; + this.deepenNots = Collections.emptyList(); this.UNSHALLOW = newFlag("UNSHALLOW"); //$NON-NLS-1$ this.REINTERESTING = newFlag("REINTERESTING"); //$NON-NLS-1$ + this.DEEPEN_NOT = newFlag("DEEPEN_NOT"); //$NON-NLS-1$ } /** @@ -208,8 +310,10 @@ public interface DepthWalk { super(or); this.depth = depth; + this.deepenNots = Collections.emptyList(); this.UNSHALLOW = newFlag("UNSHALLOW"); //$NON-NLS-1$ this.REINTERESTING = newFlag("REINTERESTING"); //$NON-NLS-1$ + this.DEEPEN_NOT = newFlag("DEEPEN_NOT"); //$NON-NLS-1$ } /** @@ -263,6 +367,16 @@ public interface DepthWalk { } @Override + public int getDeepenSince() { + return deepenSince; + } + + @Override + public List<ObjectId> getDeepenNots() { + return deepenNots; + } + + @Override public RevFlag getUnshallowFlag() { return UNSHALLOW; } @@ -271,5 +385,10 @@ public interface DepthWalk { public RevFlag getReinterestingFlag() { return REINTERESTING; } + + @Override + public RevFlag getDeepenNotFlag() { + return DEEPEN_NOT; + } } } 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 86ecd8eaee..af4ec1f00b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java @@ -407,7 +407,7 @@ public class RevCommit extends RevObject { * @return contents of the gpg signature; null if the commit was not signed. * @since 5.1 */ - public final @Nullable byte[] getRawGpgSignature() { + public final byte[] getRawGpgSignature() { final byte[] raw = buffer; final byte[] header = {'g', 'p', 'g', 's', 'i', 'g'}; final int start = RawParseUtils.headerStart(header, raw, 0); 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 4d555d2178..400ea33c21 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java @@ -54,6 +54,7 @@ import java.util.Iterator; import java.util.List; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.LargeObjectException; @@ -1336,6 +1337,22 @@ 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. + * @return next most recent commit; null if traversal is over. + */ + @Nullable + private RevCommit nextForIterator() { + try { + return next(); + } catch (IOException e) { + throw new RevWalkException(e); + } + } + + /** * {@inheritDoc} * <p> * Returns an Iterator over the commits of this walker. @@ -1353,16 +1370,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { */ @Override public Iterator<RevCommit> iterator() { - final RevCommit first; - try { - first = RevWalk.this.next(); - } catch (MissingObjectException e) { - throw new RevWalkException(e); - } catch (IncorrectObjectTypeException e) { - throw new RevWalkException(e); - } catch (IOException e) { - throw new RevWalkException(e); - } + RevCommit first = nextForIterator(); return new Iterator<RevCommit>() { RevCommit next = first; @@ -1374,17 +1382,9 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { @Override public RevCommit next() { - try { - final RevCommit r = next; - next = RevWalk.this.next(); - return r; - } catch (MissingObjectException e) { - throw new RevWalkException(e); - } catch (IncorrectObjectTypeException e) { - throw new RevWalkException(e); - } catch (IOException e) { - throw new RevWalkException(e); - } + RevCommit r = next; + next = nextForIterator(); + return r; } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java index 633632dc01..84cd6adb8d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java @@ -57,7 +57,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.text.MessageFormat; -import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.LockFailedException; import org.eclipse.jgit.internal.JGitText; @@ -307,7 +306,6 @@ public class FileBasedConfig extends StoredConfig { * @since 4.10 */ @Override - @Nullable protected byte[] readIncludedConfig(String relPath) throws ConfigInvalidException { final File file; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java index ed05c733f3..72b4255df9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java @@ -55,7 +55,7 @@ public interface AdvertiseRefsHook { * {@link UploadPack#setAdvertisedRefs(java.util.Map)} and * {@link BaseReceivePack#setAdvertisedRefs(java.util.Map,java.util.Set)}. */ - public static final AdvertiseRefsHook DEFAULT = new AdvertiseRefsHook() { + AdvertiseRefsHook DEFAULT = new AdvertiseRefsHook() { @Override public void advertiseRefs(UploadPack uploadPack) { // Do nothing. @@ -77,7 +77,7 @@ public interface AdvertiseRefsHook { * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException * abort; the message will be sent to the user. */ - public void advertiseRefs(UploadPack uploadPack) + void advertiseRefs(UploadPack uploadPack) throws ServiceMayNotContinueException; /** @@ -90,6 +90,6 @@ public interface AdvertiseRefsHook { * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException * abort; the message will be sent to the user. */ - public void advertiseRefs(BaseReceivePack receivePack) + void advertiseRefs(BaseReceivePack receivePack) throws ServiceMayNotContinueException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java index f6ec4b90eb..c5661e5083 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java @@ -542,7 +542,7 @@ public class AmazonS3 { } buf = b.toByteArray(); if (buf.length > 0) { - err.initCause(new IOException("\n" + new String(buf))); //$NON-NLS-1$ + err.initCause(new IOException("\n" + new String(buf, UTF_8))); //$NON-NLS-1$ } } return err; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index d3419bc201..03763368a8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -72,12 +72,14 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.InvalidObjectIdException; +import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.errors.TooLargePackException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.PackLock; import org.eclipse.jgit.internal.submodule.SubmoduleValidator; +import org.eclipse.jgit.internal.submodule.SubmoduleValidator.SubmoduleValidationException; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.Config; @@ -1528,8 +1530,12 @@ public abstract class BaseReceivePack { AnyObjectId blobId = entry.getBlobId(); ObjectLoader blob = odb.open(blobId, Constants.OBJ_BLOB); - SubmoduleValidator.assertValidGitModulesFile( - new String(blob.getBytes(), UTF_8)); + try { + SubmoduleValidator.assertValidGitModulesFile( + new String(blob.getBytes(), UTF_8)); + } catch (LargeObjectException | SubmoduleValidationException e) { + throw new IOException(e); + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java index d4c514e636..19a1ab0b93 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java @@ -68,7 +68,7 @@ public interface Connection extends AutoCloseable { * modifiable. The collection can be empty if the remote side has no * refs (it is an empty/newly created repository). */ - public Map<String, Ref> getRefsMap(); + Map<String, Ref> getRefsMap(); /** * Get the complete list of refs advertised as available for fetching or @@ -82,7 +82,7 @@ public interface Connection extends AutoCloseable { * collection can be empty if the remote side has no refs (it is an * empty/newly created repository). */ - public Collection<Ref> getRefs(); + Collection<Ref> getRefs(); /** * Get a single advertised ref by name. @@ -95,7 +95,7 @@ public interface Connection extends AutoCloseable { * name of the ref to obtain. * @return the requested ref; null if the remote did not advertise this ref. */ - public Ref getRef(String name); + Ref getRef(String name); /** * {@inheritDoc} @@ -115,7 +115,7 @@ public interface Connection extends AutoCloseable { * the signature to prevent them from doing so. */ @Override - public void close(); + void close(); /** * Get the additional messages, if any, returned by the remote process. @@ -132,7 +132,7 @@ public interface Connection extends AutoCloseable { * newline (LF) character. The empty string is returned if the * remote produced no additional messages. */ - public String getMessages(); + String getMessages(); /** * User agent advertised by the remote server. @@ -141,5 +141,5 @@ public interface Connection extends AutoCloseable { * server does not advertise this version. * @since 4.0 */ - public String getPeerUserAgent(); + String getPeerUserAgent(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchConnection.java index f0c45d5fb6..1eb7cbd93a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchConnection.java @@ -109,7 +109,7 @@ public interface FetchConnection extends Connection { * protocol error, or error on remote side, or connection was * already used for fetch. */ - public void fetch(final ProgressMonitor monitor, + void fetch(final ProgressMonitor monitor, final Collection<Ref> want, final Set<ObjectId> have) throws TransportException; @@ -151,7 +151,7 @@ public interface FetchConnection extends Connection { * already used for fetch. * @since 3.0 */ - public void fetch(final ProgressMonitor monitor, + void fetch(final ProgressMonitor monitor, final Collection<Ref> want, final Set<ObjectId> have, OutputStream out) throws TransportException; @@ -173,7 +173,7 @@ public interface FetchConnection extends Connection { * @return true if the last fetch call implicitly included tag objects; * false if tags were not implicitly obtained. */ - public boolean didFetchIncludeTags(); + boolean didFetchIncludeTags(); /** * Did the last {@link #fetch(ProgressMonitor, Collection, Set)} validate @@ -196,7 +196,7 @@ public interface FetchConnection extends Connection { * client side in order to succeed; false if the last fetch assumed * the remote peer supplied a complete graph. */ - public boolean didFetchTestConnectivity(); + boolean didFetchTestConnectivity(); /** * Set the lock message used when holding a pack out of garbage collection. @@ -208,7 +208,7 @@ public interface FetchConnection extends Connection { * * @param message message to use when holding a pack in place. */ - public void setPackLockMessage(String message); + void setPackLockMessage(String message); /** * All locks created by the last @@ -218,5 +218,5 @@ public interface FetchConnection extends Connection { * fetch. The caller must release these after refs are updated in * order to safely permit garbage collection. */ - public Collection<PackLock> getPackLocks(); + Collection<PackLock> getPackLocks(); } 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 c43ab18c35..211707e9ad 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java @@ -44,6 +44,7 @@ package org.eclipse.jgit.transport; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK; import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD; @@ -337,7 +338,7 @@ class FetchProcess { try { if (lock.lock()) { try (Writer w = new OutputStreamWriter( - lock.getOutputStream())) { + lock.getOutputStream(), UTF_8)) { for (FetchHeadRecord h : fetchHeadUpdates) { h.write(w); result.add(h); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java new file mode 100644 index 0000000000..40ba3a3ad2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchRequest.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2018, Google LLC. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Set; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.ObjectId; + +/** + * Common fields between v0/v1/v2 fetch requests. + */ +abstract class FetchRequest { + + final Set<ObjectId> wantIds; + + final int depth; + + final Set<ObjectId> clientShallowCommits; + + final long filterBlobLimit; + + final Set<String> clientCapabilities; + + final int deepenSince; + + final List<String> deepenNotRefs; + + @Nullable + final String agent; + + /** + * Initialize the common fields of a fetch request. + * + * @param wantIds + * list of want ids + * @param depth + * how deep to go in the tree + * @param clientShallowCommits + * commits the client has without history + * @param filterBlobLimit + * to exclude blobs on certain conditions + * @param clientCapabilities + * capabilities sent in the request + * @param deepenNotRefs + * Requests that the shallow clone/fetch should be cut at these + * specific revisions instead of a depth. + * @param deepenSince + * Requests that the shallow clone/fetch should be cut at a + * specific time, instead of depth + * @param agent + * agent as reported by the client in the request body + */ + FetchRequest(@NonNull Set<ObjectId> wantIds, int depth, + @NonNull Set<ObjectId> clientShallowCommits, long filterBlobLimit, + @NonNull Set<String> clientCapabilities, int deepenSince, + @NonNull List<String> deepenNotRefs, @Nullable String agent) { + this.wantIds = requireNonNull(wantIds); + this.depth = depth; + this.clientShallowCommits = requireNonNull(clientShallowCommits); + this.filterBlobLimit = filterBlobLimit; + this.clientCapabilities = requireNonNull(clientCapabilities); + this.deepenSince = deepenSince; + this.deepenNotRefs = requireNonNull(deepenNotRefs); + this.agent = agent; + } + + /** + * @return object ids in the "want" (and "want-ref") lines of the request + */ + @NonNull + Set<ObjectId> getWantIds() { + return wantIds; + } + + /** + * @return the depth set in a "deepen" line. 0 by default. + */ + int getDepth() { + return depth; + } + + /** + * Shallow commits the client already has. + * + * These are sent by the client in "shallow" request lines. + * + * @return set of commits the client has declared as shallow. + */ + @NonNull + Set<ObjectId> getClientShallowCommits() { + return clientShallowCommits; + } + + /** + * @return the blob limit set in a "filter" line (-1 if not set) + */ + long getFilterBlobLimit() { + return filterBlobLimit; + } + + /** + * Capabilities that the client wants enabled from the server. + * + * Capabilities are options that tune the expected response from the server, + * like "thin-pack", "no-progress" or "ofs-delta". This list should be a + * subset of the capabilities announced by the server in its first response. + * + * These options are listed and well-defined in the git protocol + * specification. + * + * The agent capability is not included in this set. It can be retrieved via + * {@link #getAgent()}. + * + * @return capabilities sent by the client (excluding the "agent" + * capability) + */ + @NonNull + Set<String> getClientCapabilities() { + return clientCapabilities; + } + + /** + * The value in a "deepen-since" line in the request, indicating the + * timestamp where to stop fetching/cloning. + * + * @return timestamp in seconds since the epoch, where to stop the shallow + * fetch/clone. Defaults to 0 if not set in the request. + */ + int getDeepenSince() { + return deepenSince; + } + + /** + * @return refs received in "deepen-not" lines. + */ + @NonNull + List<String> getDeepenNotRefs() { + return deepenNotRefs; + } + + /** + * @return string identifying the agent (as sent in the request body by the + * client) + */ + @Nullable + String getAgent() { + return agent; + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java new file mode 100644 index 0000000000..05f4a8155f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV0Request.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2018, Google LLC. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.ObjectId; + +/** + * Fetch request in the V0/V1 protocol. + */ +final class FetchV0Request extends FetchRequest { + + FetchV0Request(@NonNull Set<ObjectId> wantIds, int depth, + @NonNull Set<ObjectId> clientShallowCommits, long filterBlobLimit, + @NonNull Set<String> clientCapabilities, @Nullable String agent) { + super(wantIds, depth, clientShallowCommits, filterBlobLimit, + clientCapabilities, 0, Collections.emptyList(), agent); + } + + static final class Builder { + + int depth; + + final Set<ObjectId> wantIds = new HashSet<>(); + + final Set<ObjectId> clientShallowCommits = new HashSet<>(); + + long filterBlobLimit = -1; + + final Set<String> clientCaps = new HashSet<>(); + + String agent; + + /** + * @param objectId + * object id received in a "want" line + * @return this builder + */ + Builder addWantId(ObjectId objectId) { + wantIds.add(objectId); + return this; + } + + /** + * @param d + * depth set in a "deepen" line + * @return this builder + */ + Builder setDepth(int d) { + depth = d; + return this; + } + + /** + * @param shallowOid + * object id received in a "shallow" line + * @return this builder + */ + Builder addClientShallowCommit(ObjectId shallowOid) { + clientShallowCommits.add(shallowOid); + return this; + } + + /** + * @param clientCapabilities + * client capabilities sent by the client in the first want + * line of the request + * @return this builder + */ + Builder addClientCapabilities(Collection<String> clientCapabilities) { + clientCaps.addAll(clientCapabilities); + return this; + } + + /** + * @param clientAgent + * agent line sent by the client in the request body + * @return this builder + */ + Builder setAgent(String clientAgent) { + agent = clientAgent; + return this; + } + + /** + * @param filterBlobLim + * blob limit set in a "filter" line + * @return this builder + */ + Builder setFilterBlobLimit(long filterBlobLim) { + filterBlobLimit = filterBlobLim; + return this; + } + + FetchV0Request build() { + return new FetchV0Request(wantIds, depth, clientShallowCommits, + filterBlobLimit, clientCaps, 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 34f3484951..ac6361cdeb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java @@ -42,70 +42,62 @@ */ package org.eclipse.jgit.transport; +import static java.util.Objects.requireNonNull; + import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.lib.ObjectId; /** - * fetch protocol v2 request. + * Fetch request from git protocol v2. * * <p> * This is used as an input to {@link ProtocolV2Hook}. * * @since 5.1 */ -public final class FetchV2Request { +public final class FetchV2Request extends FetchRequest { private final List<ObjectId> peerHas; private final List<String> wantedRefs; - private final Set<ObjectId> wantsIds; - - private final Set<ObjectId> clientShallowCommits; - - private final int deepenSince; - - private final List<String> deepenNotRefs; - - private final int depth; - - private final long filterBlobLimit; - - private final Set<String> options; - private final boolean doneReceived; - private FetchV2Request(List<ObjectId> peerHas, - List<String> wantedRefs, Set<ObjectId> wantsIds, - Set<ObjectId> clientShallowCommits, int deepenSince, - List<String> deepenNotRefs, int depth, long filterBlobLimit, - boolean doneReceived, Set<String> options) { - this.peerHas = peerHas; - this.wantedRefs = wantedRefs; - this.wantsIds = wantsIds; - this.clientShallowCommits = clientShallowCommits; - this.deepenSince = deepenSince; - this.deepenNotRefs = deepenNotRefs; - this.depth = depth; - this.filterBlobLimit = filterBlobLimit; + @NonNull + private final List<String> serverOptions; + + FetchV2Request(@NonNull List<ObjectId> peerHas, + @NonNull List<String> wantedRefs, + @NonNull Set<ObjectId> wantIds, + @NonNull Set<ObjectId> clientShallowCommits, int deepenSince, + @NonNull List<String> deepenNotRefs, int depth, + long filterBlobLimit, + boolean doneReceived, @NonNull Set<String> clientCapabilities, + @Nullable String agent, @NonNull List<String> serverOptions) { + super(wantIds, depth, clientShallowCommits, filterBlobLimit, + clientCapabilities, deepenSince, deepenNotRefs, agent); + this.peerHas = requireNonNull(peerHas); + this.wantedRefs = requireNonNull(wantedRefs); this.doneReceived = doneReceived; - this.options = options; + this.serverOptions = requireNonNull(serverOptions); } /** - * @return object ids in the "have" lines of the request + * @return object ids received in the "have" lines */ @NonNull List<ObjectId> getPeerHas() { - return this.peerHas; + return peerHas; } /** - * @return list of references in the "want-ref" lines of the request + * @return list of references received in "want-ref" lines */ @NonNull List<String> getWantedRefs() { @@ -113,59 +105,6 @@ public final class FetchV2Request { } /** - * @return object ids in the "want" (but not "want-ref") lines of the request - */ - @NonNull - Set<ObjectId> getWantsIds() { - return wantsIds; - } - - /** - * Shallow commits the client already has. - * - * These are sent by the client in "shallow" request lines. - * - * @return set of commits the client has declared as shallow. - */ - @NonNull - Set<ObjectId> getClientShallowCommits() { - return clientShallowCommits; - } - - /** - * The value in a "deepen-since" line in the request, indicating the - * timestamp where to stop fetching/cloning. - * - * @return timestamp in seconds since the epoch, where to stop the shallow - * fetch/clone. Defaults to 0 if not set in the request. - */ - int getDeepenSince() { - return deepenSince; - } - - /** - * @return the refs in "deepen-not" lines in the request. - */ - @NonNull - List<String> getDeepenNotRefs() { - return deepenNotRefs; - } - - /** - * @return the depth set in a "deepen" line. 0 by default. - */ - int getDepth() { - return depth; - } - - /** - * @return the blob limit set in a "filter" line (-1 if not set) - */ - long getFilterBlobLimit() { - return filterBlobLimit; - } - - /** * @return true if the request had a "done" line */ boolean wasDoneReceived() { @@ -173,17 +112,16 @@ public final class FetchV2Request { } /** - * Options that tune the expected response from the server, like - * "thin-pack", "no-progress" or "ofs-delta" + * Options received in server-option lines. The caller can choose to act on + * these in an application-specific way * - * These are options listed and well-defined in the git protocol - * specification + * @return Immutable list of server options received in the request * - * @return options found in the request lines + * @since 5.2 */ @NonNull - Set<String> getOptions() { - return options; + public List<String> getServerOptions() { + return serverOptions; } /** @return A builder of {@link FetchV2Request}. */ @@ -191,20 +129,19 @@ public final class FetchV2Request { return new Builder(); } - /** A builder for {@link FetchV2Request}. */ static final class Builder { - List<ObjectId> peerHas = new ArrayList<>(); + final List<ObjectId> peerHas = new ArrayList<>(); - List<String> wantedRefs = new ArrayList<>(); + final List<String> wantedRefs = new ArrayList<>(); - Set<ObjectId> wantsIds = new HashSet<>(); + final Set<ObjectId> wantIds = new HashSet<>(); - Set<ObjectId> clientShallowCommits = new HashSet<>(); + final Set<ObjectId> clientShallowCommits = new HashSet<>(); - List<String> deepenNotRefs = new ArrayList<>(); + final List<String> deepenNotRefs = new ArrayList<>(); - Set<String> options = new HashSet<>(); + final Set<String> clientCapabilities = new HashSet<>(); int depth; @@ -214,13 +151,18 @@ public final class FetchV2Request { boolean doneReceived; + @Nullable + String agent; + + final List<String> serverOptions = new ArrayList<>(); + private Builder() { } /** * @param objectId - * from a "have" line in a fetch request - * @return the builder + * object id received in a "have" line + * @return this builder */ Builder addPeerHas(ObjectId objectId) { peerHas.add(objectId); @@ -228,11 +170,11 @@ public final class FetchV2Request { } /** - * From a "want-ref" line in a fetch request + * Ref received in "want-ref" line and the object-id it refers to * * @param refName * reference name - * @return the builder + * @return this builder */ Builder addWantedRef(String refName) { wantedRefs.add(refName); @@ -240,42 +182,42 @@ public final class FetchV2Request { } /** - * @param option - * fetch request lines acting as options - * @return the builder + * @param clientCapability + * capability line sent by the client + * @return this builder */ - Builder addOption(String option) { - options.add(option); + Builder addClientCapability(String clientCapability) { + clientCapabilities.add(clientCapability); return this; } /** - * @param objectId - * from a "want" line in a fetch request - * @return the builder + * @param wantId + * object id received in a "want" line + * @return this builder */ - Builder addWantsId(ObjectId objectId) { - wantsIds.add(objectId); + Builder addWantId(ObjectId wantId) { + wantIds.add(wantId); return this; } /** * @param shallowOid - * from a "shallow" line in the fetch request - * @return the builder + * object id received in a "shallow" line + * @return this builder */ Builder addClientShallowCommit(ObjectId shallowOid) { - this.clientShallowCommits.add(shallowOid); + clientShallowCommits.add(shallowOid); return this; } /** * @param d - * from a "deepen" line in the fetch request - * @return the builder + * Depth received in a "deepen" line + * @return this builder */ Builder setDepth(int d) { - this.depth = d; + depth = d; return this; } @@ -284,32 +226,34 @@ public final class FetchV2Request { * 0 if not set. */ int getDepth() { - return this.depth; + return depth; } /** - * @return if there has been any "deepen not" line in the request + * @return true if there has been at least one "deepen not" line in the + * request so far */ boolean hasDeepenNotRefs() { return !deepenNotRefs.isEmpty(); } /** - * @param deepenNotRef reference in a "deepen not" line - * @return the builder + * @param deepenNotRef + * reference received in a "deepen not" line + * @return this builder */ Builder addDeepenNotRef(String deepenNotRef) { - this.deepenNotRefs.add(deepenNotRef); + deepenNotRefs.add(deepenNotRef); return this; } /** * @param value * Unix timestamp received in a "deepen since" line - * @return the builder + * @return this builder */ Builder setDeepenSince(int value) { - this.deepenSince = value; + deepenSince = value; return this; } @@ -318,35 +262,66 @@ public final class FetchV2Request { * by default. */ int getDeepenSince() { - return this.deepenSince; + return deepenSince; } /** - * @param filterBlobLimit + * @param filterBlobLim * set in a "filter" line - * @return the builder + * @return this builder */ - Builder setFilterBlobLimit(long filterBlobLimit) { - this.filterBlobLimit = filterBlobLimit; + Builder setFilterBlobLimit(long filterBlobLim) { + filterBlobLimit = filterBlobLim; return this; } /** * Mark that the "done" line has been received. * - * @return the builder + * @return this builder */ Builder setDoneReceived() { - this.doneReceived = true; + doneReceived = true; return this; } + + /** + * Value of an agent line received after the command and before the + * arguments. E.g. "agent=a.b.c/1.0" should set "a.b.c/1.0". + * + * @param agentValue + * the client-supplied agent capability, without the leading + * "agent=" + * @return this builder + */ + Builder setAgent(@Nullable String agentValue) { + agent = agentValue; + return this; + } + + /** + * Records an application-specific option supplied in a server-option + * line, for later retrieval with + * {@link FetchV2Request#getServerOptions}. + * + * @param value + * the client-supplied server-option capability, without + * leading "server-option=". + * @return this builder + */ + Builder addServerOption(@NonNull String value) { + serverOptions.add(value); + return this; + } + /** * @return Initialized fetch request */ FetchV2Request build() { - return new FetchV2Request(peerHas, wantedRefs, wantsIds, + return new FetchV2Request(peerHas, wantedRefs, wantIds, clientShallowCommits, deepenSince, deepenNotRefs, - depth, filterBlobLimit, doneReceived, options); + depth, filterBlobLimit, doneReceived, clientCapabilities, + agent, Collections.unmodifiableList(serverOptions)); } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java new file mode 100644 index 0000000000..39e87b246c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +/** + * An interface providing FTP operations over a {@link RemoteSession}. All + * operations are supposed to throw {@link FtpException} for remote file system + * errors and other IOExceptions on connection errors. + * + * @since 5.2 + */ +public interface FtpChannel { + + /** + * An {@link Exception} for reporting SFTP errors. + */ + static class FtpException extends IOException { + + private static final long serialVersionUID = 7176525179280330876L; + + public static final int OK = 0; + + public static final int EOF = 1; + + public static final int NO_SUCH_FILE = 2; + + public static final int NO_PERMISSION = 3; + + public static final int UNSPECIFIED_FAILURE = 4; + + public static final int PROTOCOL_ERROR = 5; + + public static final int UNSUPPORTED = 8; + + private final int status; + + public FtpException(String message, int status) { + super(message); + this.status = status; + } + + public FtpException(String message, int status, Throwable cause) { + super(message, cause); + this.status = status; + } + + public int getStatus() { + return status; + } + } + + /** + * Connects the {@link FtpChannel} to the remote end. + * + * @param timeout + * for establishing the FTP connection + * @param unit + * of the {@code timeout} + * @throws IOException + */ + void connect(int timeout, TimeUnit unit) throws IOException; + + /** + * Disconnects and {@link FtpChannel}. + */ + void disconnect(); + + /** + * @return whether the {@link FtpChannel} is connected + */ + boolean isConnected(); + + /** + * Changes the current remote directory. + * + * @param path + * target directory + * @throws IOException + * if the operation could not be performed remotely + */ + void cd(String path) throws IOException; + + /** + * @return the current remote directory path + * @throws IOException + */ + String pwd() throws IOException; + + /** + * Simplified remote directory entry. + */ + interface DirEntry { + String getFilename(); + + long getModifiedTime(); + + boolean isDirectory(); + } + + /** + * Lists contents of a remote directory + * + * @param path + * of the directory to list + * @return the directory entries + * @throws IOException + */ + Collection<DirEntry> ls(String path) throws IOException; + + /** + * Deletes a directory on the remote file system. The directory must be + * empty. + * + * @param path + * to delete + * @throws IOException + */ + void rmdir(String path) throws IOException; + + /** + * Creates a directory on the remote file system. + * + * @param path + * to create + * @throws IOException + */ + void mkdir(String path) throws IOException; + + /** + * Obtain an {@link InputStream} to read the contents of a remote file. + * + * @param path + * of the file to read + * + * @return the stream to read from + * @throws IOException + */ + InputStream get(String path) throws IOException; + + /** + * Obtain an {@link OutputStream} to write to a remote file. If the file + * exists already, it will be overwritten. + * + * @param path + * of the file to read + * + * @return the stream to read from + * @throws IOException + */ + OutputStream put(String path) throws IOException; + + /** + * Deletes a file on the remote file system. + * + * @param path + * to delete + * @throws IOException + * if the file does not exist or could otherwise not be deleted + */ + void rm(String path) throws IOException; + + /** + * Deletes a file on the remote file system. If the file does not exist, no + * exception is thrown. + * + * @param path + * to delete + * @throws IOException + * if the file exist but could not be deleted + */ + default void delete(String path) throws IOException { + try { + rm(path); + } catch (FileNotFoundException e) { + // Ignore; it's OK if the file doesn't exist + } catch (FtpException f) { + if (f.getStatus() == FtpException.NO_SUCH_FILE) { + return; + } + throw f; + } + } + + /** + * Renames a file on the remote file system. If {@code to} exists, it is + * replaced by {@code from}. (POSIX rename() semantics) + * + * @param from + * original name of the file + * @param to + * new name of the file + * @throws IOException + * @see <a href= + * "http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html">stdio.h: + * rename()</a> + */ + void rename(String from, String to) throws IOException; + +} 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 760ac6c1d7..1561c93b95 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java @@ -245,6 +245,20 @@ public final class GitProtocolConstants { public static final String CAPABILITY_REF_IN_WANT = "ref-in-want"; //$NON-NLS-1$ /** + * The server supports arbitrary options + * + * @since 5.2 + */ + public static final String CAPABILITY_SERVER_OPTION = "server-option"; //$NON-NLS-1$ + + /** + * Option for passing application-specific options to the server. + * + * @since 5.2 + */ + public static final String OPTION_SERVER_OPTION = "server-option"; //$NON-NLS-1$ + + /** * The server supports listing refs using protocol v2. * * @since 5.0 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalPushConnection.java index 732be63dc1..f05e0b8c7d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalPushConnection.java @@ -46,6 +46,7 @@ package org.eclipse.jgit.transport; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.io.UncheckedIOException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; @@ -103,10 +104,13 @@ class InternalPushConnection<C> extends BasePackPushConnection { // Ignored. Client cannot use this repository. } catch (ServiceNotAuthorizedException e) { // Ignored. Client cannot use this repository. - } catch (IOException err) { - // Client side of the pipes should report the problem. - } catch (RuntimeException err) { - // Clients side will notice we went away, and report. + } catch (IOException e) { + // Since the InternalPushConnection + // is used in tests, we want to avoid hiding exceptions + // because they can point to programming errors on the server + // side. By rethrowing, the default handler will dump it + // to stderr. + throw new UncheckedIOException(e); } finally { try { out_r.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java index 4e712a5567..0bdd6ba812 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java @@ -52,7 +52,6 @@ package org.eclipse.jgit.transport; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; -import static org.eclipse.jgit.transport.OpenSshConfig.SSH_PORT; import java.io.File; import java.io.FileInputStream; @@ -275,10 +274,11 @@ public abstract class JschConfigSessionFactory extends SshSessionFactory { } private static String hostName(Session s) { - if (s.getPort() == SSH_PORT) { + if (s.getPort() == SshConstants.SSH_DEFAULT_PORT) { return s.getHost(); } - return String.format("[%s]:%d", s.getHost(), s.getPort()); //$NON-NLS-1$ + return String.format("[%s]:%d", s.getHost(), //$NON-NLS-1$ + Integer.valueOf(s.getPort())); } private void copyConfigValueToSession(Session session, Config cfg, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java index e3ef832343..843b90c951 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java @@ -52,6 +52,11 @@ import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; @@ -59,8 +64,10 @@ import org.eclipse.jgit.util.io.IsolatedOutputStream; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; +import com.jcraft.jsch.SftpException; /** * Run remote commands using Jsch. @@ -109,12 +116,24 @@ public class JschSession implements RemoteSession { * @return a channel suitable for Sftp operations. * @throws com.jcraft.jsch.JSchException * on problems getting the channel. + * @deprecated since 5.2; use {@link #getFtpChannel()} instead */ + @Deprecated public Channel getSftpChannel() throws JSchException { return sock.openChannel("sftp"); //$NON-NLS-1$ } /** + * {@inheritDoc} + * + * @since 5.2 + */ + @Override + public FtpChannel getFtpChannel() { + return new JschFtpChannel(); + } + + /** * Implementation of Process for running a single command using Jsch. * <p> * Uses the Jsch session to do actual command execution and manage the @@ -233,4 +252,154 @@ public class JschSession implements RemoteSession { return exitValue(); } } + + private class JschFtpChannel implements FtpChannel { + + private ChannelSftp ftp; + + @Override + public void connect(int timeout, TimeUnit unit) throws IOException { + try { + ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$ + ftp.connect((int) unit.toMillis(timeout)); + } catch (JSchException e) { + ftp = null; + throw new IOException(e.getLocalizedMessage(), e); + } + } + + @Override + public void disconnect() { + ftp.disconnect(); + ftp = null; + } + + private <T> T map(Callable<T> op) throws IOException { + try { + return op.call(); + } catch (Exception e) { + if (e instanceof SftpException) { + throw new FtpChannel.FtpException(e.getLocalizedMessage(), + ((SftpException) e).id, e); + } + throw new IOException(e.getLocalizedMessage(), e); + } + } + + @Override + public boolean isConnected() { + return ftp != null && sock.isConnected(); + } + + @Override + public void cd(String path) throws IOException { + map(() -> { + ftp.cd(path); + return null; + }); + } + + @Override + public String pwd() throws IOException { + return map(() -> ftp.pwd()); + } + + @Override + public Collection<DirEntry> ls(String path) throws IOException { + return map(() -> { + List<DirEntry> result = new ArrayList<>(); + for (Object e : ftp.ls(path)) { + ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e; + result.add(new DirEntry() { + + @Override + public String getFilename() { + return entry.getFilename(); + } + + @Override + public long getModifiedTime() { + return entry.getAttrs().getMTime(); + } + + @Override + public boolean isDirectory() { + return entry.getAttrs().isDir(); + } + }); + } + return result; + }); + } + + @Override + public void rmdir(String path) throws IOException { + map(() -> { + ftp.rm(path); + return null; + }); + } + + @Override + public void mkdir(String path) throws IOException { + map(() -> { + ftp.mkdir(path); + return null; + }); + } + + @Override + public InputStream get(String path) throws IOException { + return map(() -> ftp.get(path)); + } + + @Override + public OutputStream put(String path) throws IOException { + return map(() -> ftp.put(path)); + } + + @Override + public void rm(String path) throws IOException { + map(() -> { + ftp.rm(path); + return null; + }); + } + + @Override + public void rename(String from, String to) throws IOException { + map(() -> { + // Plain FTP rename will fail if "to" exists. Jsch knows about + // the FTP extension "posix-rename@openssh.com", which will + // remove "to" first if it exists. + if (hasPosixRename()) { + ftp.rename(from, to); + } else if (!to.equals(from)) { + // Try to remove "to" first. With git, we typically get this + // when a lock file is moved over the file locked. Note that + // the check for to being equal to from may still fail in + // the general case, but for use with JGit's TransportSftp + // it should be good enough. + delete(to); + ftp.rename(from, to); + } + return null; + }); + } + + /** + * Determine whether the server has the posix-rename extension. + * + * @return {@code true} if it is supported, {@code false} otherwise + * @see <a href= + * "https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?annotate=HEAD">OpenSSH + * deviations and extensions to the published SSH protocol</a> + * @see <a href= + * "http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html">stdio.h: + * rename()</a> + */ + private boolean hasPosixRename() { + return "1".equals(ftp.getExtension("posix-rename@openssh.com")); //$NON-NLS-1$//$NON-NLS-2$ + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/LsRefsV2Request.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/LsRefsV2Request.java index 3aff584a00..add373147c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/LsRefsV2Request.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/LsRefsV2Request.java @@ -42,9 +42,15 @@ */ package org.eclipse.jgit.transport; +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; + /** * ls-refs protocol v2 request. * @@ -60,11 +66,20 @@ public final class LsRefsV2Request { private final boolean peel; + @Nullable + private final String agent; + + @NonNull + private final List<String> serverOptions; + private LsRefsV2Request(List<String> refPrefixes, boolean symrefs, - boolean peel) { + boolean peel, @Nullable String agent, + @NonNull List<String> serverOptions) { this.refPrefixes = refPrefixes; this.symrefs = symrefs; this.peel = peel; + this.agent = agent; + this.serverOptions = requireNonNull(serverOptions); } /** @return ref prefixes that the client requested. */ @@ -82,6 +97,34 @@ public final class LsRefsV2Request { return peel; } + /** + * @return agent as reported by the client + * + * @since 5.2 + */ + @Nullable + public String getAgent() { + return agent; + } + + /** + * Get application-specific options provided by the client using + * --server-option. + * <p> + * It returns just the content, without the "server-option=" prefix. E.g. a + * request with server-option=A and server-option=B lines returns the list + * [A, B]. + * + * @return application-specific options from the client as an unmodifiable + * list + * + * @since 5.2 + */ + @NonNull + public List<String> getServerOptions() { + return serverOptions; + } + /** @return A builder of {@link LsRefsV2Request}. */ public static Builder builder() { return new Builder(); @@ -95,6 +138,10 @@ public final class LsRefsV2Request { private boolean peel; + private final List<String> serverOptions = new ArrayList<>(); + + private String agent; + private Builder() { } @@ -125,10 +172,43 @@ public final class LsRefsV2Request { return this; } + /** + * Records an application-specific option supplied in a server-option + * line, for later retrieval with + * {@link LsRefsV2Request#getServerOptions}. + * + * @param value + * the client-supplied server-option capability, without + * leading "server-option=". + * @return this builder + * @since 5.2 + */ + public Builder addServerOption(@NonNull String value) { + serverOptions.add(value); + return this; + } + + /** + * Value of an agent line received after the command and before the + * arguments. E.g. "agent=a.b.c/1.0" should set "a.b.c/1.0". + * + * @param value + * the client-supplied agent capability, without leading + * "agent=" + * @return this builder + * + * @since 5.2 + */ + public Builder setAgent(@Nullable String value) { + agent = value; + return this; + } + /** @return LsRefsV2Request */ public LsRefsV2Request build() { return new LsRefsV2Request( - Collections.unmodifiableList(refPrefixes), symrefs, peel); + Collections.unmodifiableList(refPrefixes), symrefs, peel, + agent, Collections.unmodifiableList(serverOptions)); } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java index 4f1eba66d3..7dd019ba27 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java @@ -42,10 +42,13 @@ package org.eclipse.jgit.transport; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.BufferedReader; import java.io.File; -import java.io.FileReader; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.time.Instant; import java.util.Collection; import java.util.HashMap; @@ -214,7 +217,8 @@ public class NetRC { this.hosts.clear(); this.lastModified = FS.DETECTED.lastModifiedInstant(this.netrc); - try (BufferedReader r = new BufferedReader(new FileReader(netrc))) { + try (BufferedReader r = new BufferedReader( + new InputStreamReader(new FileInputStream(netrc), UTF_8))) { String line = null; NetRCEntry entry = new NetRCEntry(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NonceGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NonceGenerator.java index 51fe9070c0..fc22034340 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NonceGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NonceGenerator.java @@ -66,7 +66,7 @@ public interface NonceGenerator { * @return The nonce to be signed by the pusher * @throws java.lang.IllegalStateException */ - public String createNonce(Repository db, long timestamp) + String createNonce(Repository db, long timestamp) throws IllegalStateException; /** @@ -91,6 +91,6 @@ public interface NonceGenerator { * @return a NonceStatus indicating the trustworthiness of the received * nonce. */ - public NonceStatus verify(String received, String sent, + NonceStatus verify(String received, String sent, Repository db, boolean allowSlop, int slop); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java index 4dd5df9cd6..32e1dff234 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, 2017, Google Inc. + * Copyright (C) 2008, 2018, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -43,30 +43,16 @@ package org.eclipse.jgit.transport; -import java.io.BufferedReader; +import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive; + import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Set; +import java.util.TreeMap; -import org.eclipse.jgit.errors.InvalidPatternException; -import org.eclipse.jgit.fnmatch.FileNameMatcher; -import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; +import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry; import org.eclipse.jgit.util.FS; -import org.eclipse.jgit.util.StringUtils; -import org.eclipse.jgit.util.SystemReader; import com.jcraft.jsch.ConfigRepository; @@ -84,8 +70,7 @@ import com.jcraft.jsch.ConfigRepository; * <li>JSch's OpenSSHConfig doesn't monitor for config file changes. * </ul> * <p> - * Therefore implement our own parser to read an OpenSSH configuration file. It - * makes the critical options available to + * This parser makes the critical options available to * {@link org.eclipse.jgit.transport.SshSessionFactory} via * {@link org.eclipse.jgit.transport.OpenSshConfig.Host} objects returned by * {@link #lookup(String)}, and implements a fully conforming @@ -93,49 +78,11 @@ import com.jcraft.jsch.ConfigRepository; * {@link com.jcraft.jsch.ConfigRepository.Config}s via * {@link #getConfig(String)}. * </p> - * <p> - * Limitations compared to the full OpenSSH 7.5 parser: - * </p> - * <ul> - * <li>This parser does not handle Match or Include keywords. - * <li>This parser does not do host name canonicalization (Jsch ignores it - * anyway). - * </ul> - * <p> - * Note that OpenSSH's readconf.c is a validating parser; Jsch's - * ConfigRepository OTOH treats all option values as plain strings, so any - * validation must happen in Jsch outside of the parser. Thus this parser does - * not validate option values, except for a few options when constructing a - * {@link org.eclipse.jgit.transport.OpenSshConfig.Host} object. - * </p> - * <p> - * This config does %-substitutions for the following tokens: - * </p> - * <ul> - * <li>%% - single % - * <li>%C - short-hand for %l%h%p%r. See %p and %r below; the replacement may be - * done partially only and may leave %p or %r or both unreplaced. - * <li>%d - home directory path - * <li>%h - remote host name - * <li>%L - local host name without domain - * <li>%l - FQDN of the local host - * <li>%n - host name as specified in {@link #lookup(String)} - * <li>%p - port number; replaced only if set in the config - * <li>%r - remote user name; replaced only if set in the config - * <li>%u - local user name - * </ul> - * <p> - * If the config doesn't set the port or the remote user name, %p and %r remain - * un-substituted. It's the caller's responsibility to replace them with values - * obtained from the connection URI. %i is not handled; Java has no concept of a - * "user ID". - * </p> + * + * @see OpenSshConfigFile */ public class OpenSshConfig implements ConfigRepository { - /** IANA assigned port number for SSH. */ - static final int SSH_PORT = 22; - /** * Obtain the user's configuration data. * <p> @@ -154,43 +101,17 @@ public class OpenSshConfig implements ConfigRepository { if (home == null) home = new File(".").getAbsoluteFile(); //$NON-NLS-1$ - final File config = new File(new File(home, ".ssh"), Constants.CONFIG); //$NON-NLS-1$ - final OpenSshConfig osc = new OpenSshConfig(home, config); - osc.refresh(); - return osc; + final File config = new File(new File(home, SshConstants.SSH_DIR), + SshConstants.CONFIG); + return new OpenSshConfig(home, config); } - /** The user's home directory, as key files may be relative to here. */ - private final File home; - - /** The .ssh/config file we read and monitor for updates. */ - private final File configFile; - - /** Modification time of {@link #configFile} when it was last loaded. */ - private Instant lastModified; - - /** - * Encapsulates entries read out of the configuration file, and - * {@link Host}s created from that. - */ - private static class State { - Map<String, HostEntry> entries = new LinkedHashMap<>(); - Map<String, Host> hosts = new HashMap<>(); - - @Override - @SuppressWarnings("nls") - public String toString() { - return "State [entries=" + entries + ", hosts=" + hosts + "]"; - } - } - - /** State read from the config file, plus {@link Host}s created from it. */ - private State state; + /** The base file. */ + private OpenSshConfigFile configFile; OpenSshConfig(File h, File cfg) { - home = h; - configFile = cfg; - state = new State(); + configFile = new OpenSshConfigFile(h, cfg, + SshSessionFactory.getLocalUserName()); } /** @@ -203,603 +124,8 @@ public class OpenSshConfig implements ConfigRepository { * @return r configuration for the requested name. Never null. */ public Host lookup(String hostName) { - final State cache = refresh(); - Host h = cache.hosts.get(hostName); - if (h != null) { - return h; - } - HostEntry fullConfig = new HostEntry(); - // Initialize with default entries at the top of the file, before the - // first Host block. - fullConfig.merge(cache.entries.get(HostEntry.DEFAULT_NAME)); - for (Map.Entry<String, HostEntry> e : cache.entries.entrySet()) { - String key = e.getKey(); - if (isHostMatch(key, hostName)) { - fullConfig.merge(e.getValue()); - } - } - fullConfig.substitute(hostName, home); - h = new Host(fullConfig, hostName, home); - cache.hosts.put(hostName, h); - return h; - } - - private synchronized State refresh() { - final Instant mtime = FS.DETECTED.lastModifiedInstant(configFile); - if (!mtime.equals(lastModified)) { - State newState = new State(); - try (FileInputStream in = new FileInputStream(configFile)) { - newState.entries = parse(in); - } catch (IOException none) { - // Ignore -- we'll set and return an empty state - } - lastModified = mtime; - state = newState; - } - return state; - } - - private Map<String, HostEntry> parse(InputStream in) - throws IOException { - final Map<String, HostEntry> m = new LinkedHashMap<>(); - final BufferedReader br = new BufferedReader(new InputStreamReader(in)); - final List<HostEntry> current = new ArrayList<>(4); - String line; - - // The man page doesn't say so, but the OpenSSH parser (readconf.c) - // starts out in active mode and thus always applies any lines that - // occur before the first host block. We gather those options in a - // HostEntry for DEFAULT_NAME. - HostEntry defaults = new HostEntry(); - current.add(defaults); - m.put(HostEntry.DEFAULT_NAME, defaults); - - while ((line = br.readLine()) != null) { - line = line.trim(); - if (line.isEmpty() || line.startsWith("#")) { //$NON-NLS-1$ - continue; - } - String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$ - // Although the ssh-config man page doesn't say so, the OpenSSH - // parser does allow quoted keywords. - String keyword = dequote(parts[0].trim()); - // man 5 ssh-config says lines had the format "keyword arguments", - // with no indication that arguments were optional. However, let's - // not crap out on missing arguments. See bug 444319. - String argValue = parts.length > 1 ? parts[1].trim() : ""; //$NON-NLS-1$ - - if (StringUtils.equalsIgnoreCase("Host", keyword)) { //$NON-NLS-1$ - current.clear(); - for (String name : HostEntry.parseList(argValue)) { - if (name == null || name.isEmpty()) { - // null should not occur, but better be safe than sorry. - continue; - } - HostEntry c = m.get(name); - if (c == null) { - c = new HostEntry(); - m.put(name, c); - } - current.add(c); - } - continue; - } - - if (current.isEmpty()) { - // We received an option outside of a Host block. We - // don't know who this should match against, so skip. - continue; - } - - if (HostEntry.isListKey(keyword)) { - List<String> args = HostEntry.parseList(argValue); - for (HostEntry entry : current) { - entry.setValue(keyword, args); - } - } else if (!argValue.isEmpty()) { - argValue = dequote(argValue); - for (HostEntry entry : current) { - entry.setValue(keyword, argValue); - } - } - } - - return m; - } - - private static boolean isHostMatch(final String pattern, - final String name) { - if (pattern.startsWith("!")) { //$NON-NLS-1$ - return !patternMatchesHost(pattern.substring(1), name); - } else { - return patternMatchesHost(pattern, name); - } - } - - private static boolean patternMatchesHost(final String pattern, - final String name) { - if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) { - final FileNameMatcher fn; - try { - fn = new FileNameMatcher(pattern, null); - } catch (InvalidPatternException e) { - return false; - } - fn.append(name); - return fn.isMatch(); - } else { - // Not a pattern but a full host name - return pattern.equals(name); - } - } - - private static String dequote(String value) { - if (value.startsWith("\"") && value.endsWith("\"") //$NON-NLS-1$ //$NON-NLS-2$ - && value.length() > 1) - return value.substring(1, value.length() - 1); - return value; - } - - private static String nows(String value) { - final StringBuilder b = new StringBuilder(); - for (int i = 0; i < value.length(); i++) { - if (!Character.isSpaceChar(value.charAt(i))) - b.append(value.charAt(i)); - } - return b.toString(); - } - - private static Boolean yesno(String value) { - if (StringUtils.equalsIgnoreCase("yes", value)) //$NON-NLS-1$ - return Boolean.TRUE; - return Boolean.FALSE; - } - - private static File toFile(String path, File home) { - if (path.startsWith("~/")) { //$NON-NLS-1$ - return new File(home, path.substring(2)); - } - File ret = new File(path); - if (ret.isAbsolute()) { - return ret; - } - return new File(home, path); - } - - private static int positive(String value) { - if (value != null) { - try { - return Integer.parseUnsignedInt(value); - } catch (NumberFormatException e) { - // Ignore - } - } - return -1; - } - - static String userName() { - return AccessController.doPrivileged(new PrivilegedAction<String>() { - @Override - public String run() { - return SystemReader.getInstance() - .getProperty(Constants.OS_USER_NAME_KEY); - } - }); - } - - private static class HostEntry implements ConfigRepository.Config { - - /** - * "Host name" of the HostEntry for the default options before the first - * host block in a config file. - */ - public static final String DEFAULT_NAME = ""; //$NON-NLS-1$ - - // See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys - // to ssh-config keys. - private static final Map<String, String> KEY_MAP = new HashMap<>(); - - static { - KEY_MAP.put("kex", "KexAlgorithms"); //$NON-NLS-1$//$NON-NLS-2$ - KEY_MAP.put("server_host_key", "HostKeyAlgorithms"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("cipher.c2s", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("cipher.s2c", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("mac.c2s", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("mac.s2c", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("compression.s2c", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("compression.c2s", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("MaxAuthTries", "NumberOfPasswordPrompts"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - /** - * Keys that can be specified multiple times, building up a list. (I.e., - * those are the keys that do not follow the general rule of "first - * occurrence wins".) - */ - private static final Set<String> MULTI_KEYS = new HashSet<>(); - - static { - MULTI_KEYS.add("CERTIFICATEFILE"); //$NON-NLS-1$ - MULTI_KEYS.add("IDENTITYFILE"); //$NON-NLS-1$ - MULTI_KEYS.add("LOCALFORWARD"); //$NON-NLS-1$ - MULTI_KEYS.add("REMOTEFORWARD"); //$NON-NLS-1$ - MULTI_KEYS.add("SENDENV"); //$NON-NLS-1$ - } - - /** - * Keys that take a whitespace-separated list of elements as argument. - * Because the dequote-handling is different, we must handle those in - * the parser. There are a few other keys that take comma-separated - * lists as arguments, but for the parser those are single arguments - * that must be quoted if they contain whitespace, and taking them apart - * is the responsibility of the user of those keys. - */ - private static final Set<String> LIST_KEYS = new HashSet<>(); - - static { - LIST_KEYS.add("CANONICALDOMAINS"); //$NON-NLS-1$ - LIST_KEYS.add("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$ - LIST_KEYS.add("SENDENV"); //$NON-NLS-1$ - LIST_KEYS.add("USERKNOWNHOSTSFILE"); //$NON-NLS-1$ - } - - private Map<String, String> options; - - private Map<String, List<String>> multiOptions; - - private Map<String, List<String>> listOptions; - - @Override - public String getHostname() { - return getValue("HOSTNAME"); //$NON-NLS-1$ - } - - @Override - public String getUser() { - return getValue("USER"); //$NON-NLS-1$ - } - - @Override - public int getPort() { - return positive(getValue("PORT")); //$NON-NLS-1$ - } - - private static String mapKey(String key) { - String k = KEY_MAP.get(key); - if (k == null) { - k = key; - } - return k.toUpperCase(Locale.ROOT); - } - - private String findValue(String key) { - String k = mapKey(key); - String result = options != null ? options.get(k) : null; - if (result == null) { - // Also check the list and multi options. Modern OpenSSH treats - // UserKnownHostsFile and GlobalKnownHostsFile as list-valued, - // and so does this parser. Jsch 0.1.54 in general doesn't know - // about list-valued options (it _does_ know multi-valued - // options, though), and will ask for a single value for such - // options. - // - // Let's be lenient and return at least the first value from - // a list-valued or multi-valued key for which Jsch asks for a - // single value. - List<String> values = listOptions != null ? listOptions.get(k) - : null; - if (values == null) { - values = multiOptions != null ? multiOptions.get(k) : null; - } - if (values != null && !values.isEmpty()) { - result = values.get(0); - } - } - return result; - } - - @Override - public String getValue(String key) { - // See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() for this - // special case. - if (key.equals("compression.s2c") //$NON-NLS-1$ - || key.equals("compression.c2s")) { //$NON-NLS-1$ - String foo = findValue(key); - if (foo == null || foo.equals("no")) { //$NON-NLS-1$ - return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$ - } - return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$ - } - return findValue(key); - } - - @Override - public String[] getValues(String key) { - String k = mapKey(key); - List<String> values = listOptions != null ? listOptions.get(k) - : null; - if (values == null) { - values = multiOptions != null ? multiOptions.get(k) : null; - } - if (values == null || values.isEmpty()) { - return new String[0]; - } - return values.toArray(new String[0]); - } - - public void setValue(String key, String value) { - String k = key.toUpperCase(Locale.ROOT); - if (MULTI_KEYS.contains(k)) { - if (multiOptions == null) { - multiOptions = new HashMap<>(); - } - List<String> values = multiOptions.get(k); - if (values == null) { - values = new ArrayList<>(4); - multiOptions.put(k, values); - } - values.add(value); - } else { - if (options == null) { - options = new HashMap<>(); - } - if (!options.containsKey(k)) { - options.put(k, value); - } - } - } - - public void setValue(String key, List<String> values) { - if (values.isEmpty()) { - // Can occur only on a missing argument: ignore. - return; - } - String k = key.toUpperCase(Locale.ROOT); - // Check multi-valued keys first; because of the replacement - // strategy, they must take precedence over list-valued keys - // which always follow the "first occurrence wins" strategy. - // - // Note that SendEnv is a multi-valued list-valued key. (It's - // rather immaterial for JGit, though.) - if (MULTI_KEYS.contains(k)) { - if (multiOptions == null) { - multiOptions = new HashMap<>(2 * MULTI_KEYS.size()); - } - List<String> items = multiOptions.get(k); - if (items == null) { - items = new ArrayList<>(values); - multiOptions.put(k, items); - } else { - items.addAll(values); - } - } else { - if (listOptions == null) { - listOptions = new HashMap<>(2 * LIST_KEYS.size()); - } - if (!listOptions.containsKey(k)) { - listOptions.put(k, values); - } - } - } - - public static boolean isListKey(String key) { - return LIST_KEYS.contains(key.toUpperCase(Locale.ROOT)); - } - - /** - * Splits the argument into a list of whitespace-separated elements. - * Elements containing whitespace must be quoted and will be de-quoted. - * - * @param argument - * argument part of the configuration line as read from the - * config file - * @return a {@link List} of elements, possibly empty and possibly - * containing empty elements - */ - public static List<String> parseList(String argument) { - List<String> result = new ArrayList<>(4); - int start = 0; - int length = argument.length(); - while (start < length) { - // Skip whitespace - if (Character.isSpaceChar(argument.charAt(start))) { - start++; - continue; - } - if (argument.charAt(start) == '"') { - int stop = argument.indexOf('"', ++start); - if (stop < start) { - // No closing double quote: skip - break; - } - result.add(argument.substring(start, stop)); - start = stop + 1; - } else { - int stop = start + 1; - while (stop < length - && !Character.isSpaceChar(argument.charAt(stop))) { - stop++; - } - result.add(argument.substring(start, stop)); - start = stop + 1; - } - } - return result; - } - - protected void merge(HostEntry entry) { - if (entry == null) { - // Can occur if we could not read the config file - return; - } - if (entry.options != null) { - if (options == null) { - options = new HashMap<>(); - } - for (Map.Entry<String, String> item : entry.options - .entrySet()) { - if (!options.containsKey(item.getKey())) { - options.put(item.getKey(), item.getValue()); - } - } - } - if (entry.listOptions != null) { - if (listOptions == null) { - listOptions = new HashMap<>(2 * LIST_KEYS.size()); - } - for (Map.Entry<String, List<String>> item : entry.listOptions - .entrySet()) { - if (!listOptions.containsKey(item.getKey())) { - listOptions.put(item.getKey(), item.getValue()); - } - } - - } - if (entry.multiOptions != null) { - if (multiOptions == null) { - multiOptions = new HashMap<>(2 * MULTI_KEYS.size()); - } - for (Map.Entry<String, List<String>> item : entry.multiOptions - .entrySet()) { - List<String> values = multiOptions.get(item.getKey()); - if (values == null) { - values = new ArrayList<>(item.getValue()); - multiOptions.put(item.getKey(), values); - } else { - values.addAll(item.getValue()); - } - } - } - } - - private class Replacer { - private final Map<Character, String> replacements = new HashMap<>(); - - public Replacer(String originalHostName, File home) { - replacements.put(Character.valueOf('%'), "%"); //$NON-NLS-1$ - replacements.put(Character.valueOf('d'), home.getPath()); - // Needs special treatment... - String host = getValue("HOSTNAME"); //$NON-NLS-1$ - replacements.put(Character.valueOf('h'), originalHostName); - if (host != null && host.indexOf('%') >= 0) { - host = substitute(host, "h"); //$NON-NLS-1$ - options.put("HOSTNAME", host); //$NON-NLS-1$ - } - if (host != null) { - replacements.put(Character.valueOf('h'), host); - } - String localhost = SystemReader.getInstance().getHostname(); - replacements.put(Character.valueOf('l'), localhost); - int period = localhost.indexOf('.'); - if (period > 0) { - localhost = localhost.substring(0, period); - } - replacements.put(Character.valueOf('L'), localhost); - replacements.put(Character.valueOf('n'), originalHostName); - replacements.put(Character.valueOf('p'), getValue("PORT")); //$NON-NLS-1$ - replacements.put(Character.valueOf('r'), getValue("USER")); //$NON-NLS-1$ - replacements.put(Character.valueOf('u'), userName()); - replacements.put(Character.valueOf('C'), - substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$ - } - - public String substitute(String input, String allowed) { - if (input == null || input.length() <= 1 - || input.indexOf('%') < 0) { - return input; - } - StringBuilder builder = new StringBuilder(); - int start = 0; - int length = input.length(); - while (start < length) { - int percent = input.indexOf('%', start); - if (percent < 0 || percent + 1 >= length) { - builder.append(input.substring(start)); - break; - } - String replacement = null; - char ch = input.charAt(percent + 1); - if (ch == '%' || allowed.indexOf(ch) >= 0) { - replacement = replacements.get(Character.valueOf(ch)); - } - if (replacement == null) { - builder.append(input.substring(start, percent + 2)); - } else { - builder.append(input.substring(start, percent)) - .append(replacement); - } - start = percent + 2; - } - return builder.toString(); - } - } - - private List<String> substitute(List<String> values, String allowed, - Replacer r) { - List<String> result = new ArrayList<>(values.size()); - for (String value : values) { - result.add(r.substitute(value, allowed)); - } - return result; - } - - private List<String> replaceTilde(List<String> values, File home) { - List<String> result = new ArrayList<>(values.size()); - for (String value : values) { - result.add(toFile(value, home).getPath()); - } - return result; - } - - protected void substitute(String originalHostName, File home) { - Replacer r = new Replacer(originalHostName, home); - if (multiOptions != null) { - List<String> values = multiOptions.get("IDENTITYFILE"); //$NON-NLS-1$ - if (values != null) { - values = substitute(values, "dhlru", r); //$NON-NLS-1$ - values = replaceTilde(values, home); - multiOptions.put("IDENTITYFILE", values); //$NON-NLS-1$ - } - values = multiOptions.get("CERTIFICATEFILE"); //$NON-NLS-1$ - if (values != null) { - values = substitute(values, "dhlru", r); //$NON-NLS-1$ - values = replaceTilde(values, home); - multiOptions.put("CERTIFICATEFILE", values); //$NON-NLS-1$ - } - } - if (listOptions != null) { - List<String> values = listOptions.get("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$ - if (values != null) { - values = replaceTilde(values, home); - listOptions.put("GLOBALKNOWNHOSTSFILE", values); //$NON-NLS-1$ - } - values = listOptions.get("USERKNOWNHOSTSFILE"); //$NON-NLS-1$ - if (values != null) { - values = replaceTilde(values, home); - listOptions.put("USERKNOWNHOSTSFILE", values); //$NON-NLS-1$ - } - } - if (options != null) { - // HOSTNAME already done in Replacer constructor - String value = options.get("IDENTITYAGENT"); //$NON-NLS-1$ - if (value != null) { - value = r.substitute(value, "dhlru"); //$NON-NLS-1$ - value = toFile(value, home).getPath(); - options.put("IDENTITYAGENT", value); //$NON-NLS-1$ - } - } - // Match is not implemented and would need to be done elsewhere - // anyway. ControlPath, LocalCommand, ProxyCommand, and - // RemoteCommand are not used by Jsch. - } - - @Override - @SuppressWarnings("nls") - public String toString() { - return "HostEntry [options=" + options + ", multiOptions=" - + multiOptions + ", listOptions=" + listOptions + "]"; - } + HostEntry entry = configFile.lookup(hostName, -1, null); + return new Host(entry, hostName, configFile.getLocalUserName()); } /** @@ -830,8 +156,34 @@ public class OpenSshConfig implements ConfigRepository { int connectionAttempts; + private HostEntry entry; + private Config config; + // See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys + // to ssh-config keys. + private static final Map<String, String> KEY_MAP = new TreeMap<>( + String.CASE_INSENSITIVE_ORDER); + + static { + KEY_MAP.put("kex", SshConstants.KEX_ALGORITHMS); //$NON-NLS-1$ + KEY_MAP.put("server_host_key", SshConstants.HOST_KEY_ALGORITHMS); //$NON-NLS-1$ + KEY_MAP.put("cipher.c2s", SshConstants.CIPHERS); //$NON-NLS-1$ + KEY_MAP.put("cipher.s2c", SshConstants.CIPHERS); //$NON-NLS-1$ + KEY_MAP.put("mac.c2s", SshConstants.MACS); //$NON-NLS-1$ + KEY_MAP.put("mac.s2c", SshConstants.MACS); //$NON-NLS-1$ + KEY_MAP.put("compression.s2c", SshConstants.COMPRESSION); //$NON-NLS-1$ + KEY_MAP.put("compression.c2s", SshConstants.COMPRESSION); //$NON-NLS-1$ + KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$ + KEY_MAP.put("MaxAuthTries", //$NON-NLS-1$ + SshConstants.NUMBER_OF_PASSWORD_PROMPTS); + } + + private static String mapKey(String key) { + String k = KEY_MAP.get(key); + return k != null ? k : key; + } + /** * Creates a new uninitialized {@link Host}. */ @@ -839,9 +191,9 @@ public class OpenSshConfig implements ConfigRepository { // For API backwards compatibility with pre-4.9 JGit } - Host(Config config, String hostName, File homeDir) { - this.config = config; - complete(hostName, homeDir); + Host(HostEntry entry, String hostName, String localUserName) { + this.entry = entry; + complete(hostName, localUserName); } /** @@ -911,42 +263,84 @@ public class OpenSshConfig implements ConfigRepository { } - private void complete(String initialHostName, File homeDir) { + private void complete(String initialHostName, String localUserName) { // Try to set values from the options. - hostName = config.getHostname(); - user = config.getUser(); - port = config.getPort(); + hostName = entry.getValue(SshConstants.HOST_NAME); + user = entry.getValue(SshConstants.USER); + port = positive(entry.getValue(SshConstants.PORT)); connectionAttempts = positive( - config.getValue("ConnectionAttempts")); //$NON-NLS-1$ - strictHostKeyChecking = config.getValue("StrictHostKeyChecking"); //$NON-NLS-1$ - String value = config.getValue("BatchMode"); //$NON-NLS-1$ - if (value != null) { - batchMode = yesno(value); - } - value = config.getValue("PreferredAuthentications"); //$NON-NLS-1$ - if (value != null) { - preferredAuthentications = nows(value); - } + entry.getValue(SshConstants.CONNECTION_ATTEMPTS)); + strictHostKeyChecking = entry + .getValue(SshConstants.STRICT_HOST_KEY_CHECKING); + batchMode = Boolean.valueOf(OpenSshConfigFile + .flag(entry.getValue(SshConstants.BATCH_MODE))); + preferredAuthentications = entry + .getValue(SshConstants.PREFERRED_AUTHENTICATIONS); // Fill in defaults if still not set - if (hostName == null) { + if (hostName == null || hostName.isEmpty()) { hostName = initialHostName; } - if (user == null) { - user = OpenSshConfig.userName(); + if (user == null || user.isEmpty()) { + user = localUserName; } if (port <= 0) { - port = OpenSshConfig.SSH_PORT; + port = SshConstants.SSH_DEFAULT_PORT; } if (connectionAttempts <= 0) { connectionAttempts = 1; } - String[] identityFiles = config.getValues("IdentityFile"); //$NON-NLS-1$ - if (identityFiles != null && identityFiles.length > 0) { - identityFile = toFile(identityFiles[0], homeDir); + List<String> identityFiles = entry + .getValues(SshConstants.IDENTITY_FILE); + if (identityFiles != null && !identityFiles.isEmpty()) { + identityFile = new File(identityFiles.get(0)); } } Config getConfig() { + if (config == null) { + config = new Config() { + + @Override + public String getHostname() { + return Host.this.getHostName(); + } + + @Override + public String getUser() { + return Host.this.getUser(); + } + + @Override + public int getPort() { + return Host.this.getPort(); + } + + @Override + public String getValue(String key) { + // See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() + // for this special case. + if (key.equals("compression.s2c") //$NON-NLS-1$ + || key.equals("compression.c2s")) { //$NON-NLS-1$ + if (!OpenSshConfigFile.flag( + Host.this.entry.getValue(mapKey(key)))) { + return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$ + } + return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$ + } + return Host.this.entry.getValue(mapKey(key)); + } + + @Override + public String[] getValues(String key) { + List<String> values = Host.this.entry + .getValues(mapKey(key)); + if (values == null) { + return new String[0]; + } + return values.toArray(new String[0]); + } + }; + } return config; } @@ -958,7 +352,7 @@ public class OpenSshConfig implements ConfigRepository { + ", preferredAuthentications=" + preferredAuthentications + ", batchMode=" + batchMode + ", strictHostKeyChecking=" + strictHostKeyChecking + ", connectionAttempts=" - + connectionAttempts + ", config=" + config + "]"; + + connectionAttempts + ", entry=" + entry + "]"; } } @@ -978,9 +372,7 @@ public class OpenSshConfig implements ConfigRepository { /** {@inheritDoc} */ @Override - @SuppressWarnings("nls") public String toString() { - return "OpenSshConfig [home=" + home + ", configFile=" + configFile - + ", lastModified=" + lastModified + ", state=" + state + "]"; + return "OpenSshConfig [configFile=" + configFile + ']'; //$NON-NLS-1$ } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHook.java index 5cbb6f5dfb..ba5d2f3c8f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHook.java @@ -63,7 +63,7 @@ import java.util.Collection; */ public interface PostReceiveHook { /** A simple no-op hook. */ - public static final PostReceiveHook NULL = new PostReceiveHook() { + PostReceiveHook NULL = new PostReceiveHook() { @Override public void onPostReceive(final ReceivePack rp, final Collection<ReceiveCommand> commands) { @@ -81,6 +81,6 @@ public interface PostReceiveHook { * unmodifiable set of successfully completed commands. May be * the empty set. */ - public void onPostReceive(ReceivePack rp, + void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHook.java index 09667eb01a..3aa3b127e5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHook.java @@ -57,7 +57,7 @@ import org.eclipse.jgit.storage.pack.PackStatistics; */ public interface PostUploadHook { /** A simple no-op hook. */ - public static final PostUploadHook NULL = new PostUploadHook() { + PostUploadHook NULL = new PostUploadHook() { @Override public void onPostUpload(PackStatistics stats) { // Do nothing. @@ -72,5 +72,5 @@ public interface PostUploadHook { * {@link org.eclipse.jgit.internal.storage.pack.PackWriter} for * the uploaded pack */ - public void onPostUpload(PackStatistics stats); + void onPostUpload(PackStatistics stats); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHook.java index 77c1a8af29..30845d3b68 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHook.java @@ -79,7 +79,7 @@ import java.util.Collection; */ public interface PreReceiveHook { /** A simple no-op hook. */ - public static final PreReceiveHook NULL = new PreReceiveHook() { + PreReceiveHook NULL = new PreReceiveHook() { @Override public void onPreReceive(final ReceivePack rp, final Collection<ReceiveCommand> commands) { @@ -99,5 +99,5 @@ public interface PreReceiveHook { * unmodifiable set of valid commands still pending execution. * May be the empty set. */ - public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands); + void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHook.java index 2e1cd5800a..65dc241584 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHook.java @@ -59,7 +59,7 @@ import org.eclipse.jgit.lib.ObjectId; */ public interface PreUploadHook { /** A simple no-op hook. */ - public static final PreUploadHook NULL = new PreUploadHook() { + PreUploadHook NULL = new PreUploadHook() { @Override public void onBeginNegotiateRound(UploadPack up, Collection<? extends ObjectId> wants, int cntOffered) @@ -96,7 +96,7 @@ public interface PreUploadHook { * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException * abort; the message will be sent to the user. */ - public void onBeginNegotiateRound(UploadPack up, + void onBeginNegotiateRound(UploadPack up, Collection<? extends ObjectId> wants, int cntOffered) throws ServiceMayNotContinueException; @@ -120,7 +120,7 @@ public interface PreUploadHook { * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException * abort; the message will be sent to the user. */ - public void onEndNegotiateRound(UploadPack up, + void onEndNegotiateRound(UploadPack up, Collection<? extends ObjectId> wants, int cntCommon, int cntNotFound, boolean ready) throws ServiceMayNotContinueException; @@ -141,7 +141,7 @@ public interface PreUploadHook { * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException * abort; the message will be sent to the user. */ - public void onSendPack(UploadPack up, Collection<? extends ObjectId> wants, + void onSendPack(UploadPack up, Collection<? extends ObjectId> wants, Collection<? extends ObjectId> haves) throws ServiceMayNotContinueException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java new file mode 100644 index 0000000000..21498d6f5c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2018, Google LLC. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport; + +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_FILTER; + +import java.io.EOFException; +import java.io.IOException; +import java.text.MessageFormat; + +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.transport.parser.FirstWant; +import org.eclipse.jgit.lib.ObjectId; + +/** + * Parser for git protocol versions 0 and 1. + * + * It reads the lines coming through the {@link PacketLineIn} and builds a + * {@link FetchV0Request} object. + * + * It requires a transferConfig object to know if the server supports filters. + */ +final class ProtocolV0Parser { + + private final TransferConfig transferConfig; + + ProtocolV0Parser(TransferConfig transferConfig) { + this.transferConfig = transferConfig; + } + + /** + * Parse an incoming protocol v1 upload request arguments from the wire. + * + * The incoming PacketLineIn is consumed until an END line, but the caller + * is responsible for closing it (if needed). + * + * @param pckIn + * incoming lines. This method will read until an END line. + * @return a FetchV0Request with the data received in the wire. + * @throws PackProtocolException + * @throws IOException + */ + FetchV0Request recvWants(PacketLineIn pckIn) + throws PackProtocolException, IOException { + FetchV0Request.Builder reqBuilder = new FetchV0Request.Builder(); + + boolean isFirst = true; + boolean filterReceived = false; + + for (;;) { + String line; + try { + line = pckIn.readString(); + } catch (EOFException eof) { + if (isFirst) { + break; + } + throw eof; + } + + if (line == PacketLineIn.END) { + break; + } + + if (line.startsWith("deepen ")) { //$NON-NLS-1$ + int depth = Integer.parseInt(line.substring(7)); + if (depth <= 0) { + throw new PackProtocolException( + MessageFormat.format(JGitText.get().invalidDepth, + Integer.valueOf(depth))); + } + reqBuilder.setDepth(depth); + continue; + } + + if (line.startsWith("shallow ")) { //$NON-NLS-1$ + reqBuilder.addClientShallowCommit( + ObjectId.fromString(line.substring(8))); + continue; + } + + if (transferConfig.isAllowFilter() + && line.startsWith(OPTION_FILTER + " ")) { //$NON-NLS-1$ + String arg = line.substring(OPTION_FILTER.length() + 1); + + if (filterReceived) { + throw new PackProtocolException( + JGitText.get().tooManyFilters); + } + filterReceived = true; + + reqBuilder.setFilterBlobLimit(ProtocolV2Parser.filterLine(arg)); + continue; + } + + if (!line.startsWith("want ") || line.length() < 45) { //$NON-NLS-1$ + throw new PackProtocolException(MessageFormat + .format(JGitText.get().expectedGot, "want", line)); //$NON-NLS-1$ + } + + if (isFirst) { + if (line.length() > 45) { + FirstWant firstLine = FirstWant.fromLine(line); + reqBuilder.addClientCapabilities(firstLine.getCapabilities()); + reqBuilder.setAgent(firstLine.getAgent()); + line = firstLine.getLine(); + } + } + + reqBuilder.addWantId(ObjectId.fromString(line.substring(5))); + isFirst = false; + } + + return reqBuilder.build(); + } + +} 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 2cc50a7f38..8f4b86ee0a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java @@ -44,15 +44,20 @@ package org.eclipse.jgit.transport; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_DEEPEN_RELATIVE; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_FILTER; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_INCLUDE_TAG; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_NO_PROGRESS; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_OFS_DELTA; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SERVER_OPTION; 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_WANT_REF; import java.io.IOException; import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.JGitText; @@ -73,6 +78,35 @@ final class ProtocolV2Parser { this.transferConfig = transferConfig; } + /* + * Read lines until DELIM or END, calling the appropiate consumer. + * + * Returns the last read line (so caller can check if there is more to read + * in the line). + */ + private static String consumeCapabilities(PacketLineIn pckIn, + Consumer<String> serverOptionConsumer, + Consumer<String> agentConsumer) throws IOException { + + String serverOptionPrefix = OPTION_SERVER_OPTION + '='; + String agentPrefix = OPTION_AGENT + '='; + + String line = pckIn.readString(); + while (line != PacketLineIn.DELIM && line != PacketLineIn.END) { + if (line.startsWith(serverOptionPrefix)) { + serverOptionConsumer + .accept(line.substring(serverOptionPrefix.length())); + } else if (line.startsWith(agentPrefix)) { + agentConsumer.accept(line.substring(agentPrefix.length())); + } else { + // Unrecognized capability. Ignore it. + } + line = pckIn.readString(); + } + + return line; + } + /** * Parse the incoming fetch request arguments from the wire. The caller must * be sure that what is comings is a fetch request before coming here. @@ -93,21 +127,26 @@ final class ProtocolV2Parser { // Packs are always sent multiplexed and using full 64K // lengths. - reqBuilder.addOption(OPTION_SIDE_BAND_64K); + reqBuilder.addClientCapability(OPTION_SIDE_BAND_64K); - String line; + String line = consumeCapabilities(pckIn, + serverOption -> reqBuilder.addServerOption(serverOption), + agent -> reqBuilder.setAgent(agent)); - // Currently, we do not support any capabilities, so the next - // line is DELIM. - if ((line = pckIn.readString()) != PacketLineIn.DELIM) { - throw new PackProtocolException(MessageFormat - .format(JGitText.get().unexpectedPacketLine, line)); + if (line == PacketLineIn.END) { + return reqBuilder.build(); + } + + if (line != PacketLineIn.DELIM) { + throw new PackProtocolException( + MessageFormat.format(JGitText.get().unexpectedPacketLine, + line)); } boolean filterReceived = false; while ((line = pckIn.readString()) != PacketLineIn.END) { if (line.startsWith("want ")) { //$NON-NLS-1$ - reqBuilder.addWantsId(ObjectId.fromString(line.substring(5))); + reqBuilder.addWantId(ObjectId.fromString(line.substring(5))); } else if (transferConfig.isAllowRefInWant() && line.startsWith(OPTION_WANT_REF + " ")) { //$NON-NLS-1$ reqBuilder.addWantedRef(line.substring(OPTION_WANT_REF.length() + 1)); @@ -116,13 +155,13 @@ final class ProtocolV2Parser { } else if (line.equals("done")) { //$NON-NLS-1$ reqBuilder.setDoneReceived(); } else if (line.equals(OPTION_THIN_PACK)) { - reqBuilder.addOption(OPTION_THIN_PACK); + reqBuilder.addClientCapability(OPTION_THIN_PACK); } else if (line.equals(OPTION_NO_PROGRESS)) { - reqBuilder.addOption(OPTION_NO_PROGRESS); + reqBuilder.addClientCapability(OPTION_NO_PROGRESS); } else if (line.equals(OPTION_INCLUDE_TAG)) { - reqBuilder.addOption(OPTION_INCLUDE_TAG); + reqBuilder.addClientCapability(OPTION_INCLUDE_TAG); } else if (line.equals(OPTION_OFS_DELTA)) { - reqBuilder.addOption(OPTION_OFS_DELTA); + reqBuilder.addClientCapability(OPTION_OFS_DELTA); } else if (line.startsWith("shallow ")) { //$NON-NLS-1$ reqBuilder.addClientShallowCommit( ObjectId.fromString(line.substring(8))); @@ -149,7 +188,7 @@ final class ProtocolV2Parser { JGitText.get().deepenNotWithDeepen); } } else if (line.equals(OPTION_DEEPEN_RELATIVE)) { - reqBuilder.addOption(OPTION_DEEPEN_RELATIVE); + reqBuilder.addClientCapability(OPTION_DEEPEN_RELATIVE); } else if (line.startsWith("deepen-since ")) { //$NON-NLS-1$ int ts = Integer.parseInt(line.substring(13)); if (ts <= 0) { @@ -180,6 +219,57 @@ final class ProtocolV2Parser { } /** + * Parse the incoming ls-refs request arguments from the wire. This is meant + * for calling immediately after the caller has consumed a "command=ls-refs" + * line indicating the beginning of a ls-refs request. + * + * The incoming PacketLineIn is consumed until an END line, but the caller + * is responsible for closing it (if needed) + * + * @param pckIn + * incoming lines. This method will read until an END line. + * @return a LsRefsV2Request object with the data received in the wire. + * @throws PackProtocolException + * for inconsistencies in the protocol (e.g. unexpected lines) + * @throws IOException + * reporting problems reading the incoming messages from the + * wire + */ + LsRefsV2Request parseLsRefsRequest(PacketLineIn pckIn) + throws PackProtocolException, IOException { + LsRefsV2Request.Builder builder = LsRefsV2Request.builder(); + List<String> prefixes = new ArrayList<>(); + + String line = consumeCapabilities(pckIn, + serverOption -> builder.addServerOption(serverOption), + agent -> builder.setAgent(agent)); + + if (line == PacketLineIn.END) { + return builder.build(); + } + + if (line != PacketLineIn.DELIM) { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().unexpectedPacketLine, line)); + } + + while ((line = pckIn.readString()) != PacketLineIn.END) { + if (line.equals("peel")) { //$NON-NLS-1$ + builder.setPeel(true); + } else if (line.equals("symrefs")) { //$NON-NLS-1$ + builder.setSymrefs(true); + } else if (line.startsWith("ref-prefix ")) { //$NON-NLS-1$ + prefixes.add(line.substring("ref-prefix ".length())); //$NON-NLS-1$ + } else { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().unexpectedPacketLine, line)); + } + } + + return builder.setRefPrefixes(prefixes).build(); + } + + /* * Process the content of "filter" line from the protocol. It has a shape * like "blob:none" or "blob:limit=N", with limit a positive number. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java index ff2939a3d6..7f98d4dcc9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java @@ -113,7 +113,7 @@ public interface PushConnection extends Connection { * created. Non-critical errors concerning only isolated refs * should be placed in refUpdates. */ - public void push(final ProgressMonitor monitor, + void push(final ProgressMonitor monitor, final Map<String, RemoteRefUpdate> refUpdates) throws TransportException; @@ -163,7 +163,7 @@ public interface PushConnection extends Connection { * should be placed in refUpdates. * @since 3.0 */ - public void push(final ProgressMonitor monitor, + void push(final ProgressMonitor monitor, final Map<String, RemoteRefUpdate> refUpdates, OutputStream out) throws TransportException; 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 35fb0b17a7..577aaf4e9e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -76,8 +76,6 @@ public class ReceivePack extends BaseReceivePack { /** If {@link BasePackPushConnection#CAPABILITY_REPORT_STATUS} is enabled. */ private boolean reportStatus; - private boolean echoCommandFailures; - /** Whether the client intends to use push options. */ private boolean usePushOptions; private List<String> pushOptions; @@ -191,9 +189,11 @@ public class ReceivePack extends BaseReceivePack { * messages before sending the command results. This is usually * not necessary, but may help buggy Git clients that discard the * errors when all branches fail. + * @deprecated no widely used Git versions need this any more */ + @Deprecated public void setEchoCommandFailures(boolean echo) { - echoCommandFailures = echo; + // No-op. } /** @@ -269,36 +269,28 @@ public class ReceivePack extends BaseReceivePack { } } - if (unpackError == null) { - boolean atomic = isCapabilityEnabled(CAPABILITY_ATOMIC); - setAtomic(atomic); + try { + if (unpackError == null) { + boolean atomic = isCapabilityEnabled(CAPABILITY_ATOMIC); + setAtomic(atomic); - validateCommands(); - if (atomic && anyRejects()) - failPendingCommands(); + validateCommands(); + if (atomic && anyRejects()) { + failPendingCommands(); + } - preReceive.onPreReceive(this, filterCommands(Result.NOT_ATTEMPTED)); - if (atomic && anyRejects()) - failPendingCommands(); - executeCommands(); + preReceive.onPreReceive( + this, filterCommands(Result.NOT_ATTEMPTED)); + if (atomic && anyRejects()) { + failPendingCommands(); + } + executeCommands(); + } + } finally { + unlockPack(); } - unlockPack(); if (reportStatus) { - if (echoCommandFailures && msgOut != null) { - sendStatusReport(false, unpackError, new Reporter() { - @Override - void sendString(String s) throws IOException { - msgOut.write(Constants.encode(s + "\n")); //$NON-NLS-1$ - } - }); - msgOut.flush(); - try { - Thread.sleep(500); - } catch (InterruptedException wakeUp) { - // Ignore an early wake up. - } - } sendStatusReport(true, unpackError, new Reporter() { @Override void sendString(String s) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java index 4662435ea7..6595cab71d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java @@ -207,7 +207,8 @@ public abstract class RefAdvertiser { * <p> * This method must be invoked prior to any of the following: * <ul> - * <li>{@link #send(Map)} + * <li>{@link #send(Map)}</li> + * <li>{@link #send(Collection)}</li> * </ul> * * @param deref @@ -223,8 +224,9 @@ public abstract class RefAdvertiser { * <p> * This method must be invoked prior to any of the following: * <ul> - * <li>{@link #send(Map)} - * <li>{@link #advertiseHave(AnyObjectId)} + * <li>{@link #send(Map)}</li> + * <li>{@link #send(Collection)}</li> + * <li>{@link #advertiseHave(AnyObjectId)}</li> * </ul> * * @param name @@ -257,8 +259,9 @@ public abstract class RefAdvertiser { * <p> * This method must be invoked prior to any of the following: * <ul> - * <li>{@link #send(Map)} - * <li>{@link #advertiseHave(AnyObjectId)} + * <li>{@link #send(Map)}</li> + * <li>{@link #send(Collection)}</li> + * <li>{@link #advertiseHave(AnyObjectId)}</li> * </ul> * * @param from diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefFilter.java index 992ddc6e53..d6d6198f5b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefFilter.java @@ -61,7 +61,7 @@ public interface RefFilter { /** * The default filter, allows all refs to be shown. */ - public static final RefFilter DEFAULT = new RefFilter() { + RefFilter DEFAULT = new RefFilter() { @Override public Map<String, Ref> filter (Map<String, Ref> refs) { return refs; @@ -76,5 +76,5 @@ public interface RefFilter { * @return * the filtered map of refs. */ - public Map<String, Ref> filter(Map<String, Ref> refs); + Map<String, Ref> filter(Map<String, Ref> refs); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java index 931653fa90..9a67f0f8fe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java @@ -521,7 +521,7 @@ public class RemoteRefUpdate { : "(null)") + "..." + (newObjectId != null ? newObjectId.name() : "(null)") + (fastForward ? ", fastForward" : "") - + ", srcRef=" + srcRef + + ", srcRef=" + srcRef + (forceUpdate ? ", forceUpdate" : "") + ", message=" + (message != null ? "\"" + message + "\"" : "null") + "]"; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java index 525c895f45..e2109c2c5b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java @@ -78,10 +78,21 @@ public interface RemoteSession { * a TransportException may be thrown (a subclass of * java.io.IOException). */ - public Process exec(String commandName, int timeout) throws IOException; + Process exec(String commandName, int timeout) throws IOException; + + /** + * Obtain an {@link FtpChannel} for performing FTP operations over this + * {@link RemoteSession}. The default implementation returns {@code null}. + * + * @return the {@link FtpChannel} + * @since 5.2 + */ + default FtpChannel getFtpChannel() { + return null; + } /** * Disconnect the remote session */ - public void disconnect(); + void disconnect(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java index 90600cbb98..fde4401289 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java @@ -236,7 +236,7 @@ public class SideBandInputStream extends InputStream { messages.write(msg); if (out != null) - out.write(msg.getBytes()); + out.write(msg.getBytes(UTF_8)); } private void beginTask(int totalWorkUnits) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java new file mode 100644 index 0000000000..2b79d7105c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2018 Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +import org.eclipse.jgit.lib.Constants; + +/** + * Constants relating to ssh. + * + * @since 5.2 + */ +@SuppressWarnings("nls") +public final class SshConstants { + + private SshConstants() { + // No instances, please. + } + + /** IANA assigned port number for ssh. */ + public static final int SSH_DEFAULT_PORT = 22; + + /** URI scheme for ssh. */ + public static final String SSH_SCHEME = "ssh"; + + /** URI scheme for sftp. */ + public static final String SFTP_SCHEME = "sftp"; + + /** Default name for a ssh directory. */ + public static final String SSH_DIR = ".ssh"; + + /** Name of the ssh config file. */ + public static final String CONFIG = Constants.CONFIG; + + /** Default name of the user "known hosts" file. */ + public static final String KNOWN_HOSTS = "known_hosts"; + + // Config file keys + + /** Key in an ssh config file. */ + public static final String BATCH_MODE = "BatchMode"; + + /** Key in an ssh config file. */ + public static final String CANONICAL_DOMAINS = "CanonicalDomains"; + + /** Key in an ssh config file. */ + public static final String CERTIFICATE_FILE = "CertificateFile"; + + /** Key in an ssh config file. */ + public static final String CIPHERS = "Ciphers"; + + /** Key in an ssh config file. */ + public static final String COMPRESSION = "Compression"; + + /** Key in an ssh config file. */ + public static final String CONNECTION_ATTEMPTS = "ConnectionAttempts"; + + /** Key in an ssh config file. */ + public static final String CONTROL_PATH = "ControlPath"; + + /** Key in an ssh config file. */ + public static final String GLOBAL_KNOWN_HOSTS_FILE = "GlobalKnownHostsFile"; + + /** Key in an ssh config file. */ + public static final String HOST = "Host"; + + /** Key in an ssh config file. */ + public static final String HOST_KEY_ALGORITHMS = "HostKeyAlgorithms"; + + /** Key in an ssh config file. */ + public static final String HOST_NAME = "HostName"; + + /** Key in an ssh config file. */ + public static final String IDENTITIES_ONLY = "IdentitiesOnly"; + + /** Key in an ssh config file. */ + public static final String IDENTITY_AGENT = "IdentityAgent"; + + /** Key in an ssh config file. */ + public static final String IDENTITY_FILE = "IdentityFile"; + + /** Key in an ssh config file. */ + public static final String KEX_ALGORITHMS = "KexAlgorithms"; + + /** Key in an ssh config file. */ + public static final String LOCAL_COMMAND = "LocalCommand"; + + /** Key in an ssh config file. */ + public static final String LOCAL_FORWARD = "LocalForward"; + + /** Key in an ssh config file. */ + public static final String MACS = "MACs"; + + /** Key in an ssh config file. */ + public static final String NUMBER_OF_PASSWORD_PROMPTS = "NumberOfPasswordPrompts"; + + /** Key in an ssh config file. */ + public static final String PORT = "Port"; + + /** Key in an ssh config file. */ + public static final String PREFERRED_AUTHENTICATIONS = "PreferredAuthentications"; + + /** Key in an ssh config file. */ + public static final String PROXY_COMMAND = "ProxyCommand"; + + /** Key in an ssh config file. */ + public static final String REMOTE_COMMAND = "RemoteCommand"; + + /** Key in an ssh config file. */ + public static final String REMOTE_FORWARD = "RemoteForward"; + + /** Key in an ssh config file. */ + public static final String SEND_ENV = "SendEnv"; + + /** Key in an ssh config file. */ + public static final String STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking"; + + /** Key in an ssh config file. */ + public static final String USER = "User"; + + /** Key in an ssh config file. */ + public static final String USER_KNOWN_HOSTS_FILE = "UserKnownHostsFile"; + + // Values + + /** Flag value. */ + public static final String YES = "yes"; + + /** Flag value. */ + public static final String ON = "on"; + + /** Flag value. */ + public static final String TRUE = "true"; + + /** Flag value. */ + public static final String NO = "no"; + + /** Flag value. */ + public static final String OFF = "off"; + + /** Flag value. */ + public static final String FALSE = "false"; + + // Default identity file names + + /** Name of the default RSA private identity file. */ + public static final String ID_RSA = "id_rsa"; + + /** Name of the default DSA private identity file. */ + public static final String ID_DSA = "id_dsa"; + + /** Name of the default ECDSA private identity file. */ + public static final String ID_ECDSA = "id_ecdsa"; + + /** Name of the default ECDSA private identity file. */ + public static final String ID_ED25519 = "id_ed25519"; + + /** All known default identity file names. */ + public static final String[] DEFAULT_IDENTITIES = { // + ID_RSA, ID_DSA, ID_ECDSA, ID_ED25519 + }; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java index ae357dfb75..005a0c2d0e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java @@ -44,8 +44,13 @@ package org.eclipse.jgit.transport; +import java.security.AccessController; +import java.security.PrivilegedAction; + import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.SystemReader; /** * Creates and destroys SSH connections to a remote system. @@ -88,21 +93,38 @@ public abstract class SshSessionFactory { } /** + * Retrieves the local user name as defined by the system property + * "user.name". + * + * @return the user name + * @since 5.2 + */ + public static String getLocalUserName() { + return AccessController.doPrivileged(new PrivilegedAction<String>() { + @Override + public String run() { + return SystemReader.getInstance() + .getProperty(Constants.OS_USER_NAME_KEY); + } + }); + } + + /** * Open (or reuse) a session to a host. * <p> * A reasonable UserInfo that can interact with the end-user (if necessary) * is installed on the returned session by this method. * <p> - * The caller must connect the session by invoking <code>connect()</code> - * if it has not already been connected. + * The caller must connect the session by invoking <code>connect()</code> if + * it has not already been connected. * * @param uri * URI information about the remote host * @param credentialsProvider * provider to support authentication, may be null. * @param fs - * the file system abstraction which will be necessary to - * perform certain file system operations. + * the file system abstraction which will be necessary to perform + * certain file system operations. * @param tms * Timeout value, in milliseconds. * @return a session that can contact the remote host. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java index db95396047..a3e655cd92 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java @@ -106,7 +106,8 @@ public class TransferConfig { this.name = name; } - static @Nullable ProtocolVersion parse(@Nullable String name) { + @Nullable + static ProtocolVersion parse(@Nullable String name) { if (name == null) { return null; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundle.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundle.java index 6a285e59f5..ee851cc620 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundle.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundle.java @@ -58,5 +58,5 @@ public interface TransportBundle extends PackTransport { /** * Bundle signature */ - public static final String V2_BUNDLE_SIGNATURE = "# v2 git bundle"; //$NON-NLS-1$ + String V2_BUNDLE_SIGNATURE = "# v2 git bundle"; //$NON-NLS-1$ } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java index f129ba34da..5c68308f90 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java @@ -53,13 +53,14 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; @@ -73,12 +74,6 @@ import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.SymbolicRef; -import com.jcraft.jsch.Channel; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.SftpATTRS; -import com.jcraft.jsch.SftpException; - /** * Transport over the non-Git aware SFTP (SSH based FTP) protocol. * <p> @@ -158,24 +153,16 @@ public class TransportSftp extends SshTransport implements WalkTransport { return r; } - ChannelSftp newSftp() throws TransportException { - final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0; - try { - // @TODO: Fix so that this operation is generic and casting to - // JschSession is no longer necessary. - final Channel channel = ((JschSession) getSession()) - .getSftpChannel(); - channel.connect(tms); - return (ChannelSftp) channel; - } catch (JSchException je) { - throw new TransportException(uri, je.getMessage(), je); - } + FtpChannel newSftp() throws IOException { + FtpChannel channel = getSession().getFtpChannel(); + channel.connect(getTimeout(), TimeUnit.SECONDS); + return channel; } class SftpObjectDB extends WalkRemoteObjectDatabase { private final String objectsPath; - private ChannelSftp ftp; + private FtpChannel ftp; SftpObjectDB(String path) throws TransportException { if (path.startsWith("/~")) //$NON-NLS-1$ @@ -187,13 +174,13 @@ public class TransportSftp extends SshTransport implements WalkTransport { ftp.cd(path); ftp.cd("objects"); //$NON-NLS-1$ objectsPath = ftp.pwd(); - } catch (TransportException err) { - close(); - throw err; - } catch (SftpException je) { + } catch (FtpChannel.FtpException f) { throw new TransportException(MessageFormat.format( JGitText.get().cannotEnterObjectsPath, path, - je.getMessage()), je); + f.getMessage()), f); + } catch (IOException ioe) { + close(); + throw new TransportException(uri, ioe.getMessage(), ioe); } } @@ -204,13 +191,13 @@ public class TransportSftp extends SshTransport implements WalkTransport { ftp.cd(parent.objectsPath); ftp.cd(p); objectsPath = ftp.pwd(); - } catch (TransportException err) { - close(); - throw err; - } catch (SftpException je) { + } catch (FtpChannel.FtpException f) { throw new TransportException(MessageFormat.format( JGitText.get().cannotEnterPathFromParent, p, - parent.objectsPath, je.getMessage()), je); + parent.objectsPath, f.getMessage()), f); + } catch (IOException ioe) { + close(); + throw new TransportException(uri, ioe.getMessage(), ioe); } } @@ -238,41 +225,32 @@ public class TransportSftp extends SshTransport implements WalkTransport { Collection<String> getPackNames() throws IOException { final List<String> packs = new ArrayList<>(); try { - @SuppressWarnings("unchecked") - final Collection<ChannelSftp.LsEntry> list = ftp.ls("pack"); //$NON-NLS-1$ - final HashMap<String, ChannelSftp.LsEntry> files; - final HashMap<String, Integer> mtimes; - - files = new HashMap<>(); - mtimes = new HashMap<>(); - - for (ChannelSftp.LsEntry ent : list) - files.put(ent.getFilename(), ent); - for (ChannelSftp.LsEntry ent : list) { - final String n = ent.getFilename(); - if (!n.startsWith("pack-") || !n.endsWith(".pack")) //$NON-NLS-1$ //$NON-NLS-2$ + Collection<FtpChannel.DirEntry> list = ftp.ls("pack"); //$NON-NLS-1$ + Set<String> files = list.stream() + .map(FtpChannel.DirEntry::getFilename) + .collect(Collectors.toSet()); + HashMap<String, Long> mtimes = new HashMap<>(); + + for (FtpChannel.DirEntry ent : list) { + String n = ent.getFilename(); + if (!n.startsWith("pack-") || !n.endsWith(".pack")) { //$NON-NLS-1$ //$NON-NLS-2$ continue; - - final String in = n.substring(0, n.length() - 5) + ".idx"; //$NON-NLS-1$ - if (!files.containsKey(in)) + } + String in = n.substring(0, n.length() - 5) + ".idx"; //$NON-NLS-1$ + if (!files.contains(in)) { continue; - - mtimes.put(n, Integer.valueOf(ent.getAttrs().getMTime())); + } + mtimes.put(n, Long.valueOf(ent.getModifiedTime())); packs.add(n); } - Collections.sort(packs, new Comparator<String>() { - @Override - public int compare(String o1, String o2) { - return mtimes.get(o2).intValue() - - mtimes.get(o1).intValue(); - } - }); - } catch (SftpException je) { + Collections.sort(packs, + (o1, o2) -> mtimes.get(o2).compareTo(mtimes.get(o1))); + } catch (FtpChannel.FtpException f) { throw new TransportException( MessageFormat.format(JGitText.get().cannotListPackPath, - objectsPath, je.getMessage()), - je); + objectsPath, f.getMessage()), + f); } return packs; } @@ -280,27 +258,25 @@ public class TransportSftp extends SshTransport implements WalkTransport { @Override FileStream open(String path) throws IOException { try { - final SftpATTRS a = ftp.lstat(path); - return new FileStream(ftp.get(path), a.getSize()); - } catch (SftpException je) { - if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) + return new FileStream(ftp.get(path)); + } catch (FtpChannel.FtpException f) { + if (f.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) { throw new FileNotFoundException(path); + } throw new TransportException(MessageFormat.format( JGitText.get().cannotGetObjectsPath, objectsPath, path, - je.getMessage()), je); + f.getMessage()), f); } } @Override void deleteFile(String path) throws IOException { try { - ftp.rm(path); - } catch (SftpException je) { - if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) - return; + ftp.delete(path); + } catch (FtpChannel.FtpException f) { throw new TransportException(MessageFormat.format( JGitText.get().cannotDeleteObjectsPath, objectsPath, - path, je.getMessage()), je); + path, f.getMessage()), f); } // Prune any now empty directories. @@ -312,7 +288,7 @@ public class TransportSftp extends SshTransport implements WalkTransport { dir = dir.substring(0, s); ftp.rmdir(dir); s = dir.lastIndexOf('/'); - } catch (SftpException je) { + } catch (IOException je) { // If we cannot delete it, leave it alone. It may have // entries still in it, or maybe we lack write access on // the parent. Either way it isn't a fatal error. @@ -323,25 +299,31 @@ public class TransportSftp extends SshTransport implements WalkTransport { } @Override - OutputStream writeFile(final String path, - final ProgressMonitor monitor, final String monitorTask) - throws IOException { + OutputStream writeFile(String path, ProgressMonitor monitor, + String monitorTask) throws IOException { + Throwable err = null; try { return ftp.put(path); - } catch (SftpException je) { - if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + } catch (FileNotFoundException e) { + mkdir_p(path); + } catch (FtpChannel.FtpException je) { + if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) { mkdir_p(path); - try { - return ftp.put(path); - } catch (SftpException je2) { - je = je2; - } + } else { + err = je; } - - throw new TransportException(MessageFormat.format( - JGitText.get().cannotWriteObjectsPath, objectsPath, - path, je.getMessage()), je); } + if (err == null) { + try { + return ftp.put(path); + } catch (IOException e) { + err = e; + } + } + throw new TransportException( + MessageFormat.format(JGitText.get().cannotWriteObjectsPath, + objectsPath, path, err.getMessage()), + err); } @Override @@ -351,15 +333,15 @@ public class TransportSftp extends SshTransport implements WalkTransport { super.writeFile(lock, data); try { ftp.rename(lock, path); - } catch (SftpException je) { + } catch (IOException e) { throw new TransportException(MessageFormat.format( JGitText.get().cannotWriteObjectsPath, objectsPath, - path, je.getMessage()), je); + path, e.getMessage()), e); } } catch (IOException err) { try { ftp.rm(lock); - } catch (SftpException e) { + } catch (IOException e) { // Ignore deletion failure, we are already // failing anyway. } @@ -373,23 +355,30 @@ public class TransportSftp extends SshTransport implements WalkTransport { return; path = path.substring(0, s); + Throwable err = null; try { ftp.mkdir(path); - } catch (SftpException je) { - if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + return; + } catch (FileNotFoundException f) { + mkdir_p(path); + } catch (FtpChannel.FtpException je) { + if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) { mkdir_p(path); - try { - ftp.mkdir(path); - return; - } catch (SftpException je2) { - je = je2; - } + } else { + err = je; } - - throw new TransportException(MessageFormat.format( - JGitText.get().cannotMkdirObjectPath, objectsPath, path, - je.getMessage()), je); } + if (err == null) { + try { + ftp.mkdir(path); + return; + } catch (IOException e) { + err = e; + } + } + throw new TransportException(MessageFormat.format( + JGitText.get().cannotMkdirObjectPath, objectsPath, path, + err.getMessage()), err); } Map<String, Ref> readAdvertisedRefs() throws TransportException { @@ -400,34 +389,33 @@ public class TransportSftp extends SshTransport implements WalkTransport { return avail; } - @SuppressWarnings("unchecked") - private void readLooseRefs(final TreeMap<String, Ref> avail, - final String dir, final String prefix) - throws TransportException { - final Collection<ChannelSftp.LsEntry> list; + private void readLooseRefs(TreeMap<String, Ref> avail, String dir, + String prefix) throws TransportException { + final Collection<FtpChannel.DirEntry> list; try { list = ftp.ls(dir); - } catch (SftpException je) { + } catch (IOException e) { throw new TransportException(MessageFormat.format( JGitText.get().cannotListObjectsPath, objectsPath, dir, - je.getMessage()), je); + e.getMessage()), e); } - for (ChannelSftp.LsEntry ent : list) { - final String n = ent.getFilename(); + for (FtpChannel.DirEntry ent : list) { + String n = ent.getFilename(); if (".".equals(n) || "..".equals(n)) //$NON-NLS-1$ //$NON-NLS-2$ continue; - final String nPath = dir + "/" + n; //$NON-NLS-1$ - if (ent.getAttrs().isDir()) + String nPath = dir + "/" + n; //$NON-NLS-1$ + if (ent.isDirectory()) { readLooseRefs(avail, nPath, prefix + n + "/"); //$NON-NLS-1$ - else + } else { readRef(avail, nPath, prefix + n); + } } } - private Ref readRef(final TreeMap<String, Ref> avail, - final String path, final String name) throws TransportException { + private Ref readRef(TreeMap<String, Ref> avail, String path, + String name) throws TransportException { final String line; try (BufferedReader br = openReader(path)) { line = br.readLine(); @@ -439,10 +427,10 @@ public class TransportSftp extends SshTransport implements WalkTransport { err.getMessage()), err); } - if (line == null) + if (line == null) { throw new TransportException( MessageFormat.format(JGitText.get().emptyRef, name)); - + } if (line.startsWith("ref: ")) { //$NON-NLS-1$ final String target = line.substring("ref: ".length()); //$NON-NLS-1$ Ref r = avail.get(target); @@ -467,8 +455,9 @@ public class TransportSftp extends SshTransport implements WalkTransport { } private Storage loose(Ref r) { - if (r != null && r.getStorage() == Storage.PACKED) + if (r != null && r.getStorage() == Storage.PACKED) { return Storage.LOOSE_PACKED; + } return Storage.LOOSE; } @@ -476,8 +465,9 @@ public class TransportSftp extends SshTransport implements WalkTransport { void close() { if (ftp != null) { try { - if (ftp.isConnected()) + if (ftp.isConnected()) { ftp.disconnect(); + } } finally { ftp = null; } 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 48a3e0b38f..2fbcaa2928 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -49,6 +49,7 @@ import static org.eclipse.jgit.lib.Constants.R_TAGS; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_REF_IN_WANT; import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_FETCH; import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_LS_REFS; +import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SERVER_OPTION; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_ALLOW_REACHABLE_SHA1_IN_WANT; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_ALLOW_TIP_SHA1_IN_WANT; @@ -70,11 +71,11 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -88,6 +89,7 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.PackWriter; +import org.eclipse.jgit.internal.transport.parser.FirstWant; import org.eclipse.jgit.lib.BitmapIndex; import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder; import org.eclipse.jgit.lib.Constants; @@ -96,6 +98,7 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.AsyncRevObjectQueue; import org.eclipse.jgit.revwalk.BitmapWalker; @@ -179,44 +182,53 @@ public class UploadPack { throws PackProtocolException, IOException; } - /** Data in the first line of a request, the line itself plus options. */ + /** + * Data in the first line of a want-list, the line itself plus options. + * + * @deprecated Use {@link FirstWant} instead + */ + @Deprecated public static class FirstLine { - private final String line; - private final Set<String> options; + + private final FirstWant firstWant; /** - * Parse the first line of a receive-pack request. - * * @param line * line from the client. */ public FirstLine(String line) { - if (line.length() > 45) { - final HashSet<String> opts = new HashSet<>(); - String opt = line.substring(45); - if (opt.startsWith(" ")) //$NON-NLS-1$ - opt = opt.substring(1); - for (String c : opt.split(" ")) //$NON-NLS-1$ - opts.add(c); - this.line = line.substring(0, 45); - this.options = Collections.unmodifiableSet(opts); - } else { - this.line = line; - this.options = Collections.emptySet(); + try { + firstWant = FirstWant.fromLine(line); + } catch (PackProtocolException e) { + throw new UncheckedIOException(e); } } /** @return non-capabilities part of the line. */ public String getLine() { - return line; + return firstWant.getLine(); } - /** @return options parsed from the line. */ + /** @return capabilities parsed from the line. */ public Set<String> getOptions() { - return options; + if (firstWant.getAgent() != null) { + Set<String> caps = new HashSet<>(firstWant.getCapabilities()); + caps.add(OPTION_AGENT + '=' + firstWant.getAgent()); + return caps; + } + return firstWant.getCapabilities(); } } + /* + * {@link java.util.function.Consumer} doesn't allow throwing checked + * exceptions. Define our own to propagate IOExceptions. + */ + @FunctionalInterface + private static interface IOConsumer<R> { + void accept(R t) throws IOException; + } + /** Database we read the objects from. */ private final Repository db; @@ -288,12 +300,11 @@ public class UploadPack { /** Hook for taking post upload actions. */ private PostUploadHook postUploadHook = PostUploadHook.NULL; - /** Capabilities requested by the client. */ - private Set<String> options; + /** Caller user agent */ String userAgent; /** Raw ObjectIds the client has asked for, before validating them. */ - private final Set<ObjectId> wantIds = new HashSet<>(); + private Set<ObjectId> wantIds = new HashSet<>(); /** Objects the client wants to obtain. */ private final Set<RevObject> wantAll = new HashSet<>(); @@ -301,25 +312,6 @@ public class UploadPack { /** Objects on both sides, these don't have to be sent. */ private final Set<RevObject> commonBase = new HashSet<>(); - /** Shallow commits the client already has. */ - private Set<ObjectId> clientShallowCommits = new HashSet<>(); - - /** Desired depth from the client on a shallow request. */ - private int depth; - - /** - * Commit time of the newest objects the client has asked us using - * --shallow-since not to send. Cannot be nonzero if depth is nonzero. - */ - private int shallowSince; - - /** - * (Possibly short) ref names, ancestors of which the client has asked us - * not to send using --shallow-exclude. Cannot be non-empty if depth is - * nonzero. - */ - private List<String> deepenNotRefs = new ArrayList<>(); - /** Commit time of the oldest common commit, in seconds. */ private int oldestTime; @@ -353,7 +345,14 @@ public class UploadPack { private PackStatistics statistics; - private long filterBlobLimit = -1; + /** + * Request this instance is handling. + * + * We need to keep a reference to it for {@link PreUploadHook pre upload + * hooks}. They receive a reference this instance and invoke methods like + * getDepth() to get information about the request. + */ + private FetchRequest currentRequest; /** * Create a new pack upload for an open repository. @@ -695,10 +694,12 @@ public class UploadPack { * read. */ public boolean isSideBand() throws RequestNotYetReadException { - if (options == null) + if (currentRequest == null) { throw new RequestNotYetReadException(); - return (options.contains(OPTION_SIDE_BAND) - || options.contains(OPTION_SIDE_BAND_64K)); + } + Set<String> caps = currentRequest.getClientCapabilities(); + return caps.contains(OPTION_SIDE_BAND) + || caps.contains(OPTION_SIDE_BAND_64K); } /** @@ -829,12 +830,10 @@ public class UploadPack { } if (refs == null) { // Fast path: the advertised refs hook did not set advertised refs. - Map<String, Ref> rs = new HashMap<>(); - for (String p : refPrefixes) { - for (Ref r : db.getRefDatabase().getRefsByPrefix(p)) { - rs.put(r.getName(), r); - } - } + String[] prefixes = refPrefixes.toArray(new String[0]); + Map<String, Ref> rs = + db.getRefDatabase().getRefsByPrefix(prefixes).stream() + .collect(toMap(Ref::getName, identity(), (a, b) -> b)); if (refFilter != RefFilter.DEFAULT) { return refFilter.filter(rs); } @@ -880,12 +879,45 @@ public class UploadPack { return getAdvertisedOrDefaultRefs().get(name); } + /** + * Find a ref in the usual search path on behalf of the client. + * <p> + * This checks that the ref is present in the ref advertisement since + * otherwise the client might not be supposed to be able to read it. + * + * @param name + * short name of the ref to find, e.g. "master" to find + * "refs/heads/master". + * @return the requested Ref, or {@code null} if it is not visible or + * does not exist. + * @throws java.io.IOException + * on failure to read the ref or check it for visibility. + */ + @Nullable + private Ref findRef(String name) throws IOException { + if (refs != null) { + return RefDatabase.findRef(refs, name); + } + if (!advertiseRefsHookCalled) { + advertiseRefsHook.advertiseRefs(this); + advertiseRefsHookCalled = true; + } + if (refs == null && + refFilter == RefFilter.DEFAULT && + transferConfig.hasDefaultRefFilter()) { + // Fast path: no ref filtering is needed. + return db.getRefDatabase().getRef(name); + } + return RefDatabase.findRef(getAdvertisedOrDefaultRefs(), name); + } + private void service() throws IOException { boolean sendPack = false; // If it's a non-bidi request, we need to read the entire request before // writing a response. Buffer the response until then. PackStatistics.Accumulator accumulator = new PackStatistics.Accumulator(); List<ObjectId> unshallowCommits = new ArrayList<>(); + FetchRequest req; try { if (biDirectionalPipe) sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut)); @@ -896,29 +928,46 @@ public class UploadPack { long negotiateStart = System.currentTimeMillis(); accumulator.advertised = advertised.size(); - recvWants(); - if (wantIds.isEmpty()) { - preUploadHook.onBeginNegotiateRound(this, wantIds, 0); - preUploadHook.onEndNegotiateRound(this, wantIds, 0, 0, false); + + ProtocolV0Parser parser = new ProtocolV0Parser(transferConfig); + req = parser.recvWants(pckIn); + currentRequest = req; + + wantIds = req.getWantIds(); + + if (req.getWantIds().isEmpty()) { + preUploadHook.onBeginNegotiateRound(this, req.getWantIds(), 0); + preUploadHook.onEndNegotiateRound(this, req.getWantIds(), 0, 0, + false); return; } - accumulator.wants = wantIds.size(); + accumulator.wants = req.getWantIds().size(); - if (options.contains(OPTION_MULTI_ACK_DETAILED)) { + if (req.getClientCapabilities().contains(OPTION_MULTI_ACK_DETAILED)) { multiAck = MultiAck.DETAILED; - noDone = options.contains(OPTION_NO_DONE); - } else if (options.contains(OPTION_MULTI_ACK)) + noDone = req.getClientCapabilities().contains(OPTION_NO_DONE); + } else if (req.getClientCapabilities().contains(OPTION_MULTI_ACK)) multiAck = MultiAck.CONTINUE; else multiAck = MultiAck.OFF; - if (!clientShallowCommits.isEmpty()) - verifyClientShallow(clientShallowCommits); - if (depth != 0) - processShallow(null, unshallowCommits, true); - if (!clientShallowCommits.isEmpty()) - walk.assumeShallow(clientShallowCommits); - sendPack = negotiate(accumulator); + if (!req.getClientShallowCommits().isEmpty()) { + verifyClientShallow(req.getClientShallowCommits()); + } + + if (req.getDepth() != 0 || req.getDeepenSince() != 0) { + computeShallowsAndUnshallows(req, shallow -> { + pckOut.writeString("shallow " + shallow.name() + '\n'); //$NON-NLS-1$ + }, unshallow -> { + pckOut.writeString("unshallow " + unshallow.name() + '\n'); //$NON-NLS-1$ + unshallowCommits.add(unshallow); + }, Collections.emptyList()); + pckOut.end(); + } + + if (!req.getClientShallowCommits().isEmpty()) + walk.assumeShallow(req.getClientShallowCommits()); + sendPack = negotiate(req, accumulator); accumulator.timeNegotiating += System.currentTimeMillis() - negotiateStart; @@ -968,35 +1017,14 @@ public class UploadPack { } if (sendPack) { - sendPack(accumulator, refs == null ? null : refs.values(), unshallowCommits); + sendPack(accumulator, req, refs == null ? null : refs.values(), + unshallowCommits, Collections.emptyList()); } } private void lsRefsV2() throws IOException { - LsRefsV2Request.Builder builder = LsRefsV2Request.builder(); - List<String> prefixes = new ArrayList<>(); - String line = pckIn.readString(); - // Currently, we do not support any capabilities, so the next - // line is DELIM if there are arguments or END if not. - if (line == PacketLineIn.DELIM) { - while ((line = pckIn.readString()) != PacketLineIn.END) { - if (line.equals("peel")) { //$NON-NLS-1$ - builder.setPeel(true); - } else if (line.equals("symrefs")) { //$NON-NLS-1$ - builder.setSymrefs(true); - } else if (line.startsWith("ref-prefix ")) { //$NON-NLS-1$ - prefixes.add(line.substring("ref-prefix ".length())); //$NON-NLS-1$ - } else { - throw new PackProtocolException(MessageFormat - .format(JGitText.get().unexpectedPacketLine, line)); - } - } - } else if (line != PacketLineIn.END) { - throw new PackProtocolException(MessageFormat - .format(JGitText.get().unexpectedPacketLine, line)); - } - LsRefsV2Request req = builder.setRefPrefixes(prefixes).build(); - + ProtocolV2Parser parser = new ProtocolV2Parser(transferConfig); + LsRefsV2Request req = parser.parseLsRefsRequest(pckIn); protocolV2Hook.onLsRefs(req); rawOut.stopBuffering(); @@ -1029,20 +1057,23 @@ public class UploadPack { ProtocolV2Parser parser = new ProtocolV2Parser(transferConfig); FetchV2Request req = parser.parseFetchRequest(pckIn); + currentRequest = req; rawOut.stopBuffering(); protocolV2Hook.onFetch(req); // TODO(ifrade): Refactor to pass around the Request object, instead of // copying data back to class fields - options = req.getOptions(); - clientShallowCommits = req.getClientShallowCommits(); - depth = req.getDepth(); - shallowSince = req.getDeepenSince(); - filterBlobLimit = req.getFilterBlobLimit(); - deepenNotRefs = req.getDeepenNotRefs(); - - wantIds.addAll(req.getWantsIds()); + 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()); + } + Map<String, ObjectId> wantedRefs = new TreeMap<>(); for (String refName : req.getWantedRefs()) { Ref ref = getRef(refName); @@ -1055,21 +1086,27 @@ public class UploadPack { throw new PackProtocolException(MessageFormat .format(JGitText.get().invalidRefName, refName)); } - wantIds.add(oid); + // TODO(ifrade): Avoid mutating the parsed request. + req.getWantIds().add(oid); wantedRefs.put(refName, oid); } + wantIds = req.getWantIds(); boolean sectionSent = false; - @Nullable List<ObjectId> shallowCommits = null; + boolean mayHaveShallow = req.getDepth() != 0 + || req.getDeepenSince() != 0 + || !req.getDeepenNotRefs().isEmpty(); + List<ObjectId> shallowCommits = new ArrayList<>(); List<ObjectId> unshallowCommits = new ArrayList<>(); if (!req.getClientShallowCommits().isEmpty()) { verifyClientShallow(req.getClientShallowCommits()); } - if (req.getDepth() != 0 || req.getDeepenSince() != 0 - || !req.getDeepenNotRefs().isEmpty()) { - shallowCommits = new ArrayList<>(); - processShallow(shallowCommits, unshallowCommits, false); + if (mayHaveShallow) { + computeShallowsAndUnshallows(req, + shallowCommit -> shallowCommits.add(shallowCommit), + unshallowCommit -> unshallowCommits.add(unshallowCommit), + deepenNots); } if (!req.getClientShallowCommits().isEmpty()) walk.assumeShallow(req.getClientShallowCommits()); @@ -1095,7 +1132,7 @@ public class UploadPack { } if (req.wasDoneReceived() || okToGiveUp()) { - if (shallowCommits != null) { + if (mayHaveShallow) { if (sectionSent) pckOut.writeDelim(); pckOut.writeString("shallow-info\n"); //$NON-NLS-1$ @@ -1125,10 +1162,11 @@ public class UploadPack { pckOut.writeDelim(); pckOut.writeString("packfile\n"); //$NON-NLS-1$ sendPack(new PackStatistics.Accumulator(), - req.getOptions().contains(OPTION_INCLUDE_TAG) + req, + req.getClientCapabilities().contains(OPTION_INCLUDE_TAG) ? db.getRefDatabase().getRefsByPrefix(R_TAGS) : null, - unshallowCommits); + unshallowCommits, deepenNots); // sendPack invokes pckOut.end() for us, so we do not // need to invoke it here. } else { @@ -1179,6 +1217,7 @@ public class UploadPack { (transferConfig.isAllowFilter() ? OPTION_FILTER + ' ' : "") + //$NON-NLS-1$ (advertiseRefInWant ? CAPABILITY_REF_IN_WANT + ' ' : "") + //$NON-NLS-1$ OPTION_SHALLOW); + caps.add(CAPABILITY_SERVER_OPTION); return caps; } @@ -1227,28 +1266,28 @@ public class UploadPack { } /* - * Determines what "shallow" and "unshallow" lines to send to the user. - * The information is written to shallowCommits (if not null) and - * unshallowCommits, and also written to #pckOut (if writeToPckOut is - * true). + * Determines what object ids must be marked as shallow or unshallow for the + * client. */ - private void processShallow(@Nullable List<ObjectId> shallowCommits, - List<ObjectId> unshallowCommits, - boolean writeToPckOut) throws IOException { - if (options.contains(OPTION_DEEPEN_RELATIVE) || - shallowSince != 0 || - !deepenNotRefs.isEmpty()) { - // TODO(jonathantanmy): Implement deepen-relative, deepen-since, - // and deepen-not. + private void computeShallowsAndUnshallows(FetchRequest req, + IOConsumer<ObjectId> shallowFunc, + IOConsumer<ObjectId> unshallowFunc, + List<ObjectId> deepenNots) + throws IOException { + if (req.getClientCapabilities().contains(OPTION_DEEPEN_RELATIVE)) { + // TODO(jonathantanmy): Implement deepen-relative throw new UnsupportedOperationException(); } - int walkDepth = depth - 1; + int walkDepth = req.getDepth() == 0 ? Integer.MAX_VALUE + : req.getDepth() - 1; try (DepthWalk.RevWalk depthWalk = new DepthWalk.RevWalk( walk.getObjectReader(), walkDepth)) { + depthWalk.setDeepenSince(req.getDeepenSince()); + // Find all the commits which will be shallow - for (ObjectId o : wantIds) { + for (ObjectId o : req.getWantIds()) { try { depthWalk.markRoot(depthWalk.parseCommit(o)); } catch (IncorrectObjectTypeException notCommit) { @@ -1256,35 +1295,32 @@ public class UploadPack { } } + depthWalk.setDeepenNots(deepenNots); + RevCommit o; + boolean atLeastOne = false; while ((o = depthWalk.next()) != null) { DepthWalk.Commit c = (DepthWalk.Commit) o; + atLeastOne = true; + + boolean isBoundary = (c.getDepth() == walkDepth) || c.isBoundary(); // Commits at the boundary which aren't already shallow in // the client need to be marked as such - if (c.getDepth() == walkDepth - && !clientShallowCommits.contains(c)) { - if (shallowCommits != null) { - shallowCommits.add(c.copy()); - } - if (writeToPckOut) { - pckOut.writeString("shallow " + o.name()); //$NON-NLS-1$ - } + if (isBoundary && !req.getClientShallowCommits().contains(c)) { + shallowFunc.accept(c.copy()); } // Commits not on the boundary which are shallow in the client // need to become unshallowed - if (c.getDepth() < walkDepth - && clientShallowCommits.remove(c)) { - unshallowCommits.add(c.copy()); - if (writeToPckOut) { - pckOut.writeString("unshallow " + c.name()); //$NON-NLS-1$ - } + if (!isBoundary && req.getClientShallowCommits().remove(c)) { + unshallowFunc.accept(c.copy()); } } - } - if (writeToPckOut) { - pckOut.end(); + if (!atLeastOne) { + throw new PackProtocolException( + JGitText.get().noCommitsSelectedForShallow); + } } } @@ -1436,67 +1472,6 @@ public class UploadPack { return msgOut; } - private void recvWants() throws IOException { - boolean isFirst = true; - boolean filterReceived = false; - for (;;) { - String line; - try { - line = pckIn.readString(); - } catch (EOFException eof) { - if (isFirst) - break; - throw eof; - } - - if (line == PacketLineIn.END) - break; - - if (line.startsWith("deepen ")) { //$NON-NLS-1$ - depth = Integer.parseInt(line.substring(7)); - if (depth <= 0) { - throw new PackProtocolException( - MessageFormat.format(JGitText.get().invalidDepth, - Integer.valueOf(depth))); - } - continue; - } - - if (line.startsWith("shallow ")) { //$NON-NLS-1$ - clientShallowCommits.add(ObjectId.fromString(line.substring(8))); - continue; - } - - if (transferConfig.isAllowFilter() - && line.startsWith(OPTION_FILTER + " ")) { //$NON-NLS-1$ - String arg = line.substring(OPTION_FILTER.length() + 1); - - if (filterReceived) { - throw new PackProtocolException(JGitText.get().tooManyFilters); - } - filterReceived = true; - - filterBlobLimit = ProtocolV2Parser.filterLine(arg); - continue; - } - - if (!line.startsWith("want ") || line.length() < 45) //$NON-NLS-1$ - throw new PackProtocolException(MessageFormat.format(JGitText.get().expectedGot, "want", line)); //$NON-NLS-1$ - - if (isFirst) { - if (line.length() > 45) { - FirstLine firstLine = new FirstLine(line); - options = firstLine.getOptions(); - line = firstLine.getLine(); - } else - options = Collections.emptySet(); - } - - wantIds.add(ObjectId.fromString(line.substring(5))); - isFirst = false; - } - } - /** * Returns the clone/fetch depth. Valid only after calling recvWants(). A * depth of 1 means return only the wants. @@ -1505,9 +1480,9 @@ public class UploadPack { * @since 4.0 */ public int getDepth() { - if (options == null) + if (currentRequest == null) throw new RequestNotYetReadException(); - return depth; + return currentRequest.getDepth(); } /** @@ -1526,10 +1501,15 @@ public class UploadPack { * @since 4.0 */ public String getPeerUserAgent() { - return UserAgent.getAgent(options, userAgent); + if (currentRequest != null && currentRequest.getAgent() != null) { + return currentRequest.getAgent(); + } + + return userAgent; } - private boolean negotiate(PackStatistics.Accumulator accumulator) + private boolean negotiate(FetchRequest req, + PackStatistics.Accumulator accumulator) throws IOException { okToGiveUp = Boolean.FALSE; @@ -1545,7 +1525,7 @@ public class UploadPack { // disconnected, and will try another request with actual want/have. // Don't report the EOF here, its a bug in the protocol that the client // just disconnects without sending an END. - if (!biDirectionalPipe && depth > 0) + if (!biDirectionalPipe && req.getDepth() > 0) return false; throw eof; } @@ -1926,25 +1906,31 @@ public class UploadPack { * Send the requested objects to the client. * * @param accumulator - * where to write statistics about the content of the pack. + * where to write statistics about the content of the pack. + * @param req + * request in process * @param allTags - * refs to search for annotated tags to include in the pack - * if the {@link #OPTION_INCLUDE_TAG} capability was - * requested. + * refs to search for annotated tags to include in the pack if + * the {@link #OPTION_INCLUDE_TAG} capability was requested. * @param unshallowCommits - * shallow commits on the client that are now becoming - * unshallow + * shallow commits on the client that are now becoming unshallow + * @param deepenNots + * objects that the client specified using --shallow-exclude * @throws IOException - * if an error occured while generating or writing the pack. + * if an error occurred while generating or writing the pack. */ private void sendPack(PackStatistics.Accumulator accumulator, + FetchRequest req, @Nullable Collection<Ref> allTags, - List<ObjectId> unshallowCommits) throws IOException { - final boolean sideband = options.contains(OPTION_SIDE_BAND) - || options.contains(OPTION_SIDE_BAND_64K); + List<ObjectId> unshallowCommits, + List<ObjectId> deepenNots) throws IOException { + Set<String> caps = req.getClientCapabilities(); + boolean sideband = caps.contains(OPTION_SIDE_BAND) + || caps.contains(OPTION_SIDE_BAND_64K); if (sideband) { try { - sendPack(true, accumulator, allTags, unshallowCommits); + sendPack(true, req, accumulator, allTags, unshallowCommits, + deepenNots); } catch (ServiceMayNotContinueException noPack) { // This was already reported on (below). throw noPack; @@ -1965,7 +1951,7 @@ public class UploadPack { throw err; } } else { - sendPack(false, accumulator, allTags, unshallowCommits); + sendPack(false, req, accumulator, allTags, unshallowCommits, deepenNots); } } @@ -1989,35 +1975,39 @@ public class UploadPack { * Send the requested objects to the client. * * @param sideband - * whether to wrap the pack in side-band pkt-lines, - * interleaved with progress messages and errors. + * whether to wrap the pack in side-band pkt-lines, interleaved + * with progress messages and errors. + * @param req + * request being processed * @param accumulator - * where to write statistics about the content of the pack. + * where to write statistics about the content of the pack. * @param allTags - * refs to search for annotated tags to include in the pack - * if the {@link #OPTION_INCLUDE_TAG} capability was - * requested. + * refs to search for annotated tags to include in the pack if + * the {@link #OPTION_INCLUDE_TAG} capability was requested. * @param unshallowCommits - * shallow commits on the client that are now becoming - * unshallow + * shallow commits on the client that are now becoming unshallow + * @param deepenNots + * objects that the client specified using --shallow-exclude * @throws IOException - * if an error occured while generating or writing the pack. + * if an error occurred while generating or writing the pack. */ private void sendPack(final boolean sideband, + FetchRequest req, PackStatistics.Accumulator accumulator, @Nullable Collection<Ref> allTags, - List<ObjectId> unshallowCommits) throws IOException { + List<ObjectId> unshallowCommits, + List<ObjectId> deepenNots) throws IOException { ProgressMonitor pm = NullProgressMonitor.INSTANCE; OutputStream packOut = rawOut; if (sideband) { int bufsz = SideBandOutputStream.SMALL_BUF; - if (options.contains(OPTION_SIDE_BAND_64K)) + if (req.getClientCapabilities().contains(OPTION_SIDE_BAND_64K)) bufsz = SideBandOutputStream.MAX_BUF; packOut = new SideBandOutputStream(SideBandOutputStream.CH_DATA, bufsz, rawOut); - if (!options.contains(OPTION_NO_PROGRESS)) { + if (!req.getClientCapabilities().contains(OPTION_NO_PROGRESS)) { msgOut = new SideBandOutputStream( SideBandOutputStream.CH_PROGRESS, bufsz, rawOut); pm = new SideBandProgressMonitor(msgOut); @@ -2053,17 +2043,20 @@ public class UploadPack { accumulator); try { pw.setIndexDisabled(true); - if (filterBlobLimit >= 0) { - pw.setFilterBlobLimit(filterBlobLimit); + if (req.getFilterBlobLimit() >= 0) { + pw.setFilterBlobLimit(req.getFilterBlobLimit()); pw.setUseCachedPacks(false); } else { pw.setUseCachedPacks(true); } - pw.setUseBitmaps(depth == 0 && clientShallowCommits.isEmpty()); - pw.setClientShallowCommits(clientShallowCommits); + pw.setUseBitmaps( + req.getDepth() == 0 + && req.getClientShallowCommits().isEmpty()); + pw.setClientShallowCommits(req.getClientShallowCommits()); pw.setReuseDeltaCommits(true); - pw.setDeltaBaseAsOffset(options.contains(OPTION_OFS_DELTA)); - pw.setThin(options.contains(OPTION_THIN_PACK)); + pw.setDeltaBaseAsOffset( + req.getClientCapabilities().contains(OPTION_OFS_DELTA)); + pw.setThin(req.getClientCapabilities().contains(OPTION_THIN_PACK)); pw.setReuseValidatingObjects(false); // Objects named directly by references go at the beginning @@ -2082,14 +2075,22 @@ public class UploadPack { } RevWalk rw = walk; - if (depth > 0) { - pw.setShallowPack(depth, unshallowCommits); - rw = new DepthWalk.RevWalk(walk.getObjectReader(), depth - 1); - rw.assumeShallow(clientShallowCommits); + if (req.getDepth() > 0 || req.getDeepenSince() != 0 || !deepenNots.isEmpty()) { + int walkDepth = req.getDepth() == 0 ? Integer.MAX_VALUE + : req.getDepth() - 1; + pw.setShallowPack(req.getDepth(), unshallowCommits); + + DepthWalk.RevWalk dw = new DepthWalk.RevWalk( + walk.getObjectReader(), walkDepth); + dw.setDeepenSince(req.getDeepenSince()); + dw.setDeepenNots(deepenNots); + dw.assumeShallow(req.getClientShallowCommits()); + rw = dw; } if (wantAll.isEmpty()) { - pw.preparePack(pm, wantIds, commonBase, clientShallowCommits); + pw.preparePack(pm, wantIds, commonBase, + req.getClientShallowCommits()); } else { walk.reset(); @@ -2098,7 +2099,8 @@ public class UploadPack { rw = ow; } - if (options.contains(OPTION_INCLUDE_TAG) && allTags != null) { + if (req.getClientCapabilities().contains(OPTION_INCLUDE_TAG) + && allTags != null) { for (Ref ref : allTags) { ObjectId objectId = ref.getObjectId(); if (objectId == null) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java index d815bc354e..c38b00287b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java @@ -58,6 +58,8 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.TrustManager; +import org.eclipse.jgit.annotations.NonNull; + /** * The interface of connections used during HTTP communication. This interface * is that subset of the interface exposed by {@link java.net.HttpURLConnection} @@ -69,25 +71,25 @@ public interface HttpConnection { /** * @see HttpURLConnection#HTTP_OK */ - public static final int HTTP_OK = java.net.HttpURLConnection.HTTP_OK; + int HTTP_OK = java.net.HttpURLConnection.HTTP_OK; /** * @see HttpURLConnection#HTTP_MOVED_PERM * @since 4.7 */ - public static final int HTTP_MOVED_PERM = java.net.HttpURLConnection.HTTP_MOVED_PERM; + int HTTP_MOVED_PERM = java.net.HttpURLConnection.HTTP_MOVED_PERM; /** * @see HttpURLConnection#HTTP_MOVED_TEMP * @since 4.9 */ - public static final int HTTP_MOVED_TEMP = java.net.HttpURLConnection.HTTP_MOVED_TEMP; + int HTTP_MOVED_TEMP = java.net.HttpURLConnection.HTTP_MOVED_TEMP; /** * @see HttpURLConnection#HTTP_SEE_OTHER * @since 4.9 */ - public static final int HTTP_SEE_OTHER = java.net.HttpURLConnection.HTTP_SEE_OTHER; + int HTTP_SEE_OTHER = java.net.HttpURLConnection.HTTP_SEE_OTHER; /** * HTTP 1.1 additional MOVED_TEMP status code; value = 307. @@ -95,22 +97,22 @@ public interface HttpConnection { * @see #HTTP_MOVED_TEMP * @since 4.9 */ - public static final int HTTP_11_MOVED_TEMP = 307; + int HTTP_11_MOVED_TEMP = 307; /** * @see HttpURLConnection#HTTP_NOT_FOUND */ - public static final int HTTP_NOT_FOUND = java.net.HttpURLConnection.HTTP_NOT_FOUND; + int HTTP_NOT_FOUND = java.net.HttpURLConnection.HTTP_NOT_FOUND; /** * @see HttpURLConnection#HTTP_UNAUTHORIZED */ - public static final int HTTP_UNAUTHORIZED = java.net.HttpURLConnection.HTTP_UNAUTHORIZED; + int HTTP_UNAUTHORIZED = java.net.HttpURLConnection.HTTP_UNAUTHORIZED; /** * @see HttpURLConnection#HTTP_FORBIDDEN */ - public static final int HTTP_FORBIDDEN = java.net.HttpURLConnection.HTTP_FORBIDDEN; + int HTTP_FORBIDDEN = java.net.HttpURLConnection.HTTP_FORBIDDEN; /** * Get response code @@ -119,7 +121,7 @@ public interface HttpConnection { * @return the HTTP Status-Code, or -1 * @throws java.io.IOException */ - public int getResponseCode() throws IOException; + int getResponseCode() throws IOException; /** * Get URL @@ -127,7 +129,7 @@ public interface HttpConnection { * @see HttpURLConnection#getURL() * @return the URL. */ - public URL getURL(); + URL getURL(); /** * Get response message @@ -136,15 +138,15 @@ public interface HttpConnection { * @return the HTTP response message, or <code>null</code> * @throws java.io.IOException */ - public String getResponseMessage() throws IOException; + String getResponseMessage() throws IOException; /** - * Get list of header fields + * Get map of header fields * * @see HttpURLConnection#getHeaderFields() * @return a Map of header fields */ - public Map<String, List<String>> getHeaderFields(); + Map<String, List<String>> getHeaderFields(); /** * Set request property @@ -156,7 +158,7 @@ public interface HttpConnection { * @param value * the value associated with it. */ - public void setRequestProperty(String key, String value); + void setRequestProperty(String key, String value); /** * Set request method @@ -170,7 +172,7 @@ public interface HttpConnection { * @throws java.net.ProtocolException * if any. */ - public void setRequestMethod(String method) + void setRequestMethod(String method) throws ProtocolException; /** @@ -181,7 +183,7 @@ public interface HttpConnection { * a <code>boolean</code> indicating whether or not to allow * caching */ - public void setUseCaches(boolean usecaches); + void setUseCaches(boolean usecaches); /** * Set connect timeout @@ -191,7 +193,7 @@ public interface HttpConnection { * an <code>int</code> that specifies the connect timeout value * in milliseconds */ - public void setConnectTimeout(int timeout); + void setConnectTimeout(int timeout); /** * Set read timeout @@ -201,7 +203,7 @@ public interface HttpConnection { * an <code>int</code> that specifies the timeout value to be * used in milliseconds */ - public void setReadTimeout(int timeout); + void setReadTimeout(int timeout); /** * Get content type @@ -210,7 +212,7 @@ public interface HttpConnection { * @return the content type of the resource that the URL references, or * <code>null</code> if not known. */ - public String getContentType(); + String getContentType(); /** * Get input stream @@ -222,10 +224,16 @@ public interface HttpConnection { * @throws java.io.IOException * if any. */ - public InputStream getInputStream() throws IOException; + InputStream getInputStream() throws IOException; /** - * Get header field + * Get header field. According to + * {@link <a href="https://tools.ietf.org/html/rfc2616#section-4.2">RFC + * 2616</a>} header field names are case insensitive. Header fields defined + * as a comma separated list can have multiple header fields with the same + * field name. This method only returns one of these header fields. If you + * want the union of all values of all multiple header fields with the same + * field name then use {@link #getHeaderFields(String)} * * @see HttpURLConnection#getHeaderField(String) * @param name @@ -233,7 +241,22 @@ public interface HttpConnection { * @return the value of the named header field, or <code>null</code> if * there is no such field in the header. */ - public String getHeaderField(String name); + String getHeaderField(@NonNull String name); + + /** + * Get all values of given header field. According to + * {@link <a href="https://tools.ietf.org/html/rfc2616#section-4.2">RFC + * 2616</a>} header field names are case insensitive. Header fields defined + * as a comma separated list can have multiple header fields with the same + * field name. This method does not validate if the given header field is + * defined as a comma separated list. + * + * @param name + * the name of a header field. + * @return the list of values of the named header field + * @since 5.2 + */ + List<String> getHeaderFields(@NonNull String name); /** * Get content length @@ -243,7 +266,7 @@ public interface HttpConnection { * references, {@code -1} if the content length is not known, or if * the content length is greater than Integer.MAX_VALUE. */ - public int getContentLength(); + int getContentLength(); /** * Set whether or not to follow HTTP redirects. @@ -253,7 +276,7 @@ public interface HttpConnection { * a <code>boolean</code> indicating whether or not to follow * HTTP redirects. */ - public void setInstanceFollowRedirects(boolean followRedirects); + void setInstanceFollowRedirects(boolean followRedirects); /** * Set if to do output @@ -262,7 +285,7 @@ public interface HttpConnection { * @param dooutput * the new value. */ - public void setDoOutput(boolean dooutput); + void setDoOutput(boolean dooutput); /** * Set fixed length streaming mode @@ -271,7 +294,7 @@ public interface HttpConnection { * @param contentLength * The number of bytes which will be written to the OutputStream. */ - public void setFixedLengthStreamingMode(int contentLength); + void setFixedLengthStreamingMode(int contentLength); /** * Get output stream @@ -280,7 +303,7 @@ public interface HttpConnection { * @return an output stream that writes to this connection. * @throws java.io.IOException */ - public OutputStream getOutputStream() throws IOException; + OutputStream getOutputStream() throws IOException; /** * Set chunked streaming mode @@ -290,7 +313,7 @@ public interface HttpConnection { * The number of bytes to write in each chunk. If chunklen is * less than or equal to zero, a default value will be used. */ - public void setChunkedStreamingMode(int chunklen); + void setChunkedStreamingMode(int chunklen); /** * Get request method @@ -298,7 +321,7 @@ public interface HttpConnection { * @see HttpURLConnection#getRequestMethod() * @return the HTTP request method */ - public String getRequestMethod(); + String getRequestMethod(); /** * Whether we use a proxy @@ -306,7 +329,7 @@ public interface HttpConnection { * @see HttpURLConnection#usingProxy() * @return a boolean indicating if the connection is using a proxy. */ - public boolean usingProxy(); + boolean usingProxy(); /** * Connect @@ -314,7 +337,7 @@ public interface HttpConnection { * @see HttpURLConnection#connect() * @throws java.io.IOException */ - public void connect() throws IOException; + void connect() throws IOException; /** * Configure the connection so that it can be used for https communication. @@ -332,7 +355,7 @@ public interface HttpConnection { * @throws java.security.NoSuchAlgorithmException * @throws java.security.KeyManagementException */ - public void configure(KeyManager[] km, TrustManager[] tm, + void configure(KeyManager[] km, TrustManager[] tm, SecureRandom random) throws NoSuchAlgorithmException, KeyManagementException; @@ -345,6 +368,6 @@ public interface HttpConnection { * @throws java.security.NoSuchAlgorithmException * @throws java.security.KeyManagementException */ - public void setHostnameVerifier(HostnameVerifier hostnameverifier) + void setHostnameVerifier(HostnameVerifier hostnameverifier) throws NoSuchAlgorithmException, KeyManagementException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory.java index bd9d61fe66..11691451f2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory.java @@ -62,7 +62,7 @@ public interface HttpConnectionFactory { * @return a {@link org.eclipse.jgit.transport.http.HttpConnection} * @throws java.io.IOException */ - public HttpConnection create(URL url) throws IOException; + HttpConnection create(URL url) throws IOException; /** * Creates a new connection to a destination defined by a @@ -75,6 +75,6 @@ public interface HttpConnectionFactory { * @return a {@link org.eclipse.jgit.transport.http.HttpConnection} * @throws java.io.IOException */ - public HttpConnection create(URL url, Proxy proxy) + HttpConnection create(URL url, Proxy proxy) throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnection.java index 8241c59d2b..734b549294 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnection.java @@ -53,6 +53,7 @@ import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -62,6 +63,7 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; +import org.eclipse.jgit.annotations.NonNull; /** * A {@link org.eclipse.jgit.transport.http.HttpConnection} which simply * delegates every call to a {@link java.net.HttpURLConnection}. This is the @@ -72,6 +74,11 @@ import javax.net.ssl.TrustManager; public class JDKHttpConnection implements HttpConnection { HttpURLConnection wrappedUrlConnection; + // used for mock testing + JDKHttpConnection(HttpURLConnection urlConnection) { + this.wrappedUrlConnection = urlConnection; + } + /** * Constructor for JDKHttpConnection. * @@ -170,10 +177,26 @@ public class JDKHttpConnection implements HttpConnection { /** {@inheritDoc} */ @Override - public String getHeaderField(String name) { + public String getHeaderField(@NonNull String name) { return wrappedUrlConnection.getHeaderField(name); } + @Override + public List<String> getHeaderFields(@NonNull String name) { + Map<String, List<String>> m = wrappedUrlConnection.getHeaderFields(); + List<String> fields = mapValuesToListIgnoreCase(name, m); + return fields; + } + + private static List<String> mapValuesToListIgnoreCase(String keyName, + Map<String, List<String>> m) { + List<String> fields = new LinkedList<>(); + m.entrySet().stream().filter(e -> keyName.equalsIgnoreCase(e.getKey())) + .filter(e -> e.getValue() != null) + .forEach(e -> fields.addAll(e.getValue())); + return fields; + } + /** {@inheritDoc} */ @Override public int getContentLength() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ReceivePackFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ReceivePackFactory.java index 4967169776..b850d1ef94 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ReceivePackFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ReceivePackFactory.java @@ -57,7 +57,7 @@ public interface ReceivePackFactory<C> { /** * A factory disabling the ReceivePack service for all repositories */ - public static final ReceivePackFactory<?> DISABLED = new ReceivePackFactory<Object>() { + ReceivePackFactory<?> DISABLED = new ReceivePackFactory<Object>() { @Override public ReceivePack create(Object req, Repository db) throws ServiceNotEnabledException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/RepositoryResolver.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/RepositoryResolver.java index a305e4cea3..4816f21bcc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/RepositoryResolver.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/RepositoryResolver.java @@ -57,7 +57,7 @@ public interface RepositoryResolver<C> { /** * Resolver configured to open nothing. */ - public static final RepositoryResolver<?> NONE = new RepositoryResolver<Object>() { + RepositoryResolver<?> NONE = new RepositoryResolver<Object>() { @Override public Repository open(Object req, String name) throws RepositoryNotFoundException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/UploadPackFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/UploadPackFactory.java index 40d1ffdc56..bb43b136d8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/UploadPackFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/UploadPackFactory.java @@ -57,7 +57,7 @@ public interface UploadPackFactory<C> { /** * A factory disabling the UploadPack service for all repositories. */ - public static final UploadPackFactory<?> DISABLED = new UploadPackFactory<Object>() { + UploadPackFactory<?> DISABLED = new UploadPackFactory<Object>() { @Override public UploadPack create(Object req, Repository db) throws ServiceNotEnabledException { 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 299f07fb09..ddf916f41f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -1056,7 +1056,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } } if (FileMode.GITLINK == iMode - && FileMode.TREE == wtMode) { + && FileMode.TREE == wtMode && !getOptions().isDirNoGitLinks()) { return iMode; } if (FileMode.TREE == iMode diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java index 19c04619df..10a9919391 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -1696,6 +1696,13 @@ public abstract class FS { hookPath); ProcessBuilder hookProcess = runInShell(cmd, args); hookProcess.directory(runDirectory); + Map<String, String> environment = hookProcess.environment(); + environment.put(Constants.GIT_DIR_KEY, + repository.getDirectory().getAbsolutePath()); + if (!repository.isBare()) { + environment.put(Constants.GIT_WORK_TREE_KEY, + repository.getWorkTree().getAbsolutePath()); + } try { return new ProcessResult(runProcess(hookProcess, outRedirect, errRedirect, stdinArgs), Status.OK); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java index 6d60ef3f4d..96636b7994 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java @@ -152,7 +152,8 @@ public class LfsFactory { * @param outputStream * @return a {@link PrePushHook} implementation or <code>null</code> */ - public @Nullable PrePushHook getPrePushHook(Repository repo, + @Nullable + public PrePushHook getPrePushHook(Repository repo, PrintStream outputStream) { return null; } @@ -163,7 +164,8 @@ public class LfsFactory { * * @return a command to install LFS support. */ - public @Nullable LfsInstallCommand getInstallCommand() { + @Nullable + public LfsInstallCommand getInstallCommand() { return null; } @@ -294,6 +296,11 @@ public class LfsFactory { return stream.read(); } + @Override + public int read(byte b[], int off, int len) throws IOException { + return stream.read(b, off, len); + } + /** * @return the length of the stream */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java index 28f406a49e..a440cb275c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java @@ -677,10 +677,6 @@ public final class RawParseUtils { * <p> * The last element (index <code>map.size()-1</code>) always contains * <code>end</code>. - * <p> - * If the data contains a '\0' anywhere, the whole region is considered - * binary and a LineMap corresponding to a single line is returned. - * </p> * * @param buf * buffer to scan. @@ -689,18 +685,15 @@ public final class RawParseUtils { * line 1. * @param end * 1 past the end of the content within <code>buf</code>. - * @return a line map indicating the starting position of each line, or a - * map representing the entire buffer as a single line if - * <code>buf</code> contains a NUL byte. + * @return a line map indicating the starting position of each line. */ public static final IntList lineMap(byte[] buf, int ptr, int end) { - IntList map = lineMapOrNull(buf, ptr, end); - if (map == null) { - map = new IntList(3); - map.add(Integer.MIN_VALUE); + IntList map = new IntList((end - ptr) / 36); + map.fillTo(1, Integer.MIN_VALUE); + for (; ptr < end; ptr = nextLF(buf, ptr)) { map.add(ptr); - map.add(end); } + map.add(end); return map; } @@ -729,7 +722,8 @@ public final class RawParseUtils { return map; } - private static @Nullable IntList lineMapOrNull(byte[] buf, int ptr, int end) { + @Nullable + private static IntList lineMapOrNull(byte[] buf, int ptr, int end) { // Experimentally derived from multiple source repositories // the average number of bytes/line is 36. Its a rough guess // to initially size our map close to the target. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/EolStreamTypeUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/EolStreamTypeUtil.java index 822961f8de..d7c6bec219 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/EolStreamTypeUtil.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/EolStreamTypeUtil.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com> + * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which @@ -49,6 +50,7 @@ import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.util.SystemReader; /** * Utility used to create input and output stream wrappers for @@ -57,7 +59,6 @@ import org.eclipse.jgit.treewalk.WorkingTreeOptions; * @since 4.3 */ public final class EolStreamTypeUtil { - private static final boolean FORCE_EOL_LF_ON_CHECKOUT = false; private EolStreamTypeUtil() { } @@ -164,11 +165,11 @@ public final class EolStreamTypeUtil { // old git system if (attrs.isSet("crlf")) {//$NON-NLS-1$ - return EolStreamType.TEXT_LF; + return EolStreamType.TEXT_LF; // Same as isSet("text") } else if (attrs.isUnset("crlf")) {//$NON-NLS-1$ - return EolStreamType.DIRECT; + return EolStreamType.DIRECT; // Same as isUnset("text") } else if ("input".equals(attrs.getValue("crlf"))) {//$NON-NLS-1$ //$NON-NLS-2$ - return EolStreamType.TEXT_LF; + return EolStreamType.TEXT_LF; // Same as eol=lf } // new git system @@ -196,6 +197,28 @@ public final class EolStreamTypeUtil { return EolStreamType.DIRECT; } + private static EolStreamType getOutputFormat(WorkingTreeOptions options) { + switch (options.getAutoCRLF()) { + case TRUE: + return EolStreamType.TEXT_CRLF; + default: + // no decision + } + switch (options.getEOL()) { + case CRLF: + return EolStreamType.TEXT_CRLF; + case NATIVE: + if (SystemReader.getInstance().isWindows()) { + return EolStreamType.TEXT_CRLF; + } + return EolStreamType.TEXT_LF; + case LF: + default: + break; + } + return EolStreamType.DIRECT; + } + private static EolStreamType checkOutStreamType(WorkingTreeOptions options, Attributes attrs) { if (attrs.isUnset("text")) {//$NON-NLS-1$ @@ -205,57 +228,35 @@ public final class EolStreamTypeUtil { // old git system if (attrs.isSet("crlf")) {//$NON-NLS-1$ - return FORCE_EOL_LF_ON_CHECKOUT ? EolStreamType.TEXT_LF - : EolStreamType.DIRECT; + return getOutputFormat(options); // Same as isSet("text") } else if (attrs.isUnset("crlf")) {//$NON-NLS-1$ - return EolStreamType.DIRECT; + return EolStreamType.DIRECT; // Same as isUnset("text") } else if ("input".equals(attrs.getValue("crlf"))) {//$NON-NLS-1$ //$NON-NLS-2$ - return EolStreamType.DIRECT; + return EolStreamType.DIRECT; // Same as eol=lf } // new git system String eol = attrs.getValue("eol"); //$NON-NLS-1$ - if (eol != null && "crlf".equals(eol)) //$NON-NLS-1$ - return EolStreamType.TEXT_CRLF; - if (eol != null && "lf".equals(eol)) //$NON-NLS-1$ - return FORCE_EOL_LF_ON_CHECKOUT ? EolStreamType.TEXT_LF - : EolStreamType.DIRECT; - - if (attrs.isSet("text")) { //$NON-NLS-1$ - switch (options.getAutoCRLF()) { - case TRUE: - return EolStreamType.TEXT_CRLF; - default: - // no decision - } - switch (options.getEOL()) { - case CRLF: + if (eol != null) { + if ("crlf".equals(eol)) {//$NON-NLS-1$ return EolStreamType.TEXT_CRLF; - case LF: - return FORCE_EOL_LF_ON_CHECKOUT ? EolStreamType.TEXT_LF - : EolStreamType.DIRECT; - case NATIVE: - default: + } else if ("lf".equals(eol)) { //$NON-NLS-1$ return EolStreamType.DIRECT; } } + if (attrs.isSet("text")) { //$NON-NLS-1$ + return getOutputFormat(options); + } if ("auto".equals(attrs.getValue("text"))) { //$NON-NLS-1$ //$NON-NLS-2$ - switch (options.getAutoCRLF()) { - case TRUE: + EolStreamType basic = getOutputFormat(options); + switch (basic) { + case TEXT_CRLF: return EolStreamType.AUTO_CRLF; + case TEXT_LF: + return EolStreamType.AUTO_LF; default: - // no decision - } - switch (options.getEOL()) { - case CRLF: - return EolStreamType.AUTO_CRLF; - case LF: - return FORCE_EOL_LF_ON_CHECKOUT ? EolStreamType.TEXT_LF - : EolStreamType.DIRECT; - case NATIVE: - default: - return EolStreamType.DIRECT; + return basic; } } |