diff options
Diffstat (limited to 'org.eclipse.jgit')
44 files changed, 1600 insertions, 151 deletions
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 6719570e05..fc326d4462 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -22,6 +22,14 @@ </message_arguments> </filter> </resource> + <resource path="src/org/eclipse/jgit/lib/Repository.java" type="org.eclipse.jgit.lib.Repository"> + <filter id="336695337"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.Repository"/> + <message_argument value="getIdentifier()"/> + </message_arguments> + </filter> + </resource> <resource path="src/org/eclipse/jgit/revwalk/ObjectWalk.java" type="org.eclipse.jgit.revwalk.ObjectWalk"> <filter comment="ignore the risk subclasses could define the same field and cause a name clash" id="336658481"> <message_arguments> @@ -94,6 +102,26 @@ </message_arguments> </filter> </resource> + <resource path="src/org/eclipse/jgit/transport/HttpConfig.java" type="org.eclipse.jgit.transport.HttpConfig"> + <filter id="336658481"> + <message_arguments> + <message_argument value="org.eclipse.jgit.transport.HttpConfig"/> + <message_argument value="COOKIE_FILE_CACHE_LIMIT_KEY"/> + </message_arguments> + </filter> + <filter id="336658481"> + <message_arguments> + <message_argument value="org.eclipse.jgit.transport.HttpConfig"/> + <message_argument value="COOKIE_FILE_KEY"/> + </message_arguments> + </filter> + <filter id="336658481"> + <message_arguments> + <message_argument value="org.eclipse.jgit.transport.HttpConfig"/> + <message_argument value="SAVE_COOKIES_KEY"/> + </message_arguments> + </filter> + </resource> <resource path="src/org/eclipse/jgit/transport/Transport.java" type="org.eclipse.jgit.transport.Transport"> <filter comment="Marked as final since overriding a deprecated stub is likely a mistake" id="421654647"> <message_arguments> @@ -132,4 +160,24 @@ </message_arguments> </filter> </resource> + <resource path="src/org/eclipse/jgit/util/HttpSupport.java" type="org.eclipse.jgit.util.HttpSupport"> + <filter id="336658481"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.HttpSupport"/> + <message_argument value="HDR_COOKIE"/> + </message_arguments> + </filter> + <filter id="336658481"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.HttpSupport"/> + <message_argument value="HDR_SET_COOKIE"/> + </message_arguments> + </filter> + <filter id="336658481"> + <message_arguments> + <message_argument value="org.eclipse.jgit.util.HttpSupport"/> + <message_argument value="HDR_SET_COOKIE2"/> + </message_arguments> + </filter> + </resource> </component> diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 95594f29e5..893f0d4305 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -86,6 +86,7 @@ Export-Package: org.eclipse.jgit.annotations;version="5.4.0", org.eclipse.jgit.pgm", org.eclipse.jgit.internal.storage.reftree;version="5.4.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", org.eclipse.jgit.internal.submodule;version="5.4.0";x-internal:=true, + org.eclipse.jgit.internal.transport.http;version="5.4.0";x-friends:="org.eclipse.jgit.test", org.eclipse.jgit.internal.transport.parser;version="5.4.0";x-friends:="org.eclipse.jgit.http.server,org.eclipse.jgit.test", org.eclipse.jgit.internal.transport.ssh;version="5.4.0";x-friends:="org.eclipse.jgit.ssh.apache", org.eclipse.jgit.lib;version="5.4.0"; @@ -160,16 +161,17 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", com.jcraft.jsch;version="[0.1.37,0.2.0)", javax.crypto, javax.net.ssl, - org.bouncycastle;version="[1.60.0,2.0.0)", - org.bouncycastle.bcpg;version="[1.60.0,2.0.0)", - org.bouncycastle.gpg;version="[1.60.0,2.0.0)", - org.bouncycastle.gpg.keybox;version="[1.60.0,2.0.0)", - org.bouncycastle.jce.provider;version="[1.60.0,2.0.0)", - org.bouncycastle.openpgp;version="[1.60.0,2.0.0)", - org.bouncycastle.openpgp.jcajce;version="[1.60.0,2.0.0)", - org.bouncycastle.openpgp.operator;version="[1.60.0,2.0.0)", - org.bouncycastle.openpgp.operator.jcajce;version="[1.60.0,2.0.0)", - org.bouncycastle.util.encoders;version="[1.60.0,2.0.0)", + org.bouncycastle;version="[1.61.0,2.0.0)", + org.bouncycastle.bcpg;version="[1.61.0,2.0.0)", + org.bouncycastle.gpg;version="[1.61.0,2.0.0)", + org.bouncycastle.gpg.keybox;version="[1.61.0,2.0.0)", + org.bouncycastle.gpg.keybox.jcajce;version="[1.61.0,2.0.0)", + org.bouncycastle.jce.provider;version="[1.61.0,2.0.0)", + org.bouncycastle.openpgp;version="[1.61.0,2.0.0)", + org.bouncycastle.openpgp.jcajce;version="[1.61.0,2.0.0)", + org.bouncycastle.openpgp.operator;version="[1.61.0,2.0.0)", + org.bouncycastle.openpgp.operator.jcajce;version="[1.61.0,2.0.0)", + org.bouncycastle.util.encoders;version="[1.61.0,2.0.0)", org.slf4j;version="[1.7.0,2.0.0)", org.xml.sax, org.xml.sax.helpers 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 fc2a26f0d7..df42dc757b 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -139,6 +139,7 @@ configSubsectionContainsNewline=config subsection name contains newline configSubsectionContainsNullByte=config subsection name contains byte 0x00 configValueContainsNullByte=config value contains byte 0x00 configHandleIsStale=config file handle is stale, {0}. retry +configHandleMayBeLocked=config file handle may be locked by other process, {0}. retry connectionFailed=connection failed connectionTimeOut=Connection time out: {0} contextMustBeNonNegative=context must be >= 0 @@ -208,6 +209,10 @@ couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index couldNotGetAdvertisedRef=Remote {0} did not advertise Ref for branch {1}. This Ref may not exist in the remote or may be hidden by permission settings. couldNotGetRepoStatistics=Could not get repository statistics couldNotLockHEAD=Could not lock HEAD +couldNotFindTabInLine=Could not find tab in line {0}. Tab is the mandatory separator for the Netscape Cookie File Format. +couldNotFindSixTabsInLine=Could not find 6 tabs but only {0} in line '{1}'. 7 tab separated columns per line are mandatory for the Netscape Cookie File Format. +couldNotPersistCookies=Could not persist received cookies in file ''{0}'' +couldNotReadCookieFile=Could not read cookie file ''{0}'' couldNotReadIndexInOneGo=Could not read index in one go, only {0} out of {1} read couldNotReadObjectWhileParsingCommit=Could not read an object while parsing commit {0} couldNotRenameDeleteOldIndex=Could not rename delete old index @@ -455,6 +460,7 @@ mismatchOffset=mismatch offset for object {0} mismatchCRC=mismatch CRC for object {0} missingAccesskey=Missing accesskey. missingConfigurationForKey=No value for key {0} found in configuration +missingCookieFile=Configured http.cookieFile ''{0}'' is missing missingCRC=missing CRC for object {0} missingDeltaBase=delta base missingForwardImageInGITBinaryPatch=Missing forward-image in GIT binary patch @@ -625,6 +631,7 @@ rewinding=Rewinding to commit {0} s3ActionDeletion=Deletion s3ActionReading=Reading s3ActionWriting=Writing +searchForReachableBranches=Finding reachable branches searchForReuse=Finding sources searchForSizes=Getting sizes secondsAgo={0} seconds ago diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java index 9ebcf9fd0f..9ad77e65fd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java @@ -63,8 +63,7 @@ import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.InvalidPatternException; import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.ignore.internal.IMatcher; -import org.eclipse.jgit.ignore.internal.PathMatcher; +import org.eclipse.jgit.fnmatch.FileNameMatcher; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; @@ -104,12 +103,17 @@ public class DescribeCommand extends GitCommand<String> { /** * Pattern matchers to be applied to tags under consideration. */ - private List<IMatcher> matchers = new ArrayList<>(); + private List<FileNameMatcher> matchers = new ArrayList<>(); /** * Whether to use all tags (incl. lightweight) or not. */ - private boolean useTags = false; + private boolean useTags; + + /** + * Whether to show a uniquely abbreviated commit hash as a fallback or not. + */ + private boolean always; /** * Constructor for DescribeCommand. @@ -197,6 +201,21 @@ public class DescribeCommand extends GitCommand<String> { return this; } + /** + * Always describe the commit by eventually falling back to a uniquely + * abbreviated commit hash if no other name matches. + * + * @param always + * <code>true</code> enables falling back to a uniquely + * abbreviated commit hash + * @return {@code this} + * @since 5.4 + */ + public DescribeCommand setAlways(boolean always) { + this.always = always; + return this; + } + private String longDescription(Ref tag, int depth, ObjectId tip) throws IOException { return String.format( @@ -222,7 +241,7 @@ public class DescribeCommand extends GitCommand<String> { */ public DescribeCommand setMatch(String... patterns) throws InvalidPatternException { for (String p : patterns) { - matchers.add(PathMatcher.createPathMatcher(p, null, false)); + matchers.add(new FileNameMatcher(p, null)); } return this; } @@ -255,9 +274,15 @@ public class DescribeCommand extends GitCommand<String> { // Find the first tag that matches in the stream of all tags // filtered by matchers ordered by tie break order Stream<Ref> matchingTags = Stream.empty(); - for (IMatcher matcher : matchers) { + for (FileNameMatcher matcher : matchers) { Stream<Ref> m = tags.stream().filter( - tag -> matcher.matches(tag.getName(), false, false)); + tag -> { + matcher.append( + tag.getName().substring(R_TAGS.length())); + boolean result = matcher.isMatch(); + matcher.reset(); + return result; + }); matchingTags = Stream.of(matchingTags, m).flatMap(i -> i); } return matchingTags.sorted(TAG_TIE_BREAKER).findFirst(); @@ -399,8 +424,9 @@ public class DescribeCommand extends GitCommand<String> { } // if all the nodes are dominated by all the tags, the walk stops - if (candidates.isEmpty()) - return null; + if (candidates.isEmpty()) { + return always ? w.getObjectReader().abbreviate(target).name() : null; + } Candidate best = Collections.min(candidates, (Candidate o1, Candidate o2) -> o1.depth - o2.depth); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java index 9b8016ce20..66de8ae131 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java @@ -133,7 +133,7 @@ public class LogCommand extends GitCommand<Iterable<RevCommit>> { @Override public Iterable<RevCommit> call() throws GitAPIException, NoHeadException { checkCallable(); - if (pathFilters.size() > 0) + if (!pathFilters.isEmpty()) walk.setTreeFilter(AndTreeFilter.create( PathFilterGroup.create(pathFilters), TreeFilter.ANY_DIFF)); if (skip > -1 && maxCount > -1) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java index f0ad29db49..bdb2d1bbc5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java @@ -60,6 +60,7 @@ import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.RefNotAdvertisedException; import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.BranchConfig.BranchRebaseMode; @@ -67,12 +68,17 @@ import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode; import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.TagOpt; @@ -339,6 +345,45 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { PullResult result; if (pullRebaseMode != BranchRebaseMode.NONE) { + try { + Ref head = repo.exactRef(Constants.HEAD); + if (head == null) { + throw new NoHeadException(JGitText + .get().commitOnRepoWithoutHEADCurrentlyNotSupported); + } + ObjectId headId = head.getObjectId(); + if (headId == null) { + // Pull on an unborn branch: checkout + try (RevWalk revWalk = new RevWalk(repo)) { + RevCommit srcCommit = revWalk + .parseCommit(commitToMerge); + DirCacheCheckout dco = new DirCacheCheckout(repo, + repo.lockDirCache(), srcCommit.getTree()); + dco.setFailOnConflict(true); + dco.setProgressMonitor(monitor); + dco.checkout(); + RefUpdate refUpdate = repo + .updateRef(head.getTarget().getName()); + refUpdate.setNewObjectId(commitToMerge); + refUpdate.setExpectedOldObjectId(null); + refUpdate.setRefLogMessage("initial pull", false); //$NON-NLS-1$ + if (refUpdate.update() != Result.NEW) { + throw new NoHeadException(JGitText + .get().commitOnRepoWithoutHEADCurrentlyNotSupported); + } + monitor.endTask(); + return new PullResult(fetchRes, remote, + RebaseResult.result( + RebaseResult.Status.FAST_FORWARD, + srcCommit)); + } + } + } catch (NoHeadException e) { + throw e; + } catch (IOException e) { + throw new JGitInternalException(JGitText + .get().exceptionCaughtDuringExecutionOfPullCommand, e); + } RebaseCommand rebase = new RebaseCommand(repo); RebaseResult rebaseRes = rebase.setUpstream(commitToMerge) .setUpstreamName(upstreamName).setProgressMonitor(monitor) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index cdd1b80bb4..593874c121 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -490,7 +490,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { resetSoftToParent(); List<RebaseTodoLine> steps = repo.readRebaseTodo( rebaseState.getPath(GIT_REBASE_TODO), false); - RebaseTodoLine nextStep = steps.size() > 0 ? steps.get(0) : null; + RebaseTodoLine nextStep = steps.isEmpty() ? null : steps.get(0); File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP); File messageSquashFile = rebaseState.getFile(MESSAGE_SQUASH); if (isSquash && messageFixupFile.exists()) @@ -1083,7 +1083,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO), todoLines, false); - if (poppedLines.size() > 0) { + if (!poppedLines.isEmpty()) { repo.writeRebaseTodoFile(rebaseState.getPath(DONE), poppedLines, true); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index ca0024d1c9..bdaef5a366 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -200,6 +200,7 @@ public class JGitText extends TranslationBundle { /***/ public String configSubsectionContainsNullByte; /***/ public String configValueContainsNullByte; /***/ public String configHandleIsStale; + /***/ public String configHandleMayBeLocked; /***/ public String connectionFailed; /***/ public String connectionTimeOut; /***/ public String contextMustBeNonNegative; @@ -267,9 +268,13 @@ public class JGitText extends TranslationBundle { /***/ public String couldNotCheckOutBecauseOfConflicts; /***/ public String couldNotDeleteLockFileShouldNotHappen; /***/ public String couldNotDeleteTemporaryIndexFileShouldNotHappen; + /***/ public String couldNotFindTabInLine; + /***/ public String couldNotFindSixTabsInLine; /***/ public String couldNotGetAdvertisedRef; /***/ public String couldNotGetRepoStatistics; /***/ public String couldNotLockHEAD; + /***/ public String couldNotPersistCookies; + /***/ public String couldNotReadCookieFile; /***/ public String couldNotReadIndexInOneGo; /***/ public String couldNotReadObjectWhileParsingCommit; /***/ public String couldNotRenameDeleteOldIndex; @@ -516,6 +521,7 @@ public class JGitText extends TranslationBundle { /***/ public String mismatchCRC; /***/ public String missingAccesskey; /***/ public String missingConfigurationForKey; + /***/ public String missingCookieFile; /***/ public String missingCRC; /***/ public String missingDeltaBase; /***/ public String missingForwardImageInGITBinaryPatch; @@ -686,6 +692,7 @@ public class JGitText extends TranslationBundle { /***/ public String s3ActionDeletion; /***/ public String s3ActionReading; /***/ public String s3ActionWriting; + /***/ public String searchForReachableBranches; /***/ public String searchForReuse; /***/ public String searchForSizes; /***/ public String secondsAgo; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java index 5169e929e4..8e5c5a7f75 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java @@ -126,6 +126,12 @@ public abstract class DfsRepository extends Repository { /** {@inheritDoc} */ @Override + public String getIdentifier() { + return getDescription().getRepositoryName(); + } + + /** {@inheritDoc} */ + @Override public void scanForRepoChanges() throws IOException { getRefDatabase().refresh(); getObjectDatabase().clearCache(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java index d82d29e4cf..90772970ae 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java @@ -390,6 +390,17 @@ public class FileRepository extends Repository { /** {@inheritDoc} */ @Override + public String getIdentifier() { + File directory = getDirectory(); + if (directory != null) { + return directory.getPath(); + } else { + throw new IllegalStateException(); + } + } + + /** {@inheritDoc} */ + @Override public FileBasedConfig getConfig() { if (systemConfig.isOutdated()) { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java index 00124bcf27..4540860a0d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java @@ -244,7 +244,7 @@ public class GC { * If the configuration parameter "gc.pruneexpire" couldn't be * parsed */ - // TODO(ms): in 5.0 change signature and return Future<Collection<PackFile>> + // TODO(ms): change signature and return Future<Collection<PackFile>> @SuppressWarnings("FutureReturnValueIgnored") public Collection<PackFile> gc() throws IOException, ParseException { if (!background) { @@ -281,7 +281,7 @@ public class GC { } return Collections.emptyList(); }; - // TODO(ms): in 5.0 change signature and return the Future + // TODO(ms): change signature and return the Future executor().submit(gcTask); return Collections.emptyList(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java new file mode 100644 index 0000000000..075f55c733 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java @@ -0,0 +1,476 @@ +/* + * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.http; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringReader; +import java.io.Writer; +import java.net.HttpCookie; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.file.FileSnapshot; +import org.eclipse.jgit.internal.storage.file.LockFile; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.RawParseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps all cookies persisted in a <strong>Netscape Cookie File Format</strong> + * being referenced via the git config <a href= + * "https://git-scm.com/docs/git-config#git-config-httpcookieFile">http.cookieFile</a>. + * + * It will only load the cookies lazily, i.e. before calling + * {@link #getCookies(boolean)} the file is not evaluated. This class also + * allows persisting cookies in that file format. + * <p> + * In general this class is not thread-safe. So any consumer needs to take care + * of synchronization! + * + * @see <a href="http://www.cookiecentral.com/faq/#3.5">Netscape Cookie File + * Format</a> + * @see <a href= + * "https://unix.stackexchange.com/questions/36531/format-of-cookies-when-using-wget">Cookie + * format for wget</a> + * @see <a href= + * "https://github.com/curl/curl/blob/07ebaf837843124ee670e5b8c218b80b92e06e47/lib/cookie.c#L745">libcurl + * Cookie file parsing</a> + * @see <a href= + * "https://github.com/curl/curl/blob/07ebaf837843124ee670e5b8c218b80b92e06e47/lib/cookie.c#L1417">libcurl + * Cookie file writing</a> + * @see NetscapeCookieFileCache + */ +public final class NetscapeCookieFile { + + private static final String HTTP_ONLY_PREAMBLE = "#HttpOnly_"; //$NON-NLS-1$ + + private static final String COLUMN_SEPARATOR = "\t"; //$NON-NLS-1$ + + private static final String LINE_SEPARATOR = "\n"; //$NON-NLS-1$ + + /** + * Maximum number of retries to acquire the lock for writing to the + * underlying file. + */ + private static final int LOCK_ACQUIRE_MAX_RETRY_COUNT = 4; + + /** + * Sleep time in milliseconds between retries to acquire the lock for + * writing to the underlying file. + */ + private static final int LOCK_ACQUIRE_RETRY_SLEEP = 500; + + private final Path path; + + private FileSnapshot snapshot; + + private byte[] hash; + + final Date creationDate; + + private Set<HttpCookie> cookies = null; + + private static final Logger LOG = LoggerFactory + .getLogger(NetscapeCookieFile.class); + + /** + * @param path + */ + public NetscapeCookieFile(Path path) { + this(path, new Date()); + } + + NetscapeCookieFile(Path path, Date creationDate) { + this.path = path; + this.snapshot = FileSnapshot.DIRTY; + this.creationDate = creationDate; + } + + /** + * @return the path to the underlying cookie file + */ + public Path getPath() { + return path; + } + + /** + * @param refresh + * if {@code true} updates the list from the underlying cookie + * file if it has been modified since the last read otherwise + * returns the current transient state. In case the cookie file + * has never been read before will always read from the + * underlying file disregarding the value of this parameter. + * @return all cookies (may contain session cookies as well). This does not + * return a copy of the list but rather the original one. Every + * addition to the returned list can afterwards be persisted via + * {@link #write(URL)}. Errors in the underlying file will not lead + * to exceptions but rather to an empty set being returned and the + * underlying error being logged. + */ + public Set<HttpCookie> getCookies(boolean refresh) { + if (cookies == null || refresh) { + try { + byte[] in = getFileContentIfModified(); + Set<HttpCookie> newCookies = parseCookieFile(in, creationDate); + if (cookies != null) { + cookies = mergeCookies(newCookies, cookies); + } else { + cookies = newCookies; + } + return cookies; + } catch (IOException | IllegalArgumentException e) { + LOG.warn( + MessageFormat.format( + JGitText.get().couldNotReadCookieFile, path), + e); + if (cookies == null) { + cookies = new LinkedHashSet<>(); + } + } + } + return cookies; + + } + + /** + * Parses the given file and extracts all cookie information from it. + * + * @param input + * the file content to parse + * @param creationDate + * the date for the creation of the cookies (used to calculate + * the maxAge based on the expiration date given within the file) + * @return the set of parsed cookies from the given file (even expired + * ones). If there is more than one cookie with the same name in + * this file the last one overwrites the first one! + * @throws IOException + * if the given file could not be read for some reason + * @throws IllegalArgumentException + * if the given file does not have a proper format. + */ + private static Set<HttpCookie> parseCookieFile(@NonNull byte[] input, + @NonNull Date creationDate) + throws IOException, IllegalArgumentException { + + String decoded = RawParseUtils.decode(StandardCharsets.US_ASCII, input); + + Set<HttpCookie> cookies = new LinkedHashSet<>(); + try (BufferedReader reader = new BufferedReader( + new StringReader(decoded))) { + String line; + while ((line = reader.readLine()) != null) { + HttpCookie cookie = parseLine(line, creationDate); + if (cookie != null) { + cookies.add(cookie); + } + } + } + return cookies; + } + + private static HttpCookie parseLine(@NonNull String line, + @NonNull Date creationDate) { + if (line.isEmpty() || (line.startsWith("#") //$NON-NLS-1$ + && !line.startsWith(HTTP_ONLY_PREAMBLE))) { + return null; + } + String[] cookieLineParts = line.split(COLUMN_SEPARATOR, 7); + if (cookieLineParts == null) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().couldNotFindTabInLine, line)); + } + if (cookieLineParts.length < 7) { + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().couldNotFindSixTabsInLine, + Integer.valueOf(cookieLineParts.length), line)); + } + String name = cookieLineParts[5]; + String value = cookieLineParts[6]; + HttpCookie cookie = new HttpCookie(name, value); + + String domain = cookieLineParts[0]; + if (domain.startsWith(HTTP_ONLY_PREAMBLE)) { + cookie.setHttpOnly(true); + domain = domain.substring(HTTP_ONLY_PREAMBLE.length()); + } + // strip off leading "." + // (https://tools.ietf.org/html/rfc6265#section-5.2.3) + if (domain.startsWith(".")) { //$NON-NLS-1$ + domain = domain.substring(1); + } + cookie.setDomain(domain); + // domain evaluation as boolean flag not considered (i.e. always assumed + // to be true) + cookie.setPath(cookieLineParts[2]); + cookie.setSecure(Boolean.parseBoolean(cookieLineParts[3])); + + long expires = Long.parseLong(cookieLineParts[4]); + long maxAge = (expires - creationDate.getTime()) / 1000; + if (maxAge <= 0) { + return null; // skip expired cookies + } + cookie.setMaxAge(maxAge); + return cookie; + } + + /** + * Writes all the cookies being maintained in the set being returned by + * {@link #getCookies(boolean)} to the underlying file. + * + * Session-cookies will not be persisted. + * + * @param url + * url for which to write the cookies (important to derive + * default values for non-explicitly set attributes) + * @throws IOException + * @throws IllegalArgumentException + * @throws InterruptedException + */ + public void write(URL url) + throws IllegalArgumentException, IOException, InterruptedException { + try { + byte[] cookieFileContent = getFileContentIfModified(); + if (cookieFileContent != null) { + LOG.debug( + "Reading the underlying cookie file '{}' as it has been modified since the last access", //$NON-NLS-1$ + path); + // reread new changes if necessary + Set<HttpCookie> cookiesFromFile = NetscapeCookieFile + .parseCookieFile(cookieFileContent, creationDate); + this.cookies = mergeCookies(cookiesFromFile, cookies); + } + } catch (FileNotFoundException e) { + // ignore if file previously did not exist yet! + } + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (Writer writer = new OutputStreamWriter(output, + StandardCharsets.US_ASCII)) { + write(writer, cookies, url, creationDate); + } + LockFile lockFile = new LockFile(path.toFile()); + for (int retryCount = 0; retryCount < LOCK_ACQUIRE_MAX_RETRY_COUNT; retryCount++) { + if (lockFile.lock()) { + try { + lockFile.setNeedSnapshot(true); + lockFile.write(output.toByteArray()); + if (!lockFile.commit()) { + throw new IOException(MessageFormat.format( + JGitText.get().cannotCommitWriteTo, path)); + } + } finally { + lockFile.unlock(); + } + return; + } + Thread.sleep(LOCK_ACQUIRE_RETRY_SLEEP); + } + throw new IOException( + MessageFormat.format(JGitText.get().cannotLock, lockFile)); + + } + + /** + * Read the underying file and return its content but only in case it has + * been modified since the last access. Internally calculates the hash and + * maintains {@link FileSnapshot}s to prevent issues described as <a href= + * "https://github.com/git/git/blob/master/Documentation/technical/racy-git.txt">"Racy + * Git problem"</a>. Inspired by {@link FileBasedConfig#load()}. + * + * @return the file contents in case the file has been modified since the + * last access, otherwise {@code null} + * @throws IOException + */ + private byte[] getFileContentIfModified() throws IOException { + final int maxStaleRetries = 5; + int retries = 0; + File file = getPath().toFile(); + if (!file.exists()) { + LOG.warn(MessageFormat.format(JGitText.get().missingCookieFile, + file.getAbsolutePath())); + return new byte[0]; + } + while (true) { + final FileSnapshot oldSnapshot = snapshot; + final FileSnapshot newSnapshot = FileSnapshot.save(file); + try { + final byte[] in = IO.readFully(file); + byte[] newHash = hash(in); + if (Arrays.equals(hash, newHash)) { + if (oldSnapshot.equals(newSnapshot)) { + oldSnapshot.setClean(newSnapshot); + } else { + snapshot = newSnapshot; + } + } else { + snapshot = newSnapshot; + hash = newHash; + } + return in; + } catch (FileNotFoundException e) { + throw e; + } catch (IOException e) { + if (FileUtils.isStaleFileHandle(e) + && retries < maxStaleRetries) { + if (LOG.isDebugEnabled()) { + LOG.debug(MessageFormat.format( + JGitText.get().configHandleIsStale, + Integer.valueOf(retries)), e); + } + retries++; + continue; + } + throw new IOException(MessageFormat + .format(JGitText.get().cannotReadFile, getPath()), e); + } + } + + } + + private byte[] hash(final byte[] in) { + return Constants.newMessageDigest().digest(in); + } + + /** + * Writes the given cookies to the file in the Netscape Cookie File Format + * (also used by curl) + * + * @param writer + * the writer to use to persist the cookies. + * @param cookies + * the cookies to write into the file + * @param url + * the url for which to write the cookie (to derive the default + * values for certain cookie attributes) + * @param creationDate + * the date when the cookie has been created. Important for + * calculation the cookie expiration time (calculated from + * cookie's maxAge and this creation time). + * @throws IOException + */ + static void write(@NonNull Writer writer, + @NonNull Collection<HttpCookie> cookies, @NonNull URL url, + @NonNull Date creationDate) throws IOException { + for (HttpCookie cookie : cookies) { + writeCookie(writer, cookie, url, creationDate); + } + } + + private static void writeCookie(@NonNull Writer writer, + @NonNull HttpCookie cookie, @NonNull URL url, + @NonNull Date creationDate) throws IOException { + if (cookie.getMaxAge() <= 0) { + return; // skip expired cookies + } + String domain = ""; //$NON-NLS-1$ + if (cookie.isHttpOnly()) { + domain = HTTP_ONLY_PREAMBLE; + } + if (cookie.getDomain() != null) { + domain += cookie.getDomain(); + } else { + domain += url.getHost(); + } + writer.write(domain); + writer.write(COLUMN_SEPARATOR); + writer.write("TRUE"); //$NON-NLS-1$ + writer.write(COLUMN_SEPARATOR); + String path = cookie.getPath(); + if (path == null) { + path = url.getPath(); + } + writer.write(path); + writer.write(COLUMN_SEPARATOR); + writer.write(Boolean.toString(cookie.getSecure()).toUpperCase()); + writer.write(COLUMN_SEPARATOR); + final String expirationDate; + // whenCreated field is not accessible in HttpCookie + expirationDate = String + .valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000)); + writer.write(expirationDate); + writer.write(COLUMN_SEPARATOR); + writer.write(cookie.getName()); + writer.write(COLUMN_SEPARATOR); + writer.write(cookie.getValue()); + writer.write(LINE_SEPARATOR); + } + + /** + * Merge the given sets in the following way. All cookies from + * {@code cookies1} and {@code cookies2} are contained in the resulting set + * which have unique names. If there is a duplicate entry for one name only + * the entry from set {@code cookies1} ends up in the resulting set. + * + * @param cookies1 + * @param cookies2 + * + * @return the merged cookies + */ + static Set<HttpCookie> mergeCookies(Set<HttpCookie> cookies1, + @Nullable Set<HttpCookie> cookies2) { + Set<HttpCookie> mergedCookies = new LinkedHashSet<>(cookies1); + if (cookies2 != null) { + mergedCookies.addAll(cookies2); + } + return mergedCookies; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java new file mode 100644 index 0000000000..882b2d055b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.http; + +import java.nio.file.Path; + +import org.eclipse.jgit.transport.HttpConfig; +import org.eclipse.jgit.util.LRUMap; + +/** + * A cache of all known cookie files ({@link NetscapeCookieFile}). May contain + * at most {@code n} entries, where the least-recently used one is evicted as + * soon as more entries are added. The maximum number of entries (={@code n}) + * can be set via the git config key {@code http.cookieFileCacheLimit}. By + * default it is set to 10. + * <p> + * The cache is global, i.e. it is shared among all consumers within the same + * Java process. + * + * @see NetscapeCookieFile + * + */ +public class NetscapeCookieFileCache { + + private final LRUMap<Path, NetscapeCookieFile> cookieFileMap; + + private static NetscapeCookieFileCache instance; + + private NetscapeCookieFileCache(HttpConfig config) { + cookieFileMap = new LRUMap<>(config.getCookieFileCacheLimit(), + config.getCookieFileCacheLimit()); + } + + /** + * @param config + * the config which defines the limit for this cache + * @return the singleton instance of the cookie file cache. If the cache has + * already been created the given config is ignored (even if it + * differs from the config, with which the cache has originally been + * created) + */ + public static NetscapeCookieFileCache getInstance(HttpConfig config) { + if (instance == null) { + return new NetscapeCookieFileCache(config); + } else { + return instance; + } + } + + /** + * @param path + * the path of the cookie file to retrieve + * @return the cache entry belonging to the requested file + */ + public NetscapeCookieFile getEntry(Path path) { + if (!cookieFileMap.containsKey(path)) { + synchronized (NetscapeCookieFileCache.class) { + if (!cookieFileMap.containsKey(path)) { + cookieFileMap.put(path, new NetscapeCookieFile(path)); + } + } + } + return cookieFileMap.get(path); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java index d105d0d200..b0339c677f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java @@ -162,7 +162,7 @@ public final class AbbreviatedObjectId implements Serializable { r |= RawParseUtils.parseHexInt4(bs[p++]); n++; } - return r << (8 - n) * 4; + return r << ((8 - n) * 4); } static int mask(int nibbles, int word, int v) { 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 4726975d07..1032fd0df1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -107,7 +107,7 @@ public class Config { * must ensure it is a special copy of the empty string. It also must * be treated like the empty string. */ - static final String MAGIC_EMPTY_VALUE = new String(); + private static final String MISSING_ENTRY = new String(); /** * Create a configuration with no default fallback. @@ -129,6 +129,17 @@ public class Config { } /** + * Check if a given string is the "missing" value. + * + * @param value + * @return true if the given string is the "missing" value. + * @since 5.4 + */ + public static boolean isMissing(String value) { + return value == MISSING_ENTRY; + } + + /** * Globally sets a {@link org.eclipse.jgit.lib.TypedConfigGetter} that is * subsequently used to read typed values from all git configs. * @@ -1041,7 +1052,7 @@ public class Config { if (e.prefix == null || "".equals(e.prefix)) //$NON-NLS-1$ out.append('\t'); out.append(e.name); - if (MAGIC_EMPTY_VALUE != e.value) { + if (MISSING_ENTRY != e.value) { out.append(" ="); //$NON-NLS-1$ if (e.value != null) { out.append(' '); @@ -1132,7 +1143,7 @@ public class Config { e.name = readKeyName(in); if (e.name.endsWith("\n")) { //$NON-NLS-1$ e.name = e.name.substring(0, e.name.length() - 1); - e.value = MAGIC_EMPTY_VALUE; + e.value = MISSING_ENTRY; } else e.value = readValue(in); @@ -1165,7 +1176,7 @@ public class Config { private void addIncludedConfig(final List<ConfigLine> newEntries, ConfigLine line, int depth) throws ConfigInvalidException { if (!line.name.equalsIgnoreCase("path") || //$NON-NLS-1$ - line.value == null || line.value.equals(MAGIC_EMPTY_VALUE)) { + line.value == null || line.value.equals(MISSING_ENTRY)) { throw new ConfigInvalidException(MessageFormat.format( JGitText.get().invalidLineInConfigFileWithParam, line)); } @@ -1413,11 +1424,23 @@ public class Config { case '"': value.append('"'); continue; + case '\r': { + int next = in.read(); + if (next == '\n') { + continue; // CR-LF + } else if (next >= 0) { + in.reset(); + } + break; + } default: - throw new ConfigInvalidException(MessageFormat.format( - JGitText.get().badEscape, - Character.valueOf(((char) c)))); + break; } + throw new ConfigInvalidException( + MessageFormat.format(JGitText.get().badEscape, + Character.isAlphabetic(c) + ? Character.valueOf(((char) c)) + : toUnicodeLiteral(c))); } if ('"' == c) { @@ -1430,6 +1453,11 @@ public class Config { return value.length() > 0 ? value.toString() : null; } + private static String toUnicodeLiteral(int c) { + return String.format("\\u%04x", //$NON-NLS-1$ + Integer.valueOf(c)); + } + /** * Parses a section of the configuration into an application model object. * <p> 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 6a66cf682f..4c70d20d6c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java @@ -72,7 +72,7 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { if (n == null) { return defaultValue; } - if (Config.MAGIC_EMPTY_VALUE == n) { + if (Config.isMissing(n)) { return true; } try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java index 06b4b227c8..c0fcd4161f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java @@ -179,7 +179,7 @@ public class RebaseTodoFile { int nextSpace = RawParseUtils.next(buf, tokenBegin, ' '); int tokenCount = 0; - while (tokenCount < 3 && nextSpace < lineEnd) { + while (tokenCount < 3 && nextSpace <= lineEnd) { switch (tokenCount) { case 0: String actionToken = new String(buf, tokenBegin, @@ -191,8 +191,14 @@ public class RebaseTodoFile { break; case 1: nextSpace = RawParseUtils.next(buf, tokenBegin, ' '); - String commitToken = new String(buf, tokenBegin, - nextSpace - tokenBegin - 1, UTF_8); + String commitToken; + if (nextSpace > lineEnd + 1) { + commitToken = new String(buf, tokenBegin, + lineEnd - tokenBegin + 1, UTF_8); + } else { + commitToken = new String(buf, tokenBegin, + nextSpace - tokenBegin - 1, UTF_8); + } tokenBegin = nextSpace; commit = AbbreviatedObjectId.fromString(commitToken); break; 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 aac63e9d24..d53b0c926a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -240,6 +240,15 @@ public abstract class Repository implements AutoCloseable { } /** + * Get repository identifier. + * + * @return repository identifier. The returned identifier has to be unique + * within a given Git server. + * @since 5.4 + */ + public abstract String getIdentifier(); + + /** * Get the object database which stores this repository's data. * * @return the object database which stores this repository's data. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java index df9615fc9d..0d44317658 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java @@ -54,6 +54,8 @@ import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.text.MessageFormat; import java.util.Iterator; import java.util.Locale; @@ -67,6 +69,7 @@ import org.bouncycastle.gpg.keybox.KeyBox; import org.bouncycastle.gpg.keybox.KeyInformation; import org.bouncycastle.gpg.keybox.PublicKeyRingBlob; import org.bouncycastle.gpg.keybox.UserID; +import org.bouncycastle.gpg.keybox.jcajce.JcaKeyBoxBuilder; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; @@ -210,9 +213,12 @@ class BouncyCastleGpgKeyLocator { * @return publicKey the public key (maybe <code>null</code>) * @throws IOException * in case of problems reading the file + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException */ private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile) - throws IOException { + throws IOException, NoSuchAlgorithmException, + NoSuchProviderException { KeyBox keyBox = readKeyBoxFile(keyboxFile); for (KeyBlob keyBlob : keyBox.getKeyBlobs()) { if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) { @@ -236,15 +242,17 @@ class BouncyCastleGpgKeyLocator { * @return the secret key * @throws IOException * in case of issues reading key files + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException * @throws PGPException * in case of issues finding a key * @throws CanceledException * @throws URISyntaxException * @throws UnsupportedCredentialItem */ - public BouncyCastleGpgKey findSecretKey() - throws IOException, PGPException, CanceledException, - UnsupportedCredentialItem, URISyntaxException { + public BouncyCastleGpgKey findSecretKey() throws IOException, + NoSuchAlgorithmException, NoSuchProviderException, PGPException, + CanceledException, UnsupportedCredentialItem, URISyntaxException { if (exists(USER_KEYBOX_PATH)) { PGPPublicKey publicKey = // findPublicKeyInKeyBox(USER_KEYBOX_PATH); @@ -376,14 +384,12 @@ class BouncyCastleGpgKeyLocator { .getPublicKey(); } - private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException { + private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException, + NoSuchAlgorithmException, NoSuchProviderException { KeyBox keyBox; try (InputStream in = new BufferedInputStream( newInputStream(keyboxFile))) { - // note: KeyBox constructor reads in the whole InputStream at once - // this code will change in 1.61 to - // either 'new BcKeyBox(in)' or 'new JcaKeyBoxBuilder().build(in)' - keyBox = new KeyBox(in, new JcaKeyFingerprintCalculator()); + keyBox = new JcaKeyBoxBuilder().build(in); } return keyBox; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java index 4d696dd9e7..cfe0931b47 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java @@ -45,6 +45,8 @@ package org.eclipse.jgit.lib.internal; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.security.Security; import org.bouncycastle.bcpg.ArmoredOutputStream; @@ -100,7 +102,8 @@ public class BouncyCastleGpgSigner extends GpgSigner { BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey, committer, passphrasePrompt); return gpgKey != null; - } catch (PGPException | IOException | URISyntaxException e) { + } catch (PGPException | IOException | NoSuchAlgorithmException + | NoSuchProviderException | URISyntaxException e) { return false; } } @@ -109,7 +112,8 @@ public class BouncyCastleGpgSigner extends GpgSigner { PersonIdent committer, BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) throws CanceledException, UnsupportedCredentialItem, IOException, - PGPException, URISyntaxException { + NoSuchAlgorithmException, NoSuchProviderException, PGPException, + URISyntaxException { if (gpgSigningKey == null || gpgSigningKey.isEmpty()) { gpgSigningKey = committer.getEmailAddress(); } @@ -153,7 +157,8 @@ public class BouncyCastleGpgSigner extends GpgSigner { signatureGenerator.generate().encode(out); } commit.setGpgSignature(new GpgSignature(buffer.toByteArray())); - } catch (PGPException | IOException | URISyntaxException e) { + } catch (PGPException | IOException | NoSuchAlgorithmException + | NoSuchProviderException | URISyntaxException e) { throw new JGitInternalException(e.getMessage(), e); } } 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 b69327a9b3..0c3d3fec26 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -1190,7 +1190,7 @@ public class ResolveMerger extends ThreeWayMerger { * otherwise */ public boolean failed() { - return failingPaths.size() > 0; + return !failingPaths.isEmpty(); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapCalculator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapCalculator.java index e1d5d4adad..14e95670aa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapCalculator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapCalculator.java @@ -1,3 +1,45 @@ +/* + * Copyright (C) 2019, 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.revwalk; import static java.util.Objects.requireNonNull; @@ -52,8 +94,9 @@ class BitmapCalculator { * @throws MissingObjectException * the supplied id doesn't exist * @throws IncorrectObjectTypeException - * the supplied id doens't refer to a commit or a tag + * the supplied id doesn't refer to a commit or a tag * @throws IOException + * if the walk cannot open a packfile or loose object */ BitmapBuilder getBitmap(RevCommit start, ProgressMonitor pm) throws MissingObjectException, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java index ab453433d8..6e510f677b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java @@ -55,10 +55,8 @@ import org.eclipse.jgit.lib.NullProgressMonitor; /** * Checks the reachability using bitmaps. - * - * @since 5.4 */ -public class BitmappedReachabilityChecker implements ReachabilityChecker { +class BitmappedReachabilityChecker implements ReachabilityChecker { private final RevWalk walk; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java index 4012a45d3c..bba3c5cff3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java @@ -52,10 +52,8 @@ import org.eclipse.jgit.errors.MissingObjectException; /** * Checks the reachability walking the graph from the starters towards the * target. - * - * @since 5.4 */ -public class PedestrianReachabilityChecker implements ReachabilityChecker { +class PedestrianReachabilityChecker implements ReachabilityChecker { private final boolean topoSort; 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 f12eb2ff86..80fc81073c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java @@ -250,6 +250,23 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { } /** + * Get a reachability checker for commits over this revwalk. + * + * @return the most efficient reachability checker for this repository. + * @throws IOException + * if it cannot open any of the underlying indices. + * + * @since 5.4 + */ + public ReachabilityChecker createReachabilityChecker() throws IOException { + if (reader.getBitmapIndex() != null) { + return new BitmappedReachabilityChecker(this); + } + + return new PedestrianReachabilityChecker(true, this); + } + + /** * {@inheritDoc} * <p> * Release any resources used by this walker's reader. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java index fabf7075d5..2b721b8877 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java @@ -50,6 +50,9 @@ import java.util.List; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; /** @@ -153,15 +156,51 @@ public final class RevWalkUtils { RevWalk revWalk, Collection<Ref> refs) throws MissingObjectException, IncorrectObjectTypeException, IOException { + return findBranchesReachableFrom(commit, revWalk, refs, + NullProgressMonitor.INSTANCE); + } + + /** + * Find the list of branches a given commit is reachable from when following + * parents. + * <p> + * Note that this method calls + * {@link org.eclipse.jgit.revwalk.RevWalk#reset()} at the beginning. + * <p> + * In order to improve performance this method assumes clock skew among + * committers is never larger than 24 hours. + * + * @param commit + * the commit we are looking at + * @param revWalk + * The RevWalk to be used. + * @param refs + * the set of branches we want to see reachability from + * @param monitor + * the callback for progress and cancellation + * @return the list of branches a given commit is reachable from + * @throws org.eclipse.jgit.errors.MissingObjectException + * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException + * @throws java.io.IOException + * @since 5.4 + */ + public static List<Ref> findBranchesReachableFrom(RevCommit commit, + RevWalk revWalk, Collection<Ref> refs, ProgressMonitor monitor) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { // Make sure commit is from the same RevWalk commit = revWalk.parseCommit(commit.getId()); revWalk.reset(); List<Ref> result = new ArrayList<>(); - + monitor.beginTask(JGitText.get().searchForReachableBranches, + refs.size()); final int SKEW = 24*3600; // one day clock skew for (Ref ref : refs) { + if (monitor.isCancelled()) + return result; + monitor.update(1); RevObject maybehead = revWalk.parseAny(ref.getObjectId()); if (!(maybehead instanceof RevCommit)) continue; @@ -176,6 +215,7 @@ public final class RevWalkUtils { if (revWalk.isMergedInto(commit, headCommit)) result.add(ref); } + monitor.endTask(); return result; } 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 2b31ebd8e3..fc6f4a39cd 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 @@ -148,7 +148,8 @@ public class FileBasedConfig extends StoredConfig { */ @Override public void load() throws IOException, ConfigInvalidException { - final int maxStaleRetries = 5; + final int maxRetries = 5; + int retryDelayMillis = 20; int retries = 0; while (true) { final FileSnapshot oldSnapshot = snapshot; @@ -177,6 +178,22 @@ public class FileBasedConfig extends StoredConfig { } return; } catch (FileNotFoundException noFile) { + // might be locked by another process (see exception Javadoc) + if (retries < maxRetries && configFile.exists()) { + if (LOG.isDebugEnabled()) { + LOG.debug(MessageFormat.format( + JGitText.get().configHandleMayBeLocked, + Integer.valueOf(retries)), noFile); + } + try { + Thread.sleep(retryDelayMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + retries++; + retryDelayMillis *= 2; // max wait 1260 ms + continue; + } if (configFile.exists()) { throw noFile; } @@ -185,7 +202,7 @@ public class FileBasedConfig extends StoredConfig { return; } catch (IOException e) { if (FileUtils.isStaleFileHandle(e) - && retries < maxStaleRetries) { + && retries < maxRetries) { if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format( JGitText.get().configHandleIsStale, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java index 20a5d9da72..e8724b72db 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java @@ -201,7 +201,7 @@ abstract class BasePackConnection extends BaseConnection { throw noRepository(); throw eof; } - if (line == PacketLineIn.END) + if (PacketLineIn.isEnd(line)) break; if (line.startsWith("ERR ")) { //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java index 847e901980..35ea35ecb8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java @@ -383,8 +383,7 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen JGitText.get().errorOccurredDuringUnpackingOnTheRemoteEnd, unpackStatus)); } - String refLine; - while ((refLine = pckIn.readString()) != PacketLineIn.END) { + for (String refLine : pckIn.readStrings()) { boolean ok = false; int refNameEnd = -1; if (refLine.startsWith("ok ")) { //$NON-NLS-1$ 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 1741db97fe..e402de0158 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -1282,7 +1282,7 @@ public abstract class BaseReceivePack { return; throw eof; } - if (line == PacketLineIn.END) { + if (PacketLineIn.isEnd(line)) { break; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java index a09b1ff1d2..d1db51eca6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java @@ -227,7 +227,7 @@ public class BundleWriter { exc.add(r.getId()); packWriter.setIndexDisabled(true); packWriter.setDeltaBaseAsOffset(true); - packWriter.setThin(exc.size() > 0); + packWriter.setThin(!exc.isEmpty()); packWriter.setReuseValidatingObjects(false); if (exc.isEmpty()) { packWriter.setTagTargets(tagTargets); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java index 53eaa6a7f9..01f6fec7e4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java @@ -45,14 +45,12 @@ package org.eclipse.jgit.transport; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.UTF_8; -import java.io.File; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -import org.eclipse.jgit.internal.storage.dfs.DfsRepository; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.PushCertificate.NonceStatus; @@ -87,19 +85,7 @@ public class HMACSHA1NonceGenerator implements NonceGenerator { @Override public synchronized String createNonce(Repository repo, long timestamp) throws IllegalStateException { - String path; - if (repo instanceof DfsRepository) { - path = ((DfsRepository) repo).getDescription().getRepositoryName(); - } else { - File directory = repo.getDirectory(); - if (directory != null) { - path = directory.getPath(); - } else { - throw new IllegalStateException(); - } - } - - String input = path + ":" + String.valueOf(timestamp); //$NON-NLS-1$ + String input = repo.getIdentifier() + ":" + String.valueOf(timestamp); //$NON-NLS-1$ byte[] rawHmac = mac.doFinal(input.getBytes(UTF_8)); return Long.toString(timestamp) + "-" + toHex(rawHmac); //$NON-NLS-1$ } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java index 101ce35685..54c21cbc8c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java @@ -89,6 +89,30 @@ public class HttpConfig { /** git config key for the "sslVerify" setting. */ public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$ + /** + * git config key for the "cookieFile" setting. + * + * @since 5.4 + */ + public static final String COOKIE_FILE_KEY = "cookieFile"; //$NON-NLS-1$ + + /** + * git config key for the "saveCookies" setting. + * + * @since 5.4 + */ + public static final String SAVE_COOKIES_KEY = "saveCookies"; //$NON-NLS-1$ + + /** + * Custom JGit config key which holds the maximum number of cookie files to + * keep in the cache. + * + * @since 5.4 + */ + public static final String COOKIE_FILE_CACHE_LIMIT_KEY = "cookieFileCacheLimit"; //$NON-NLS-1$ + + private static final int DEFAULT_COOKIE_FILE_CACHE_LIMIT = 10; + private static final String MAX_REDIRECT_SYSTEM_PROPERTY = "http.maxRedirects"; //$NON-NLS-1$ private static final int DEFAULT_MAX_REDIRECTS = 5; @@ -153,6 +177,12 @@ public class HttpConfig { private int maxRedirects; + private String cookieFile; + + private boolean saveCookies; + + private int cookieFileCacheLimit; + /** * Get the "http.postBuffer" setting * @@ -190,6 +220,40 @@ public class HttpConfig { } /** + * Get the "http.cookieFile" setting + * + * @return the value of the "http.cookieFile" setting + * + * @since 5.4 + */ + public String getCookieFile() { + return cookieFile; + } + + /** + * Get the "http.saveCookies" setting + * + * @return the value of the "http.saveCookies" setting + * + * @since 5.4 + */ + public boolean getSaveCookies() { + return saveCookies; + } + + /** + * Get the "http.cookieFileCacheLimit" setting (gives the maximum number of + * cookie files to keep in the LRU cache) + * + * @return the value of the "http.cookieFileCacheLimit" setting + * + * @since 5.4 + */ + public int getCookieFileCacheLimit() { + return cookieFileCacheLimit; + } + + /** * Creates a new {@link org.eclipse.jgit.transport.HttpConfig} tailored to * the given {@link org.eclipse.jgit.transport.URIish}. * @@ -237,6 +301,10 @@ public class HttpConfig { if (redirectLimit < 0) { redirectLimit = MAX_REDIRECTS; } + cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY); + saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false); + cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY, + DEFAULT_COOKIE_FILE_CACHE_LIMIT); String match = findMatch(config.getSubsections(HTTP), uri); if (match != null) { // Override with more specific items @@ -251,6 +319,13 @@ public class HttpConfig { if (newMaxRedirects >= 0) { redirectLimit = newMaxRedirects; } + String urlSpecificCookieFile = config.getString(HTTP, match, + COOKIE_FILE_KEY); + if (urlSpecificCookieFile != null) { + cookieFile = urlSpecificCookieFile; + } + saveCookies = config.getBoolean(HTTP, match, SAVE_COOKIES_KEY, + saveCookies); } postBuffer = postBufferSize; sslVerify = sslVerifyFlag; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java index c6e19d5762..90f1b373ba 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java @@ -49,7 +49,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.text.MessageFormat; +import java.util.Iterator; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.JGitText; @@ -72,14 +74,25 @@ import org.slf4j.LoggerFactory; public class PacketLineIn { private static final Logger log = LoggerFactory.getLogger(PacketLineIn.class); - /** Magic return from {@link #readString()} when a flush packet is found. */ + /** + * Magic return from {@link #readString()} when a flush packet is found. + * + * @deprecated Callers should use {@link #isEnd(String)} to check if a + * string is the end marker, or + * {@link PacketLineIn#readStrings()} to iterate over all + * strings in the input stream until the marker is reached. + */ + @Deprecated public static final String END = new StringBuilder(0).toString(); /* must not string pool */ /** * Magic return from {@link #readString()} when a delim packet is found. * * @since 5.0 + * @deprecated Callers should use {@link #isDelimiter(String)} to check if a + * string is the delimiter. */ + @Deprecated public static final String DELIM = new StringBuilder(0).toString(); /* must not string pool */ static enum AckNackResult { @@ -193,6 +206,20 @@ public class PacketLineIn { } /** + * Get an iterator to read strings from the input stream. + * + * @return an iterator that calls {@link #readString()} until {@link #END} + * is encountered. + * + * @throws IOException + * on failure to read the initial packet line. + * @since 5.4 + */ + public PacketLineInIterator readStrings() throws IOException { + return new PacketLineInIterator(this); + } + + /** * Read a single UTF-8 encoded string packet from the input stream. * <p> * Unlike {@link #readString()} a trailing LF will be retained. @@ -224,6 +251,52 @@ public class PacketLineIn { return s; } + /** + * Check if a string is the delimiter marker. + * + * @param s + * the string to check + * @return true if the given string is {@link #DELIM}, otherwise false. + * @since 5.4 + */ + public static boolean isDelimiter(String s) { + return s == DELIM; + } + + /** + * Get the delimiter marker. + * <p> + * Intended for use only in tests. + * + * @return The delimiter marker. + */ + static String delimiter() { + return DELIM; + } + + /** + * Get the end marker. + * <p> + * Intended for use only in tests. + * + * @return The end marker. + */ + static String end() { + return END; + } + + /** + * Check if a string is the packet end marker. + * + * @param s + * the string to check + * @return true if the given string is {@link #END}, otherwise false. + * @since 5.4 + */ + public static boolean isEnd(String s) { + return s == END; + } + void discardUntilEnd() throws IOException { for (;;) { int n = readLength(); @@ -282,4 +355,46 @@ public class PacketLineIn { public static class InputOverLimitIOException extends IOException { private static final long serialVersionUID = 1L; } + + /** + * Iterator over packet lines. + * <p> + * Calls {@link #readString()} on the {@link PacketLineIn} until + * {@link #END} is encountered. + * + * @since 5.4 + * + */ + public static class PacketLineInIterator implements Iterable<String> { + private PacketLineIn in; + + private String current; + + PacketLineInIterator(PacketLineIn in) throws IOException { + this.in = in; + current = in.readString(); + } + + @Override + public Iterator<String> iterator() { + return new Iterator<String>() { + @Override + public boolean hasNext() { + return !PacketLineIn.isEnd(current); + } + + @Override + public String next() { + String next = current; + try { + current = in.readString(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return next; + } + }; + } + + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java index 396327aab0..428a45c09d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV0Parser.java @@ -99,7 +99,7 @@ final class ProtocolV0Parser { throw eof; } - if (line == PacketLineIn.END) { + if (PacketLineIn.isEnd(line)) { break; } 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 cb04ff69a9..caba15fc54 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java @@ -92,7 +92,7 @@ final class ProtocolV2Parser { String agentPrefix = OPTION_AGENT + '='; String line = pckIn.readString(); - while (line != PacketLineIn.DELIM && line != PacketLineIn.END) { + while (!PacketLineIn.isDelimiter(line) && !PacketLineIn.isEnd(line)) { if (line.startsWith(serverOptionPrefix)) { serverOptionConsumer .accept(line.substring(serverOptionPrefix.length())); @@ -133,40 +133,41 @@ final class ProtocolV2Parser { serverOption -> reqBuilder.addServerOption(serverOption), agent -> reqBuilder.setAgent(agent)); - if (line == PacketLineIn.END) { + if (PacketLineIn.isEnd(line)) { return reqBuilder.build(); } - if (line != PacketLineIn.DELIM) { + if (!PacketLineIn.isDelimiter(line)) { 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.addWantId(ObjectId.fromString(line.substring(5))); + for (String line2 : pckIn.readStrings()) { + if (line2.startsWith("want ")) { //$NON-NLS-1$ + reqBuilder.addWantId(ObjectId.fromString(line2.substring(5))); } else if (transferConfig.isAllowRefInWant() - && line.startsWith(OPTION_WANT_REF + " ")) { //$NON-NLS-1$ - reqBuilder.addWantedRef(line.substring(OPTION_WANT_REF.length() + 1)); - } else if (line.startsWith("have ")) { //$NON-NLS-1$ - reqBuilder.addPeerHas(ObjectId.fromString(line.substring(5))); - } else if (line.equals("done")) { //$NON-NLS-1$ + && line2.startsWith(OPTION_WANT_REF + " ")) { //$NON-NLS-1$ + reqBuilder.addWantedRef( + line2.substring(OPTION_WANT_REF.length() + 1)); + } else if (line2.startsWith("have ")) { //$NON-NLS-1$ + reqBuilder.addPeerHas(ObjectId.fromString(line2.substring(5))); + } else if (line2.equals("done")) { //$NON-NLS-1$ reqBuilder.setDoneReceived(); - } else if (line.equals(OPTION_THIN_PACK)) { + } else if (line2.equals(OPTION_THIN_PACK)) { reqBuilder.addClientCapability(OPTION_THIN_PACK); - } else if (line.equals(OPTION_NO_PROGRESS)) { + } else if (line2.equals(OPTION_NO_PROGRESS)) { reqBuilder.addClientCapability(OPTION_NO_PROGRESS); - } else if (line.equals(OPTION_INCLUDE_TAG)) { + } else if (line2.equals(OPTION_INCLUDE_TAG)) { reqBuilder.addClientCapability(OPTION_INCLUDE_TAG); - } else if (line.equals(OPTION_OFS_DELTA)) { + } else if (line2.equals(OPTION_OFS_DELTA)) { reqBuilder.addClientCapability(OPTION_OFS_DELTA); - } else if (line.startsWith("shallow ")) { //$NON-NLS-1$ + } else if (line2.startsWith("shallow ")) { //$NON-NLS-1$ reqBuilder.addClientShallowCommit( - ObjectId.fromString(line.substring(8))); - } else if (line.startsWith("deepen ")) { //$NON-NLS-1$ - int parsedDepth = Integer.parseInt(line.substring(7)); + ObjectId.fromString(line2.substring(8))); + } else if (line2.startsWith("deepen ")) { //$NON-NLS-1$ + int parsedDepth = Integer.parseInt(line2.substring(7)); if (parsedDepth <= 0) { throw new PackProtocolException( MessageFormat.format(JGitText.get().invalidDepth, @@ -181,19 +182,19 @@ final class ProtocolV2Parser { JGitText.get().deepenNotWithDeepen); } reqBuilder.setDepth(parsedDepth); - } else if (line.startsWith("deepen-not ")) { //$NON-NLS-1$ - reqBuilder.addDeepenNotRef(line.substring(11)); + } else if (line2.startsWith("deepen-not ")) { //$NON-NLS-1$ + reqBuilder.addDeepenNotRef(line2.substring(11)); if (reqBuilder.getDepth() != 0) { throw new PackProtocolException( JGitText.get().deepenNotWithDeepen); } - } else if (line.equals(OPTION_DEEPEN_RELATIVE)) { + } else if (line2.equals(OPTION_DEEPEN_RELATIVE)) { reqBuilder.addClientCapability(OPTION_DEEPEN_RELATIVE); - } else if (line.startsWith("deepen-since ")) { //$NON-NLS-1$ - int ts = Integer.parseInt(line.substring(13)); + } else if (line2.startsWith("deepen-since ")) { //$NON-NLS-1$ + int ts = Integer.parseInt(line2.substring(13)); if (ts <= 0) { throw new PackProtocolException(MessageFormat - .format(JGitText.get().invalidTimestamp, line)); + .format(JGitText.get().invalidTimestamp, line2)); } if (reqBuilder.getDepth() != 0) { throw new PackProtocolException( @@ -201,17 +202,17 @@ final class ProtocolV2Parser { } reqBuilder.setDeepenSince(ts); } else if (transferConfig.isAllowFilter() - && line.startsWith(OPTION_FILTER + ' ')) { + && line2.startsWith(OPTION_FILTER + ' ')) { if (filterReceived) { throw new PackProtocolException( JGitText.get().tooManyFilters); } filterReceived = true; reqBuilder.setFilterSpec(FilterSpec.fromFilterLine( - line.substring(OPTION_FILTER.length() + 1))); + line2.substring(OPTION_FILTER.length() + 1))); } else { throw new PackProtocolException(MessageFormat - .format(JGitText.get().unexpectedPacketLine, line)); + .format(JGitText.get().unexpectedPacketLine, line2)); } } @@ -244,25 +245,25 @@ final class ProtocolV2Parser { serverOption -> builder.addServerOption(serverOption), agent -> builder.setAgent(agent)); - if (line == PacketLineIn.END) { + if (PacketLineIn.isEnd(line)) { return builder.build(); } - if (line != PacketLineIn.DELIM) { + if (!PacketLineIn.isDelimiter(line)) { throw new PackProtocolException(MessageFormat .format(JGitText.get().unexpectedPacketLine, line)); } - while ((line = pckIn.readString()) != PacketLineIn.END) { - if (line.equals("peel")) { //$NON-NLS-1$ + for (String line2 : pckIn.readStrings()) { + if (line2.equals("peel")) { //$NON-NLS-1$ builder.setPeel(true); - } else if (line.equals("symrefs")) { //$NON-NLS-1$ + } else if (line2.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 if (line2.startsWith("ref-prefix ")) { //$NON-NLS-1$ + prefixes.add(line2.substring("ref-prefix ".length())); //$NON-NLS-1$ } else { throw new PackProtocolException(MessageFormat - .format(JGitText.get().unexpectedPacketLine, line)); + .format(JGitText.get().unexpectedPacketLine, line2)); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java index 4652c3fda8..d6adf1e0d8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -348,7 +348,7 @@ public class ReceivePack extends BaseReceivePack { pushOptions = new ArrayList<>(4); for (;;) { String option = in.readString(); - if (option == PacketLineIn.END) { + if (PacketLineIn.isEnd(option)) { break; } pushOptions.add(option); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index b752a65275..27ab879515 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -54,8 +54,11 @@ import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT; import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING; import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING; import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE; +import static org.eclipse.jgit.util.HttpSupport.HDR_COOKIE; import static org.eclipse.jgit.util.HttpSupport.HDR_LOCATION; import static org.eclipse.jgit.util.HttpSupport.HDR_PRAGMA; +import static org.eclipse.jgit.util.HttpSupport.HDR_SET_COOKIE; +import static org.eclipse.jgit.util.HttpSupport.HDR_SET_COOKIE2; import static org.eclipse.jgit.util.HttpSupport.HDR_USER_AGENT; import static org.eclipse.jgit.util.HttpSupport.HDR_WWW_AUTHENTICATE; import static org.eclipse.jgit.util.HttpSupport.METHOD_GET; @@ -68,11 +71,15 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.net.HttpCookie; import java.net.MalformedURLException; import java.net.Proxy; import java.net.ProxySelector; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.cert.CertPathBuilderException; import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; @@ -84,6 +91,8 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -100,6 +109,8 @@ import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.RefDirectory; +import org.eclipse.jgit.internal.transport.http.NetscapeCookieFile; +import org.eclipse.jgit.internal.transport.http.NetscapeCookieFileCache; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; @@ -116,6 +127,7 @@ import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.HttpSupport; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.io.DisabledOutputStream; @@ -274,6 +286,19 @@ public class TransportHttp extends HttpTransport implements WalkTransport, private boolean sslFailure = false; + /** + * All stored cookies bound to this repo (independent of the baseUrl) + */ + private final NetscapeCookieFile cookieFile; + + /** + * The cookies to be sent with each request to the given {@link #baseUrl}. + * Filtered view on top of {@link #cookieFile} where only cookies which + * apply to the current url are left. This set needs to be filtered for + * expired entries each time prior to sending them. + */ + private final Set<HttpCookie> relevantCookies; + TransportHttp(Repository local, URIish uri) throws NotSupportedException { super(local, uri); @@ -281,6 +306,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport, http = new HttpConfig(local.getConfig(), uri); proxySelector = ProxySelector.getDefault(); sslVerify = http.isSslVerify(); + cookieFile = getCookieFileFromConfig(http); + relevantCookies = filterCookies(cookieFile, baseUrl); } private URL toURL(URIish urish) throws MalformedURLException { @@ -321,6 +348,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport, http = new HttpConfig(uri); proxySelector = ProxySelector.getDefault(); sslVerify = http.isSslVerify(); + cookieFile = getCookieFileFromConfig(http); + relevantCookies = filterCookies(cookieFile, baseUrl); } /** @@ -508,6 +537,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, conn.setRequestProperty(HDR_ACCEPT, "*/*"); //$NON-NLS-1$ } final int status = HttpSupport.response(conn); + processResponseCookies(conn); switch (status) { case HttpConnection.HTTP_OK: // Check if HttpConnection did some authentication in the @@ -596,6 +626,57 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } } + void processResponseCookies(HttpConnection conn) { + if (cookieFile != null && http.getSaveCookies()) { + List<HttpCookie> foundCookies = new LinkedList<>(); + + List<String> cookieHeaderValues = conn + .getHeaderFields(HDR_SET_COOKIE); + if (!cookieHeaderValues.isEmpty()) { + foundCookies.addAll( + extractCookies(HDR_SET_COOKIE, cookieHeaderValues)); + } + cookieHeaderValues = conn.getHeaderFields(HDR_SET_COOKIE2); + if (!cookieHeaderValues.isEmpty()) { + foundCookies.addAll( + extractCookies(HDR_SET_COOKIE2, cookieHeaderValues)); + } + if (!foundCookies.isEmpty()) { + try { + // update cookie lists with the newly received cookies! + Set<HttpCookie> cookies = cookieFile.getCookies(false); + cookies.addAll(foundCookies); + cookieFile.write(baseUrl); + relevantCookies.addAll(foundCookies); + } catch (IOException | IllegalArgumentException + | InterruptedException e) { + LOG.warn(MessageFormat.format( + JGitText.get().couldNotPersistCookies, + cookieFile.getPath()), e); + } + } + } + } + + private List<HttpCookie> extractCookies(String headerKey, + List<String> headerValues) { + List<HttpCookie> foundCookies = new LinkedList<>(); + for (String headerValue : headerValues) { + foundCookies + .addAll(HttpCookie.parse(headerKey + ':' + headerValue)); + } + // HttpCookies.parse(...) is only compliant with RFC 2965. Make it RFC + // 6265 compliant by applying the logic from + // https://tools.ietf.org/html/rfc6265#section-5.2.3 + for (HttpCookie foundCookie : foundCookies) { + String domain = foundCookie.getDomain(); + if (domain != null && domain.startsWith(".")) { //$NON-NLS-1$ + foundCookie.setDomain(domain.substring(1)); + } + } + return foundCookies; + } + private static class CredentialItems { CredentialItem.InformationalMessage message; @@ -847,14 +928,35 @@ public class TransportHttp extends HttpTransport implements WalkTransport, conn.setConnectTimeout(effTimeOut); conn.setReadTimeout(effTimeOut); } + // set cookie header if necessary + if (!relevantCookies.isEmpty()) { + setCookieHeader(conn); + } + if (this.headers != null && !this.headers.isEmpty()) { - for (Map.Entry<String, String> entry : this.headers.entrySet()) + for (Map.Entry<String, String> entry : this.headers.entrySet()) { conn.setRequestProperty(entry.getKey(), entry.getValue()); + } } authMethod.configureRequest(conn); return conn; } + private void setCookieHeader(HttpConnection conn) { + StringBuilder cookieHeaderValue = new StringBuilder(); + for (HttpCookie cookie : relevantCookies) { + if (!cookie.hasExpired()) { + if (cookieHeaderValue.length() > 0) { + cookieHeaderValue.append(';'); + } + cookieHeaderValue.append(cookie.toString()); + } + } + if (cookieHeaderValue.length() > 0) { + conn.setRequestProperty(HDR_COOKIE, cookieHeaderValue.toString()); + } + } + final InputStream openInputStream(HttpConnection conn) throws IOException { InputStream input = conn.getInputStream(); @@ -868,6 +970,150 @@ public class TransportHttp extends HttpTransport implements WalkTransport, return new TransportException(uri, why); } + private static NetscapeCookieFile getCookieFileFromConfig( + HttpConfig config) { + if (!StringUtils.isEmptyOrNull(config.getCookieFile())) { + try { + Path cookieFilePath = Paths.get(config.getCookieFile()); + return NetscapeCookieFileCache.getInstance(config) + .getEntry(cookieFilePath); + } catch (InvalidPathException e) { + LOG.warn(MessageFormat.format( + JGitText.get().couldNotReadCookieFile, + config.getCookieFile()), e); + } + } + return null; + } + + private static Set<HttpCookie> filterCookies(NetscapeCookieFile cookieFile, + URL url) { + if (cookieFile != null) { + return filterCookies(cookieFile.getCookies(true), url); + } + return Collections.emptySet(); + } + + /** + * + * @param allCookies + * a list of cookies. + * @param url + * the url for which to filter the list of cookies. + * @return only the cookies from {@code allCookies} which are relevant (i.e. + * are not expired, have a matching domain, have a matching path and + * have a matching secure attribute) + */ + private static Set<HttpCookie> filterCookies(Set<HttpCookie> allCookies, + URL url) { + Set<HttpCookie> filteredCookies = new HashSet<>(); + for (HttpCookie cookie : allCookies) { + if (cookie.hasExpired()) { + continue; + } + if (!matchesCookieDomain(url.getHost(), cookie.getDomain())) { + continue; + } + if (!matchesCookiePath(url.getPath(), cookie.getPath())) { + continue; + } + if (cookie.getSecure() && !"https".equals(url.getProtocol())) { //$NON-NLS-1$ + continue; + } + filteredCookies.add(cookie); + } + return filteredCookies; + } + + /** + * + * The utility method to check whether a host name is in a cookie's domain + * or not. Similar to {@link HttpCookie#domainMatches(String, String)} but + * implements domain matching rules according to + * <a href="https://tools.ietf.org/html/rfc6265#section-5.1.3">RFC 6265, + * section 5.1.3</a> instead of the rules from + * <a href="https://tools.ietf.org/html/rfc2965#section-3.3">RFC 2965, + * section 3.3.1</a>. + * <p> + * The former rules are also used by libcurl internally. + * <p> + * The rules are as follows + * + * A string matches another domain string if at least one of the following + * conditions holds: + * <ul> + * <li>The domain string and the string are identical. (Note that both the + * domain string and the string will have been canonicalized to lower case + * at this point.)</li> + * <li>All of the following conditions hold + * <ul> + * <li>The domain string is a suffix of the string.</li> + * <li>The last character of the string that is not included in the domain + * string is a %x2E (".") character.</li> + * <li>The string is a host name (i.e., not an IP address).</li> + * </ul> + * </li> + * </ul> + * + * @param host + * the host to compare against the cookieDomain + * @param cookieDomain + * the domain to compare against + * @return {@code true} if they domain-match; {@code false} if not + * + * @see <a href= "https://tools.ietf.org/html/rfc6265#section-5.1.3">RFC + * 6265, section 5.1.3 (Domain Matching)</a> + * @see <a href= + * "https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8206092">JDK-8206092 + * : HttpCookie.domainMatches() does not match to sub-sub-domain</a> + */ + static boolean matchesCookieDomain(String host, String cookieDomain) { + cookieDomain = cookieDomain.toLowerCase(Locale.ROOT); + host = host.toLowerCase(Locale.ROOT); + if (host.equals(cookieDomain)) { + return true; + } else { + if (!host.endsWith(cookieDomain)) { + return false; + } + return host + .charAt(host.length() - cookieDomain.length() - 1) == '.'; + } + } + + /** + * The utility method to check whether a path is matching a cookie path + * domain or not. The rules are defined by + * <a href="https://tools.ietf.org/html/rfc6265#section-5.1.4">RFC 6265, + * section 5.1.4</a>: + * + * A request-path path-matches a given cookie-path if at least one of the + * following conditions holds: + * <ul> + * <li>The cookie-path and the request-path are identical.</li> + * <li>The cookie-path is a prefix of the request-path, and the last + * character of the cookie-path is %x2F ("/").</li> + * <li>The cookie-path is a prefix of the request-path, and the first + * character of the request-path that is not included in the cookie- path is + * a %x2F ("/") character.</li> + * </ul> + * @param path + * the path to check + * @param cookiePath + * the cookie's path + * + * @return {@code true} if they path-match; {@code false} if not + */ + static boolean matchesCookiePath(String path, String cookiePath) { + if (cookiePath.equals(path)) { + return true; + } + if (!cookiePath.endsWith("/")) { //$NON-NLS-1$ + cookiePath += "/"; //$NON-NLS-1$ + } + return path.startsWith(cookiePath); + } + private boolean isSmartHttp(HttpConnection c, String service) { final String expType = "application/x-" + service + "-advertisement"; //$NON-NLS-1$ //$NON-NLS-2$ final String actType = c.getContentType(); @@ -902,7 +1148,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, JGitText.get().expectedGot, exp, act)); } - while (pckIn.readString() != PacketLineIn.END) { + while (!PacketLineIn.isEnd(pckIn.readString())) { // for now, ignore the remaining header lines } } 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 3ed9886c45..9278f42adf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -106,10 +106,8 @@ import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.AsyncRevObjectQueue; import org.eclipse.jgit.revwalk.BitmapWalker; -import org.eclipse.jgit.revwalk.BitmappedReachabilityChecker; import org.eclipse.jgit.revwalk.DepthWalk; import org.eclipse.jgit.revwalk.ObjectWalk; -import org.eclipse.jgit.revwalk.PedestrianReachabilityChecker; import org.eclipse.jgit.revwalk.ReachabilityChecker; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; @@ -1231,7 +1229,7 @@ public class UploadPack { /* EOF when awaiting command is fine */ return true; } - if (command == PacketLineIn.END) { + if (PacketLineIn.isEnd(command)) { // A blank request is valid according // to the protocol; do nothing in this // case. @@ -1602,7 +1600,7 @@ public class UploadPack { throw eof; } - if (line == PacketLineIn.END) { + if (PacketLineIn.isEnd(line)) { last = processHaveLines(peerHas, last, pckOut); if (commonBase.isEmpty() || multiAck != MultiAck.OFF) pckOut.writeString("NAK\n"); //$NON-NLS-1$ @@ -1909,9 +1907,8 @@ public class UploadPack { } // All wants are commits, we can use ReachabilityChecker - ReachabilityChecker reachabilityChecker = repoHasBitmaps - ? new BitmappedReachabilityChecker(walk) - : new PedestrianReachabilityChecker(true, walk); + ReachabilityChecker reachabilityChecker = walk + .createReachabilityChecker(); List<RevCommit> starters = objectIdsToRevCommits(walk, reachableFrom); 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 c0c24872b2..3efa66459c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -1516,6 +1516,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { ObjectId blobId = entry.getObjectId(); if (entry.getStage() > 0 && entry.getStage() != DirCacheEntry.STAGE_2) { + blobId = null; // Merge conflict: check ours (stage 2) byte[] name = entry.getRawPath(); int i = 0; @@ -1523,7 +1524,8 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { dirCache.next(1); i++; entry = dirCache.getDirCacheEntry(); - if (!Arrays.equals(name, entry.getRawPath())) { + if (entry == null + || !Arrays.equals(name, entry.getRawPath())) { break; } if (entry.getStage() == DirCacheEntry.STAGE_2) { @@ -1533,17 +1535,20 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } dirCache.back(i); } - try (ObjectReader reader = repository.newObjectReader()) { - ObjectLoader loader = reader.open(blobId, Constants.OBJ_BLOB); - try { - return RawText.isCrLfText(loader.getCachedBytes()); - } catch (LargeObjectException e) { - try (InputStream in = loader.openStream()) { - return RawText.isCrLfText(in); + if (blobId != null) { + try (ObjectReader reader = repository.newObjectReader()) { + ObjectLoader loader = reader.open(blobId, + Constants.OBJ_BLOB); + try { + return RawText.isCrLfText(loader.getCachedBytes()); + } catch (LargeObjectException e) { + try (InputStream in = loader.openStream()) { + return RawText.isCrLfText(in); + } } + } catch (IOException e) { + // Ignore and return false below } - } catch (IOException e) { - // Ignore and return false below } } return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java index 716711e067..faef9fd0f4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java @@ -48,7 +48,8 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.nio.charset.Charset; -import java.nio.file.AccessDeniedException; +import java.nio.file.FileStore; +import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -57,9 +58,11 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.errors.JGitInternalException; @@ -84,7 +87,7 @@ public class FS_POSIX extends FS { private static final int DEFAULT_UMASK = 0022; private volatile int umask = -1; - private volatile boolean supportsUnixNLink = true; + private static final Map<FileStore, Boolean> CAN_HARD_LINK = new ConcurrentHashMap<>(); private volatile AtomicFileCreation supportsAtomicCreateNewFile = AtomicFileCreation.UNDEFINED; @@ -388,12 +391,18 @@ public class FS_POSIX extends FS { if (!lock.createNewFile()) { return false; } - if (supportsAtomicCreateNewFile() || !supportsUnixNLink) { + if (supportsAtomicCreateNewFile()) { return true; } Path lockPath = lock.toPath(); Path link = null; + FileStore store = Files.getFileStore(lockPath); try { + Boolean canLink = CAN_HARD_LINK.computeIfAbsent(store, + s -> Boolean.TRUE); + if (Boolean.FALSE.equals(canLink)) { + return true; + } link = Files.createLink( Paths.get(lock.getAbsolutePath() + ".lnk"), //$NON-NLS-1$ lockPath); @@ -405,11 +414,11 @@ public class FS_POSIX extends FS { nlink)); return false; } else if (nlink < 2) { - supportsUnixNLink = false; + CAN_HARD_LINK.put(store, Boolean.FALSE); } return true; } catch (UnsupportedOperationException | IllegalArgumentException e) { - supportsUnixNLink = false; + CAN_HARD_LINK.put(store, Boolean.FALSE); return true; } finally { if (link != null) { @@ -448,12 +457,18 @@ public class FS_POSIX extends FS { if (!file.createNewFile()) { return token(false, null); } - if (supportsAtomicCreateNewFile() || !supportsUnixNLink) { + if (supportsAtomicCreateNewFile()) { return token(true, null); } Path link = null; Path path = file.toPath(); + FileStore store = Files.getFileStore(path); try { + Boolean canLink = CAN_HARD_LINK.computeIfAbsent(store, + s -> Boolean.TRUE); + if (Boolean.FALSE.equals(canLink)) { + return token(true, null); + } link = Files.createLink(Paths.get(uniqueLinkPath(file)), path); Integer nlink = (Integer) (Files.getAttribute(path, "unix:nlink")); //$NON-NLS-1$ @@ -462,12 +477,12 @@ public class FS_POSIX extends FS { JGitText.get().failedAtomicFileCreation, path, nlink)); return token(false, link); } else if (nlink.intValue() < 2) { - supportsUnixNLink = false; + CAN_HARD_LINK.put(store, Boolean.FALSE); } return token(true, link); } catch (UnsupportedOperationException | IllegalArgumentException - | AccessDeniedException | SecurityException e) { - supportsUnixNLink = false; + | FileSystemException | SecurityException e) { + CAN_HARD_LINK.put(store, Boolean.FALSE); return token(true, link); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java index 98797dc64f..3ccbd72806 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java @@ -205,18 +205,21 @@ public class FS_Win32 extends FS { @Override protected File userHomeImpl() { String home = SystemReader.getInstance().getenv("HOME"); //$NON-NLS-1$ - if (home != null) + if (home != null) { return resolve(null, home); + } String homeDrive = SystemReader.getInstance().getenv("HOMEDRIVE"); //$NON-NLS-1$ if (homeDrive != null) { String homePath = SystemReader.getInstance().getenv("HOMEPATH"); //$NON-NLS-1$ - if (homePath != null) + if (homePath != null) { return new File(homeDrive, homePath); + } } String homeShare = SystemReader.getInstance().getenv("HOMESHARE"); //$NON-NLS-1$ - if (homeShare != null) + if (homeShare != null) { return new File(homeShare); + } return super.userHomeImpl(); } @@ -237,8 +240,9 @@ public class FS_Win32 extends FS { /** {@inheritDoc} */ @Override public boolean supportsSymlinks() { - if (supportSymlinks == null) + if (supportSymlinks == null) { detectSymlinkSupport(); + } return Boolean.TRUE.equals(supportSymlinks); } @@ -254,12 +258,13 @@ public class FS_Win32 extends FS { | InternalError e) { supportSymlinks = Boolean.FALSE; } finally { - if (tempFile != null) + if (tempFile != null) { try { FileUtils.delete(tempFile); } catch (IOException e) { throw new RuntimeException(e); // panic } + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java index 54e4ee01fd..640670debc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java @@ -170,6 +170,27 @@ public class HttpSupport { public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$ /** + * The {@code Cookie} header. + * + * @since 5.4 + */ + public static final String HDR_COOKIE = "Cookie"; //$NON-NLS-1$ + + /** + * The {@code Set-Cookie} header. + * + * @since 5.4 + */ + public static final String HDR_SET_COOKIE = "Set-Cookie"; //$NON-NLS-1$ + + /** + * The {@code Set-Cookie2} header. + * + * @since 5.4 + */ + public static final String HDR_SET_COOKIE2 = "Set-Cookie2"; //$NON-NLS-1$ + + /** * URL encode a value string into an output buffer. * * @param urlstr diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java new file mode 100644 index 0000000000..41c15363f8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +import java.util.LinkedHashMap; + +/** + * Map with only up to n entries. If a new entry is added so that the map + * contains more than those n entries the least-recently used entry is removed + * from the map. + * + * @param <K> + * the type of keys maintained by this map + * @param <V> + * the type of mapped values + * + * @since 5.4 + */ +public class LRUMap<K, V> extends LinkedHashMap<K, V> { + + private static final long serialVersionUID = 4329609127403759486L; + + private final int limit; + + /** + * Constructs an empty map which may contain at most the given amount of + * entries. + * + * @param initialCapacity + * the initial capacity + * @param limit + * the number of entries the map should have at most + */ + public LRUMap(int initialCapacity, int limit) { + super(initialCapacity, 0.75f, true); + this.limit = limit; + } + + @Override + protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) { + return size() > limit; + } +} |