aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit')
-rw-r--r--org.eclipse.jgit/.settings/.api_filters11
-rw-r--r--org.eclipse.jgit/META-INF/MANIFEST.MF102
-rw-r--r--org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF4
-rw-r--r--org.eclipse.jgit/pom.xml2
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties16
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java559
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java70
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java38
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java34
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java34
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java89
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java24
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java56
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java42
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java118
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java16
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java206
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java8
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java106
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java135
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java158
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java24
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/Base85.java195
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java19
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryDeltaInputStream.java211
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java118
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkOutputStream.java116
35 files changed, 2212 insertions, 388 deletions
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
new file mode 100644
index 0000000000..33331fbab7
--- /dev/null
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<component id="org.eclipse.jgit" version="2">
+ <resource path="src/org/eclipse/jgit/transport/SshConstants.java" type="org.eclipse.jgit.transport.SshConstants">
+ <filter id="1142947843">
+ <message_arguments>
+ <message_argument value="5.11.1"/>
+ <message_argument value="PUBKEY_ACCEPTED_ALGORITHMS"/>
+ </message_arguments>
+ </filter>
+ </resource>
+</component>
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 9bcca38bd9..9a5793ab25 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -3,12 +3,12 @@ Bundle-ManifestVersion: 2
Bundle-Name: %Bundle-Name
Automatic-Module-Name: org.eclipse.jgit
Bundle-SymbolicName: org.eclipse.jgit
-Bundle-Version: 5.11.2.qualifier
+Bundle-Version: 5.12.1.qualifier
Bundle-Localization: plugin
Bundle-Vendor: %Bundle-Vendor
Eclipse-ExtensibleAPI: true
-Export-Package: org.eclipse.jgit.annotations;version="5.11.2",
- org.eclipse.jgit.api;version="5.11.2";
+Export-Package: org.eclipse.jgit.annotations;version="5.12.1",
+ org.eclipse.jgit.api;version="5.12.1";
uses:="org.eclipse.jgit.transport,
org.eclipse.jgit.notes,
org.eclipse.jgit.dircache,
@@ -23,18 +23,18 @@ Export-Package: org.eclipse.jgit.annotations;version="5.11.2",
org.eclipse.jgit.revwalk.filter,
org.eclipse.jgit.blame,
org.eclipse.jgit.merge",
- org.eclipse.jgit.api.errors;version="5.11.2";
+ org.eclipse.jgit.api.errors;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.errors",
- org.eclipse.jgit.attributes;version="5.11.2";
+ org.eclipse.jgit.attributes;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.treewalk",
- org.eclipse.jgit.blame;version="5.11.2";
+ org.eclipse.jgit.blame;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.revwalk,
org.eclipse.jgit.treewalk.filter,
org.eclipse.jgit.diff",
- org.eclipse.jgit.diff;version="5.11.2";
+ org.eclipse.jgit.diff;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.attributes,
org.eclipse.jgit.revwalk,
@@ -42,44 +42,44 @@ Export-Package: org.eclipse.jgit.annotations;version="5.11.2",
org.eclipse.jgit.treewalk.filter,
org.eclipse.jgit.treewalk,
org.eclipse.jgit.util",
- org.eclipse.jgit.dircache;version="5.11.2";
+ org.eclipse.jgit.dircache;version="5.12.1";
uses:="org.eclipse.jgit.events,
org.eclipse.jgit.lib,
org.eclipse.jgit.attributes,
org.eclipse.jgit.treewalk,
org.eclipse.jgit.util",
- org.eclipse.jgit.errors;version="5.11.2";
+ org.eclipse.jgit.errors;version="5.12.1";
uses:="org.eclipse.jgit.transport,
org.eclipse.jgit.dircache,
org.eclipse.jgit.lib,
org.eclipse.jgit.internal.storage.pack",
- org.eclipse.jgit.events;version="5.11.2";
+ org.eclipse.jgit.events;version="5.12.1";
uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.fnmatch;version="5.11.2",
- org.eclipse.jgit.gitrepo;version="5.11.2";
+ org.eclipse.jgit.fnmatch;version="5.12.1",
+ org.eclipse.jgit.gitrepo;version="5.12.1";
uses:="org.xml.sax.helpers,
org.eclipse.jgit.api,
org.eclipse.jgit.lib,
org.eclipse.jgit.revwalk,
org.xml.sax",
- org.eclipse.jgit.gitrepo.internal;version="5.11.2";x-internal:=true,
- org.eclipse.jgit.hooks;version="5.11.2";uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.ignore;version="5.11.2",
- org.eclipse.jgit.ignore.internal;version="5.11.2";
+ org.eclipse.jgit.gitrepo.internal;version="5.12.1";x-internal:=true,
+ org.eclipse.jgit.hooks;version="5.12.1";uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.ignore;version="5.12.1",
+ org.eclipse.jgit.ignore.internal;version="5.12.1";
x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal;version="5.11.2";
+ org.eclipse.jgit.internal;version="5.12.1";
x-friends:="org.eclipse.jgit.test,
org.eclipse.jgit.http.test",
- org.eclipse.jgit.internal.fsck;version="5.11.2";
+ org.eclipse.jgit.internal.fsck;version="5.12.1";
x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.revwalk;version="5.11.2";
+ org.eclipse.jgit.internal.revwalk;version="5.12.1";
x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.storage.dfs;version="5.11.2";
+ org.eclipse.jgit.internal.storage.dfs;version="5.12.1";
x-friends:="org.eclipse.jgit.test,
org.eclipse.jgit.http.server,
org.eclipse.jgit.http.test,
org.eclipse.jgit.lfs.test",
- org.eclipse.jgit.internal.storage.file;version="5.11.2";
+ org.eclipse.jgit.internal.storage.file;version="5.12.1";
x-friends:="org.eclipse.jgit.test,
org.eclipse.jgit.junit,
org.eclipse.jgit.junit.http,
@@ -88,31 +88,31 @@ Export-Package: org.eclipse.jgit.annotations;version="5.11.2",
org.eclipse.jgit.pgm,
org.eclipse.jgit.pgm.test,
org.eclipse.jgit.ssh.apache",
- org.eclipse.jgit.internal.storage.io;version="5.11.2";
+ org.eclipse.jgit.internal.storage.io;version="5.12.1";
x-friends:="org.eclipse.jgit.junit,
org.eclipse.jgit.test,
org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.pack;version="5.11.2";
+ org.eclipse.jgit.internal.storage.pack;version="5.12.1";
x-friends:="org.eclipse.jgit.junit,
org.eclipse.jgit.test,
org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.reftable;version="5.11.2";
+ org.eclipse.jgit.internal.storage.reftable;version="5.12.1";
x-friends:="org.eclipse.jgit.http.test,
org.eclipse.jgit.junit,
org.eclipse.jgit.test,
org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.submodule;version="5.11.2";x-internal:=true,
- org.eclipse.jgit.internal.transport.connectivity;version="5.11.2";
+ org.eclipse.jgit.internal.submodule;version="5.12.1";x-internal:=true,
+ org.eclipse.jgit.internal.transport.connectivity;version="5.12.1";
x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.http;version="5.11.2";
+ org.eclipse.jgit.internal.transport.http;version="5.12.1";
x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.parser;version="5.11.2";
+ org.eclipse.jgit.internal.transport.parser;version="5.12.1";
x-friends:="org.eclipse.jgit.http.server,
org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.ssh;version="5.11.2";
+ org.eclipse.jgit.internal.transport.ssh;version="5.12.1";
x-friends:="org.eclipse.jgit.ssh.apache,
org.eclipse.jgit.ssh.jsch",
- org.eclipse.jgit.lib;version="5.11.2";
+ org.eclipse.jgit.lib;version="5.12.1";
uses:="org.eclipse.jgit.transport,
org.eclipse.jgit.util.sha1,
org.eclipse.jgit.dircache,
@@ -126,10 +126,10 @@ Export-Package: org.eclipse.jgit.annotations;version="5.11.2",
org.eclipse.jgit.util,
org.eclipse.jgit.submodule,
org.eclipse.jgit.util.time",
- org.eclipse.jgit.lib.internal;version="5.11.2";
+ org.eclipse.jgit.lib.internal;version="5.12.1";
x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.logging;version="5.11.2",
- org.eclipse.jgit.merge;version="5.11.2";
+ org.eclipse.jgit.logging;version="5.12.1",
+ org.eclipse.jgit.merge;version="5.12.1";
uses:="org.eclipse.jgit.dircache,
org.eclipse.jgit.lib,
org.eclipse.jgit.revwalk,
@@ -138,40 +138,40 @@ Export-Package: org.eclipse.jgit.annotations;version="5.11.2",
org.eclipse.jgit.util,
org.eclipse.jgit.api,
org.eclipse.jgit.attributes",
- org.eclipse.jgit.nls;version="5.11.2",
- org.eclipse.jgit.notes;version="5.11.2";
+ org.eclipse.jgit.nls;version="5.12.1",
+ org.eclipse.jgit.notes;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.revwalk,
org.eclipse.jgit.treewalk,
org.eclipse.jgit.merge",
- org.eclipse.jgit.patch;version="5.11.2";
+ org.eclipse.jgit.patch;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.diff",
- org.eclipse.jgit.revplot;version="5.11.2";
+ org.eclipse.jgit.revplot;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.revwalk",
- org.eclipse.jgit.revwalk;version="5.11.2";
+ org.eclipse.jgit.revwalk;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.diff,
org.eclipse.jgit.treewalk.filter,
org.eclipse.jgit.revwalk.filter,
org.eclipse.jgit.treewalk",
- org.eclipse.jgit.revwalk.filter;version="5.11.2";
+ org.eclipse.jgit.revwalk.filter;version="5.12.1";
uses:="org.eclipse.jgit.revwalk,
org.eclipse.jgit.lib,
org.eclipse.jgit.util",
- org.eclipse.jgit.storage.file;version="5.11.2";
+ org.eclipse.jgit.storage.file;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.util",
- org.eclipse.jgit.storage.pack;version="5.11.2";
+ org.eclipse.jgit.storage.pack;version="5.12.1";
uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.submodule;version="5.11.2";
+ org.eclipse.jgit.submodule;version="5.12.1";
uses:="org.eclipse.jgit.lib,
org.eclipse.jgit.diff,
org.eclipse.jgit.treewalk.filter,
org.eclipse.jgit.treewalk,
org.eclipse.jgit.util",
- org.eclipse.jgit.transport;version="5.11.2";
+ org.eclipse.jgit.transport;version="5.12.1";
uses:="javax.crypto,
org.eclipse.jgit.util.io,
org.eclipse.jgit.lib,
@@ -184,21 +184,21 @@ Export-Package: org.eclipse.jgit.annotations;version="5.11.2",
org.eclipse.jgit.transport.resolver,
org.eclipse.jgit.storage.pack,
org.eclipse.jgit.errors",
- org.eclipse.jgit.transport.http;version="5.11.2";
+ org.eclipse.jgit.transport.http;version="5.12.1";
uses:="javax.net.ssl",
- org.eclipse.jgit.transport.resolver;version="5.11.2";
+ org.eclipse.jgit.transport.resolver;version="5.12.1";
uses:="org.eclipse.jgit.transport,
org.eclipse.jgit.lib",
- org.eclipse.jgit.treewalk;version="5.11.2";
+ org.eclipse.jgit.treewalk;version="5.12.1";
uses:="org.eclipse.jgit.dircache,
org.eclipse.jgit.lib,
org.eclipse.jgit.attributes,
org.eclipse.jgit.revwalk,
org.eclipse.jgit.treewalk.filter,
org.eclipse.jgit.util",
- org.eclipse.jgit.treewalk.filter;version="5.11.2";
+ org.eclipse.jgit.treewalk.filter;version="5.12.1";
uses:="org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util;version="5.11.2";
+ org.eclipse.jgit.util;version="5.12.1";
uses:="org.eclipse.jgit.transport,
org.eclipse.jgit.hooks,
org.eclipse.jgit.revwalk,
@@ -211,12 +211,12 @@ Export-Package: org.eclipse.jgit.annotations;version="5.11.2",
org.eclipse.jgit.treewalk,
javax.net.ssl,
org.eclipse.jgit.util.time",
- org.eclipse.jgit.util.io;version="5.11.2";
+ org.eclipse.jgit.util.io;version="5.12.1";
uses:="org.eclipse.jgit.attributes,
org.eclipse.jgit.lib,
org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util.sha1;version="5.11.2",
- org.eclipse.jgit.util.time;version="5.11.2"
+ org.eclipse.jgit.util.sha1;version="5.12.1",
+ org.eclipse.jgit.util.time;version="5.12.1"
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
javax.crypto,
diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
index 7764fe1d38..03ddbb74f9 100644
--- a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@ Bundle-ManifestVersion: 2
Bundle-Name: org.eclipse.jgit - Sources
Bundle-SymbolicName: org.eclipse.jgit.source
Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 5.11.2.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit;version="5.11.2.qualifier";roots="."
+Bundle-Version: 5.12.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit;version="5.12.1.qualifier";roots="."
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index 6a52c27065..c9fc127aef 100644
--- a/org.eclipse.jgit/pom.xml
+++ b/org.eclipse.jgit/pom.xml
@@ -20,7 +20,7 @@
<parent>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit-parent</artifactId>
- <version>5.11.2-SNAPSHOT</version>
+ <version>5.12.1-SNAPSHOT</version>
</parent>
<artifactId>org.eclipse.jgit</artifactId>
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index b6b10ea506..6180737ffe 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -13,6 +13,9 @@ ambiguousObjectAbbreviation=Object abbreviation {0} is ambiguous
aNewObjectIdIsRequired=A NewObjectId is required.
anExceptionOccurredWhileTryingToAddTheIdOfHEAD=An exception occurred while trying to add the Id of HEAD
anSSHSessionHasBeenAlreadyCreated=An SSH session has been already created
+applyBinaryBaseOidWrong=Cannot apply binary patch; OID for file {0} does not match
+applyBinaryOidTooShort=Binary patch for file {0} does not have full IDs
+applyBinaryResultOidWrong=Result of binary patch for file {0} has wrong OID.
applyingCommit=Applying {0}
archiveFormatAlreadyAbsent=Archive format already absent: {0}
archiveFormatAlreadyRegistered=Archive format already registered with different implementation: {0}
@@ -37,7 +40,19 @@ badRef=Bad ref: {0}: {1}
badSectionEntry=Bad section entry: {0}
badShallowLine=Bad shallow line: {0}
bareRepositoryNoWorkdirAndIndex=Bare Repository has neither a working tree, nor an index
+base85invalidChar=Invalid base-85 character: 0x{0}
+base85length=Base-85 encoded data must have a length that is a multiple of 5
+base85overflow=Base-85 value overflow, does not fit into 32 bits: 0x{0}
+base85tooLong=Extra base-85 encoded data for output size of {0} bytes
+base85tooShort=Base-85 data decoded into less than {0} bytes
baseLengthIncorrect=base length incorrect
+binaryDeltaBaseLengthMismatch=Binary delta base length does not match, expected {0}, got {1}
+binaryDeltaInvalidOffset=Binary delta offset + length too large: {0} + {1}
+binaryDeltaInvalidResultLength=Binary delta expected result length is negative
+binaryHunkDecodeError=Binary hunk, line {0}: invalid input
+binaryHunkInvalidLength=Binary hunk, line {0}: input corrupt; expected length byte, got 0x{1}
+binaryHunkLineTooShort=Binary hunk, line {0}: input ended prematurely
+binaryHunkMissingNewline=Binary hunk, line {0}: input line not terminated by newline
bitmapMissingObject=Bitmap at {0} is missing {1}.
bitmapsMustBePrepared=Bitmaps must be prepared before they may be written.
blameNotCommittedYet=Not Committed Yet
@@ -139,6 +154,7 @@ configHandleMayBeLocked=config file handle may be locked by other process, {0}.
connectionFailed=connection failed
connectionTimeOut=Connection time out: {0}
contextMustBeNonNegative=context must be >= 0
+cookieFilePathRelative=git config http.cookieFile contains a relative path, should be absolute: {0}
corruptionDetectedReReadingAt=Corruption detected re-reading at {0}
corruptObjectBadDate=bad date
corruptObjectBadEmail=bad email
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
index e228e8276a..583767af3f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011, 2020 IBM Corporation and others
+ * Copyright (C) 2011, 2021 IBM Corporation and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -9,29 +9,68 @@
*/
package org.eclipse.jgit.api;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.Writer;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.zip.InflaterInputStream;
+import org.eclipse.jgit.api.errors.FilterFailedException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.api.errors.PatchFormatException;
+import org.eclipse.jgit.attributes.FilterCommand;
+import org.eclipse.jgit.attributes.FilterCommandRegistry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectStream;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.patch.BinaryHunk;
import org.eclipse.jgit.patch.FileHeader;
+import org.eclipse.jgit.patch.FileHeader.PatchType;
import org.eclipse.jgit.patch.HunkHeader;
import org.eclipse.jgit.patch.Patch;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
+import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
+import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
+import org.eclipse.jgit.util.io.BinaryDeltaInputStream;
+import org.eclipse.jgit.util.io.BinaryHunkInputStream;
+import org.eclipse.jgit.util.io.EolStreamTypeUtil;
+import org.eclipse.jgit.util.sha1.SHA1;
/**
* Apply a patch to files and/or to the index.
@@ -45,7 +84,7 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
private InputStream in;
/**
- * Constructs the command if the patch is to be applied to the index.
+ * Constructs the command.
*
* @param repo
*/
@@ -79,6 +118,7 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
public ApplyResult call() throws GitAPIException, PatchFormatException,
PatchApplyException {
checkCallable();
+ setCallable(false);
ApplyResult r = new ApplyResult();
try {
final Patch p = new Patch();
@@ -87,19 +127,22 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
} finally {
in.close();
}
- if (!p.getErrors().isEmpty())
+ if (!p.getErrors().isEmpty()) {
throw new PatchFormatException(p.getErrors());
+ }
+ Repository repository = getRepository();
+ DirCache cache = repository.readDirCache();
for (FileHeader fh : p.getFiles()) {
ChangeType type = fh.getChangeType();
File f = null;
switch (type) {
case ADD:
f = getFile(fh.getNewPath(), true);
- apply(f, fh);
+ apply(repository, fh.getNewPath(), cache, f, fh);
break;
case MODIFY:
f = getFile(fh.getOldPath(), false);
- apply(f, fh);
+ apply(repository, fh.getOldPath(), cache, f, fh);
break;
case DELETE:
f = getFile(fh.getOldPath(), false);
@@ -118,14 +161,14 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
throw new PatchApplyException(MessageFormat.format(
JGitText.get().renameFileFailed, f, dest), e);
}
- apply(dest, fh);
+ apply(repository, fh.getOldPath(), cache, dest, fh);
break;
case COPY:
f = getFile(fh.getOldPath(), false);
File target = getFile(fh.getNewPath(), false);
FileUtils.mkdirs(target.getParentFile(), true);
Files.copy(f.toPath(), target.toPath());
- apply(target, fh);
+ apply(repository, fh.getOldPath(), cache, target, fh);
}
r.addUpdatedFile(f);
}
@@ -133,14 +176,13 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
throw new PatchApplyException(MessageFormat.format(
JGitText.get().patchApplyException, e.getMessage()), e);
}
- setCallable(false);
return r;
}
private File getFile(String path, boolean create)
throws PatchApplyException {
File f = new File(getRepository().getWorkTree(), path);
- if (create)
+ if (create) {
try {
File parent = f.getParentFile();
FileUtils.mkdirs(parent, true);
@@ -149,22 +191,366 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
throw new PatchApplyException(MessageFormat.format(
JGitText.get().createNewFileFailed, f), e);
}
+ }
return f;
}
+ private void apply(Repository repository, String path, DirCache cache,
+ File f, FileHeader fh) throws IOException, PatchApplyException {
+ if (PatchType.BINARY.equals(fh.getPatchType())) {
+ return;
+ }
+ boolean convertCrLf = needsCrLfConversion(f, fh);
+ // Use a TreeWalk with a DirCacheIterator to pick up the correct
+ // clean/smudge filters. CR-LF handling is completely determined by
+ // whether the file or the patch have CR-LF line endings.
+ try (TreeWalk walk = new TreeWalk(repository)) {
+ walk.setOperationType(OperationType.CHECKIN_OP);
+ FileTreeIterator files = new FileTreeIterator(repository);
+ int fileIdx = walk.addTree(files);
+ int cacheIdx = walk.addTree(new DirCacheIterator(cache));
+ files.setDirCacheIterator(walk, cacheIdx);
+ walk.setFilter(AndTreeFilter.create(
+ PathFilterGroup.createFromStrings(path),
+ new NotIgnoredFilter(fileIdx)));
+ walk.setRecursive(true);
+ if (walk.next()) {
+ // If the file on disk has no newline characters, convertCrLf
+ // will be false. In that case we want to honor the normal git
+ // settings.
+ EolStreamType streamType = convertCrLf ? EolStreamType.TEXT_CRLF
+ : walk.getEolStreamType(OperationType.CHECKOUT_OP);
+ String command = walk.getFilterCommand(
+ Constants.ATTR_FILTER_TYPE_SMUDGE);
+ CheckoutMetadata checkOut = new CheckoutMetadata(streamType, command);
+ FileTreeIterator file = walk.getTree(fileIdx,
+ FileTreeIterator.class);
+ if (file != null) {
+ if (PatchType.GIT_BINARY.equals(fh.getPatchType())) {
+ applyBinary(repository, path, f, fh,
+ file::openEntryStream, file.getEntryObjectId(),
+ checkOut);
+ } else {
+ command = walk.getFilterCommand(
+ Constants.ATTR_FILTER_TYPE_CLEAN);
+ RawText raw;
+ // Can't use file.openEntryStream() as it would do CR-LF
+ // conversion as usual, not as wanted by us.
+ try (InputStream input = filterClean(repository, path,
+ new FileInputStream(f), convertCrLf, command)) {
+ raw = new RawText(
+ IO.readWholeStream(input, 0).array());
+ }
+ applyText(repository, path, raw, f, fh, checkOut);
+ }
+ return;
+ }
+ }
+ }
+ // File ignored?
+ RawText raw;
+ CheckoutMetadata checkOut;
+ if (PatchType.GIT_BINARY.equals(fh.getPatchType())) {
+ checkOut = new CheckoutMetadata(EolStreamType.DIRECT, null);
+ applyBinary(repository, path, f, fh, () -> new FileInputStream(f),
+ null, checkOut);
+ } else {
+ if (convertCrLf) {
+ try (InputStream input = EolStreamTypeUtil.wrapInputStream(
+ new FileInputStream(f), EolStreamType.TEXT_LF)) {
+ raw = new RawText(IO.readWholeStream(input, 0).array());
+ }
+ checkOut = new CheckoutMetadata(EolStreamType.TEXT_CRLF, null);
+ } else {
+ raw = new RawText(f);
+ checkOut = new CheckoutMetadata(EolStreamType.DIRECT, null);
+ }
+ applyText(repository, path, raw, f, fh, checkOut);
+ }
+ }
+
+ private boolean needsCrLfConversion(File f, FileHeader fileHeader)
+ throws IOException {
+ if (PatchType.GIT_BINARY.equals(fileHeader.getPatchType())) {
+ return false;
+ }
+ if (!hasCrLf(fileHeader)) {
+ try (InputStream input = new FileInputStream(f)) {
+ return RawText.isCrLfText(input);
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasCrLf(FileHeader fileHeader) {
+ if (PatchType.GIT_BINARY.equals(fileHeader.getPatchType())) {
+ return false;
+ }
+ for (HunkHeader header : fileHeader.getHunks()) {
+ byte[] buf = header.getBuffer();
+ int hunkEnd = header.getEndOffset();
+ int lineStart = header.getStartOffset();
+ while (lineStart < hunkEnd) {
+ int nextLineStart = RawParseUtils.nextLF(buf, lineStart);
+ if (nextLineStart > hunkEnd) {
+ nextLineStart = hunkEnd;
+ }
+ if (nextLineStart <= lineStart) {
+ break;
+ }
+ if (nextLineStart - lineStart > 1) {
+ char first = (char) (buf[lineStart] & 0xFF);
+ if (first == ' ' || first == '-') {
+ // It's an old line. Does it end in CR-LF?
+ if (buf[nextLineStart - 2] == '\r') {
+ return true;
+ }
+ }
+ }
+ lineStart = nextLineStart;
+ }
+ }
+ return false;
+ }
+
+ private InputStream filterClean(Repository repository, String path,
+ InputStream fromFile, boolean convertCrLf, String filterCommand)
+ throws IOException {
+ InputStream input = fromFile;
+ if (convertCrLf) {
+ input = EolStreamTypeUtil.wrapInputStream(input,
+ EolStreamType.TEXT_LF);
+ }
+ if (StringUtils.isEmptyOrNull(filterCommand)) {
+ return input;
+ }
+ if (FilterCommandRegistry.isRegistered(filterCommand)) {
+ LocalFile buffer = new TemporaryBuffer.LocalFile(null);
+ FilterCommand command = FilterCommandRegistry.createFilterCommand(
+ filterCommand, repository, input, buffer);
+ while (command.run() != -1) {
+ // loop as long as command.run() tells there is work to do
+ }
+ return buffer.openInputStreamWithAutoDestroy();
+ }
+ FS fs = repository.getFS();
+ ProcessBuilder filterProcessBuilder = fs.runInShell(filterCommand,
+ new String[0]);
+ filterProcessBuilder.directory(repository.getWorkTree());
+ filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY,
+ repository.getDirectory().getAbsolutePath());
+ ExecutionResult result;
+ try {
+ result = fs.execute(filterProcessBuilder, in);
+ } catch (IOException | InterruptedException e) {
+ throw new IOException(
+ new FilterFailedException(e, filterCommand, path));
+ }
+ int rc = result.getRc();
+ if (rc != 0) {
+ throw new IOException(new FilterFailedException(rc, filterCommand,
+ path, result.getStdout().toByteArray(4096), RawParseUtils
+ .decode(result.getStderr().toByteArray(4096))));
+ }
+ return result.getStdout().openInputStreamWithAutoDestroy();
+ }
+
+ /**
+ * Something that can supply an {@link InputStream}.
+ */
+ private interface StreamSupplier {
+ InputStream load() throws IOException;
+ }
+
/**
- * @param f
- * @param fh
- * @throws IOException
- * @throws PatchApplyException
+ * We write the patch result to a {@link TemporaryBuffer} and then use
+ * {@link DirCacheCheckout}.getContent() to run the result through the CR-LF
+ * and smudge filters. DirCacheCheckout needs an ObjectLoader, not a
+ * TemporaryBuffer, so this class bridges between the two, making any Stream
+ * provided by a {@link StreamSupplier} look like an ordinary git blob to
+ * DirCacheCheckout.
*/
- private void apply(File f, FileHeader fh)
+ private static class StreamLoader extends ObjectLoader {
+
+ private StreamSupplier data;
+
+ private long size;
+
+ StreamLoader(StreamSupplier data, long length) {
+ this.data = data;
+ this.size = length;
+ }
+
+ @Override
+ public int getType() {
+ return Constants.OBJ_BLOB;
+ }
+
+ @Override
+ public long getSize() {
+ return size;
+ }
+
+ @Override
+ public boolean isLarge() {
+ return true;
+ }
+
+ @Override
+ public byte[] getCachedBytes() throws LargeObjectException {
+ throw new LargeObjectException();
+ }
+
+ @Override
+ public ObjectStream openStream()
+ throws MissingObjectException, IOException {
+ return new ObjectStream.Filter(getType(), getSize(),
+ new BufferedInputStream(data.load()));
+ }
+ }
+
+ private void initHash(SHA1 hash, long size) {
+ hash.update(Constants.encodedTypeString(Constants.OBJ_BLOB));
+ hash.update((byte) ' ');
+ hash.update(Constants.encodeASCII(size));
+ hash.update((byte) 0);
+ }
+
+ private ObjectId hash(File f) throws IOException {
+ SHA1 hash = SHA1.newInstance();
+ initHash(hash, f.length());
+ try (InputStream input = new FileInputStream(f)) {
+ byte[] buf = new byte[8192];
+ int n;
+ while ((n = input.read(buf)) >= 0) {
+ hash.update(buf, 0, n);
+ }
+ }
+ return hash.toObjectId();
+ }
+
+ private void checkOid(ObjectId baseId, ObjectId id, ChangeType type, File f,
+ String path)
+ throws PatchApplyException, IOException {
+ boolean hashOk = false;
+ if (id != null) {
+ hashOk = baseId.equals(id);
+ if (!hashOk && ChangeType.ADD.equals(type)
+ && ObjectId.zeroId().equals(baseId)) {
+ // We create the file first. The OID of an empty file is not the
+ // zero id!
+ hashOk = Constants.EMPTY_BLOB_ID.equals(id);
+ }
+ } else {
+ if (ObjectId.zeroId().equals(baseId)) {
+ // File empty is OK.
+ hashOk = !f.exists() || f.length() == 0;
+ } else {
+ hashOk = baseId.equals(hash(f));
+ }
+ }
+ if (!hashOk) {
+ throw new PatchApplyException(MessageFormat
+ .format(JGitText.get().applyBinaryBaseOidWrong, path));
+ }
+ }
+
+ private void applyBinary(Repository repository, String path, File f,
+ FileHeader fh, StreamSupplier loader, ObjectId id,
+ CheckoutMetadata checkOut)
+ throws PatchApplyException, IOException {
+ if (!fh.getOldId().isComplete() || !fh.getNewId().isComplete()) {
+ throw new PatchApplyException(MessageFormat
+ .format(JGitText.get().applyBinaryOidTooShort, path));
+ }
+ BinaryHunk hunk = fh.getForwardBinaryHunk();
+ // A BinaryHunk has the start at the "literal" or "delta" token. Data
+ // starts on the next line.
+ int start = RawParseUtils.nextLF(hunk.getBuffer(),
+ hunk.getStartOffset());
+ int length = hunk.getEndOffset() - start;
+ SHA1 hash = SHA1.newInstance();
+ // Write to a buffer and copy to the file only if everything was fine
+ TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
+ try {
+ switch (hunk.getType()) {
+ case LITERAL_DEFLATED:
+ // This just overwrites the file. We need to check the hash of
+ // the base.
+ checkOid(fh.getOldId().toObjectId(), id, fh.getChangeType(), f,
+ path);
+ initHash(hash, hunk.getSize());
+ try (OutputStream out = buffer;
+ InputStream inflated = new SHA1InputStream(hash,
+ new InflaterInputStream(
+ new BinaryHunkInputStream(
+ new ByteArrayInputStream(
+ hunk.getBuffer(), start,
+ length))))) {
+ DirCacheCheckout.getContent(repository, path, checkOut,
+ new StreamLoader(() -> inflated, hunk.getSize()),
+ null, out);
+ if (!fh.getNewId().toObjectId().equals(hash.toObjectId())) {
+ throw new PatchApplyException(MessageFormat.format(
+ JGitText.get().applyBinaryResultOidWrong,
+ path));
+ }
+ }
+ try (InputStream bufIn = buffer.openInputStream()) {
+ Files.copy(bufIn, f.toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+ break;
+ case DELTA_DEFLATED:
+ // Unfortunately delta application needs random access to the
+ // base to construct the result.
+ byte[] base;
+ try (InputStream input = loader.load()) {
+ base = IO.readWholeStream(input, 0).array();
+ }
+ // At least stream the result!
+ try (BinaryDeltaInputStream input = new BinaryDeltaInputStream(
+ base,
+ new InflaterInputStream(new BinaryHunkInputStream(
+ new ByteArrayInputStream(hunk.getBuffer(),
+ start, length))))) {
+ long finalSize = input.getExpectedResultSize();
+ initHash(hash, finalSize);
+ try (OutputStream out = buffer;
+ SHA1InputStream hashed = new SHA1InputStream(hash,
+ input)) {
+ DirCacheCheckout.getContent(repository, path, checkOut,
+ new StreamLoader(() -> hashed, finalSize), null,
+ out);
+ if (!fh.getNewId().toObjectId()
+ .equals(hash.toObjectId())) {
+ throw new PatchApplyException(MessageFormat.format(
+ JGitText.get().applyBinaryResultOidWrong,
+ path));
+ }
+ }
+ }
+ try (InputStream bufIn = buffer.openInputStream()) {
+ Files.copy(bufIn, f.toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+ break;
+ default:
+ break;
+ }
+ } finally {
+ buffer.destroy();
+ }
+ }
+
+ private void applyText(Repository repository, String path, RawText rt,
+ File f, FileHeader fh, CheckoutMetadata checkOut)
throws IOException, PatchApplyException {
- RawText rt = new RawText(f);
- List<String> oldLines = new ArrayList<>(rt.size());
- for (int i = 0; i < rt.size(); i++)
- oldLines.add(rt.getString(i));
- List<String> newLines = new ArrayList<>(oldLines);
+ List<ByteBuffer> oldLines = new ArrayList<>(rt.size());
+ for (int i = 0; i < rt.size(); i++) {
+ oldLines.add(rt.getRawString(i));
+ }
+ List<ByteBuffer> newLines = new ArrayList<>(oldLines);
int afterLastHunk = 0;
int lineNumberShift = 0;
int lastHunkNewLine = -1;
@@ -182,9 +568,9 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
b.length);
RawText hrt = new RawText(b);
- List<String> hunkLines = new ArrayList<>(hrt.size());
+ List<ByteBuffer> hunkLines = new ArrayList<>(hrt.size());
for (int i = 0; i < hrt.size(); i++) {
- hunkLines.add(hrt.getString(i));
+ hunkLines.add(hrt.getRawString(i));
}
if (hh.getNewStartLine() == 0) {
@@ -253,8 +639,13 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
lineNumberShift = applyAt - hh.getNewStartLine() + 1;
int sz = hunkLines.size();
for (int j = 1; j < sz; j++) {
- String hunkLine = hunkLines.get(j);
- switch (hunkLine.charAt(0)) {
+ ByteBuffer hunkLine = hunkLines.get(j);
+ if (!hunkLine.hasRemaining()) {
+ // Completely empty line; accept as empty context line
+ applyAt++;
+ continue;
+ }
+ switch (hunkLine.array()[hunkLine.position()]) {
case ' ':
applyAt++;
break;
@@ -262,7 +653,7 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
newLines.remove(applyAt);
break;
case '+':
- newLines.add(applyAt++, hunkLine.substring(1));
+ newLines.add(applyAt++, slice(hunkLine, 1));
break;
default:
break;
@@ -271,39 +662,64 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
afterLastHunk = applyAt;
}
if (!isNoNewlineAtEndOfFile(fh)) {
- newLines.add(""); //$NON-NLS-1$
+ newLines.add(null);
}
if (!rt.isMissingNewlineAtEnd()) {
- oldLines.add(""); //$NON-NLS-1$
- }
- if (!isChanged(oldLines, newLines)) {
- return; // Don't touch the file
- }
- try (Writer fw = Files.newBufferedWriter(f.toPath())) {
- for (Iterator<String> l = newLines.iterator(); l.hasNext();) {
- fw.write(l.next());
- if (l.hasNext()) {
- // Don't bother handling line endings - if it was Windows,
- // the \r is still there!
- fw.write('\n');
+ oldLines.add(null);
+ }
+ if (oldLines.equals(newLines)) {
+ return; // Unchanged; don't touch the file
+ }
+
+ TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
+ try {
+ try (OutputStream out = buffer) {
+ for (Iterator<ByteBuffer> l = newLines.iterator(); l
+ .hasNext();) {
+ ByteBuffer line = l.next();
+ if (line == null) {
+ // Must be the marker for the final newline
+ break;
+ }
+ out.write(line.array(), line.position(), line.remaining());
+ if (l.hasNext()) {
+ out.write('\n');
+ }
}
}
+ try (OutputStream output = new FileOutputStream(f)) {
+ DirCacheCheckout.getContent(repository, path, checkOut,
+ new StreamLoader(buffer::openInputStream,
+ buffer.length()),
+ null, output);
+ }
+ } finally {
+ buffer.destroy();
}
- getRepository().getFS().setExecute(f, fh.getNewMode() == FileMode.EXECUTABLE_FILE);
+ repository.getFS().setExecute(f,
+ fh.getNewMode() == FileMode.EXECUTABLE_FILE);
}
- private boolean canApplyAt(List<String> hunkLines, List<String> newLines,
- int line) {
+ private boolean canApplyAt(List<ByteBuffer> hunkLines,
+ List<ByteBuffer> newLines, int line) {
int sz = hunkLines.size();
int limit = newLines.size();
int pos = line;
for (int j = 1; j < sz; j++) {
- String hunkLine = hunkLines.get(j);
- switch (hunkLine.charAt(0)) {
+ ByteBuffer hunkLine = hunkLines.get(j);
+ if (!hunkLine.hasRemaining()) {
+ // Empty line. Accept as empty context line.
+ if (pos >= limit || newLines.get(pos).hasRemaining()) {
+ return false;
+ }
+ pos++;
+ continue;
+ }
+ switch (hunkLine.array()[hunkLine.position()]) {
case ' ':
case '-':
if (pos >= limit
- || !newLines.get(pos).equals(hunkLine.substring(1))) {
+ || !newLines.get(pos).equals(slice(hunkLine, 1))) {
return false;
}
pos++;
@@ -315,13 +731,9 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
return true;
}
- private static boolean isChanged(List<String> ol, List<String> nl) {
- if (ol.size() != nl.size())
- return true;
- for (int i = 0; i < ol.size(); i++)
- if (!ol.get(i).equals(nl.get(i)))
- return true;
- return false;
+ private ByteBuffer slice(ByteBuffer b, int off) {
+ int newOffset = b.position() + off;
+ return ByteBuffer.wrap(b.array(), newOffset, b.limit() - newOffset);
}
private boolean isNoNewlineAtEndOfFile(FileHeader fh) {
@@ -330,8 +742,51 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
return false;
}
HunkHeader lastHunk = hunks.get(hunks.size() - 1);
- RawText lhrt = new RawText(lastHunk.getBuffer());
+ byte[] buf = new byte[lastHunk.getEndOffset()
+ - lastHunk.getStartOffset()];
+ System.arraycopy(lastHunk.getBuffer(), lastHunk.getStartOffset(), buf,
+ 0, buf.length);
+ RawText lhrt = new RawText(buf);
return lhrt.getString(lhrt.size() - 1)
.equals("\\ No newline at end of file"); //$NON-NLS-1$
}
+
+ /**
+ * An {@link InputStream} that updates a {@link SHA1} on every byte read.
+ * The hash is supposed to have been initialized before reading starts.
+ */
+ private static class SHA1InputStream extends InputStream {
+
+ private final SHA1 hash;
+
+ private final InputStream in;
+
+ SHA1InputStream(SHA1 hash, InputStream in) {
+ this.hash = hash;
+ this.in = in;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int b = in.read();
+ if (b >= 0) {
+ hash.update((byte) b);
+ }
+ return b;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int n = in.read(b, off, len);
+ if (n > 0) {
+ hash.update(b, off, n);
+ }
+ return n;
+ }
+
+ @Override
+ public void close() throws IOException {
+ in.close();
+ }
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
index 5d0154c6dc..7922f9e729 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2010, 2021 Christian Halstrick <christian.halstrick@sap.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -13,6 +13,7 @@ import java.io.IOException;
import java.text.MessageFormat;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.GitAPIException;
@@ -35,9 +36,12 @@ import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Ref.Storage;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
import org.eclipse.jgit.merge.MergeMessageFormatter;
import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.Merger;
import org.eclipse.jgit.merge.ResolveMerger;
+import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.FileTreeIterator;
@@ -61,6 +65,8 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> {
private MergeStrategy strategy = MergeStrategy.RECURSIVE;
+ private ContentMergeStrategy contentStrategy;
+
private Integer mainlineParentNumber;
private boolean noCommit = false;
@@ -121,16 +127,30 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> {
String cherryPickName = srcCommit.getId().abbreviate(7).name()
+ " " + srcCommit.getShortMessage(); //$NON-NLS-1$
- ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
- merger.setWorkingTreeIterator(new FileTreeIterator(repo));
- merger.setBase(srcParent.getTree());
- merger.setCommitNames(new String[] { "BASE", ourName, //$NON-NLS-1$
- cherryPickName });
- if (merger.merge(newHead, srcCommit)) {
- if (!merger.getModifiedFiles().isEmpty()) {
+ Merger merger = strategy.newMerger(repo);
+ merger.setProgressMonitor(monitor);
+ boolean noProblems;
+ Map<String, MergeFailureReason> failingPaths = null;
+ List<String> unmergedPaths = null;
+ if (merger instanceof ResolveMerger) {
+ ResolveMerger resolveMerger = (ResolveMerger) merger;
+ resolveMerger.setContentMergeStrategy(contentStrategy);
+ resolveMerger.setCommitNames(
+ new String[] { "BASE", ourName, cherryPickName }); //$NON-NLS-1$
+ resolveMerger
+ .setWorkingTreeIterator(new FileTreeIterator(repo));
+ resolveMerger.setBase(srcParent.getTree());
+ noProblems = merger.merge(newHead, srcCommit);
+ failingPaths = resolveMerger.getFailingPaths();
+ unmergedPaths = resolveMerger.getUnmergedPaths();
+ if (!resolveMerger.getModifiedFiles().isEmpty()) {
repo.fireEvent(new WorkingTreeModifiedEvent(
- merger.getModifiedFiles(), null));
+ resolveMerger.getModifiedFiles(), null));
}
+ } else {
+ noProblems = merger.merge(newHead, srcCommit);
+ }
+ if (noProblems) {
if (AnyObjectId.isEqual(newHead.getTree().getId(),
merger.getResultTreeId())) {
continue;
@@ -153,24 +173,26 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> {
}
cherryPickedRefs.add(src);
} else {
- if (merger.failed()) {
- return new CherryPickResult(merger.getFailingPaths());
+ if (failingPaths != null && !failingPaths.isEmpty()) {
+ return new CherryPickResult(failingPaths);
}
// there are merge conflicts
- String message = new MergeMessageFormatter()
+ String message;
+ if (unmergedPaths != null) {
+ message = new MergeMessageFormatter()
.formatWithConflicts(srcCommit.getFullMessage(),
- merger.getUnmergedPaths());
+ unmergedPaths);
+ } else {
+ message = srcCommit.getFullMessage();
+ }
if (!noCommit) {
repo.writeCherryPickHead(srcCommit.getId());
}
repo.writeMergeCommitMsg(message);
- repo.fireEvent(new WorkingTreeModifiedEvent(
- merger.getModifiedFiles(), null));
-
return CherryPickResult.CONFLICT;
}
}
@@ -291,6 +313,22 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> {
}
/**
+ * Sets the content merge strategy to use if the
+ * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+ * "recursive".
+ *
+ * @param strategy
+ * the {@link ContentMergeStrategy} to be used
+ * @return {@code this}
+ * @since 5.12
+ */
+ public CherryPickCommand setContentMergeStrategy(
+ ContentMergeStrategy strategy) {
+ this.contentStrategy = strategy;
+ return this;
+ }
+
+ /**
* Set the (1-based) parent number to diff against
*
* @param mainlineParentNumber
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
index d88f4ec561..ef56d802c8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -1,7 +1,7 @@
/*
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
- * Copyright (C) 2010-2014, Stefan Lay <stefan.lay@sap.com>
- * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> and others
+ * Copyright (C) 2010, 2014, Stefan Lay <stefan.lay@sap.com>
+ * Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -45,6 +45,7 @@ import org.eclipse.jgit.lib.Ref.Storage;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
import org.eclipse.jgit.merge.MergeConfig;
import org.eclipse.jgit.merge.MergeMessageFormatter;
import org.eclipse.jgit.merge.MergeStrategy;
@@ -71,6 +72,8 @@ public class MergeCommand extends GitCommand<MergeResult> {
private MergeStrategy mergeStrategy = MergeStrategy.RECURSIVE;
+ private ContentMergeStrategy contentStrategy;
+
private List<Ref> commits = new LinkedList<>();
private Boolean squash;
@@ -84,6 +87,20 @@ public class MergeCommand extends GitCommand<MergeResult> {
private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
/**
+ * Values for the "merge.conflictStyle" git config.
+ *
+ * @since 5.12
+ */
+ public enum ConflictStyle {
+
+ /** "merge" style: only ours/theirs. This is the default. */
+ MERGE,
+
+ /** "diff3" style: ours/base/theirs. */
+ DIFF3
+ }
+
+ /**
* The modes available for fast forward merges corresponding to the
* <code>--ff</code>, <code>--no-ff</code> and <code>--ff-only</code>
* options under <code>branch.&lt;name&gt;.mergeoptions</code>.
@@ -320,6 +337,7 @@ public class MergeCommand extends GitCommand<MergeResult> {
List<String> unmergedPaths = null;
if (merger instanceof ResolveMerger) {
ResolveMerger resolveMerger = (ResolveMerger) merger;
+ resolveMerger.setContentMergeStrategy(contentStrategy);
resolveMerger.setCommitNames(new String[] {
"BASE", "HEAD", ref.getName() }); //$NON-NLS-1$ //$NON-NLS-2$
resolveMerger.setWorkingTreeIterator(new FileTreeIterator(repo));
@@ -473,6 +491,22 @@ public class MergeCommand extends GitCommand<MergeResult> {
}
/**
+ * Sets the content merge strategy to use if the
+ * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+ * "recursive".
+ *
+ * @param strategy
+ * the {@link ContentMergeStrategy} to be used
+ * @return {@code this}
+ * @since 5.12
+ */
+ public MergeCommand setContentMergeStrategy(ContentMergeStrategy strategy) {
+ checkCallable();
+ this.contentStrategy = strategy;
+ return this;
+ }
+
+ /**
* Reference to a commit to be merged with the current head
*
* @param aCommit
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 449250890c..281ecfd011 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
@@ -1,7 +1,7 @@
/*
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
* Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
- * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> and others
+ * Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -43,6 +43,7 @@ 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.ContentMergeStrategy;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -69,6 +70,8 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> {
private MergeStrategy strategy = MergeStrategy.RECURSIVE;
+ private ContentMergeStrategy contentStrategy;
+
private TagOpt tagOption;
private FastForwardMode fastForwardMode;
@@ -275,8 +278,7 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> {
JGitText.get().pullTaskName));
// we check the updates to see which of the updated branches
- // corresponds
- // to the remote branch name
+ // corresponds to the remote branch name
AnyObjectId commitToMerge;
if (isRemote) {
Ref r = null;
@@ -354,8 +356,11 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> {
}
RebaseCommand rebase = new RebaseCommand(repo);
RebaseResult rebaseRes = rebase.setUpstream(commitToMerge)
- .setUpstreamName(upstreamName).setProgressMonitor(monitor)
- .setOperation(Operation.BEGIN).setStrategy(strategy)
+ .setProgressMonitor(monitor)
+ .setUpstreamName(upstreamName)
+ .setOperation(Operation.BEGIN)
+ .setStrategy(strategy)
+ .setContentMergeStrategy(contentStrategy)
.setPreserveMerges(
pullRebaseMode == BranchRebaseMode.PRESERVE)
.call();
@@ -363,7 +368,9 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> {
} else {
MergeCommand merge = new MergeCommand(repo);
MergeResult mergeRes = merge.include(upstreamName, commitToMerge)
- .setStrategy(strategy).setProgressMonitor(monitor)
+ .setProgressMonitor(monitor)
+ .setStrategy(strategy)
+ .setContentMergeStrategy(contentStrategy)
.setFastForward(getFastForwardMode()).call();
monitor.update(1);
result = new PullResult(fetchRes, remote, mergeRes);
@@ -442,6 +449,21 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> {
}
/**
+ * Sets the content merge strategy to use if the
+ * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+ * "recursive".
+ *
+ * @param strategy
+ * the {@link ContentMergeStrategy} to be used
+ * @return {@code this}
+ * @since 5.12
+ */
+ public PullCommand setContentMergeStrategy(ContentMergeStrategy strategy) {
+ this.contentStrategy = strategy;
+ return this;
+ }
+
+ /**
* Set the specification of annotated tag behavior during fetch
*
* @param tagOpt
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 836175dcea..a26ffc2e66 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com>
- * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> and others
+ * Copyright (C) 2016, 2021 Laurent Delaigue <laurent.delaigue@obeo.fr> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -65,6 +65,7 @@ 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.merge.ContentMergeStrategy;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
@@ -212,6 +213,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private MergeStrategy strategy = MergeStrategy.RECURSIVE;
+ private ContentMergeStrategy contentStrategy;
+
private boolean preserveMerges = false;
/**
@@ -501,8 +504,11 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
String ourCommitName = getOurCommitName();
try (Git git = new Git(repo)) {
CherryPickResult cherryPickResult = git.cherryPick()
- .include(commitToPick).setOurCommitName(ourCommitName)
- .setReflogPrefix(REFLOG_PREFIX).setStrategy(strategy)
+ .include(commitToPick)
+ .setOurCommitName(ourCommitName)
+ .setReflogPrefix(REFLOG_PREFIX)
+ .setStrategy(strategy)
+ .setContentMergeStrategy(contentStrategy)
.call();
switch (cherryPickResult.getStatus()) {
case FAILED:
@@ -556,7 +562,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
.include(commitToPick)
.setOurCommitName(ourCommitName)
.setReflogPrefix(REFLOG_PREFIX)
- .setStrategy(strategy);
+ .setStrategy(strategy)
+ .setContentMergeStrategy(contentStrategy);
if (isMerge) {
pickCommand.setMainlineParentNumber(1);
// We write a MERGE_HEAD and later commit explicitly
@@ -592,6 +599,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
MergeCommand merge = git.merge()
.setFastForward(MergeCommand.FastForwardMode.NO_FF)
.setProgressMonitor(monitor)
+ .setStrategy(strategy)
+ .setContentMergeStrategy(contentStrategy)
.setCommit(false);
for (int i = 1; i < commitToPick.getParentCount(); i++)
merge.include(newParents.get(i));
@@ -1137,7 +1146,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
private List<RevCommit> calculatePickList(RevCommit headCommit)
- throws GitAPIException, NoHeadException, IOException {
+ throws IOException {
List<RevCommit> cherryPickList = new ArrayList<>();
try (RevWalk r = new RevWalk(repo)) {
r.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true);
@@ -1587,6 +1596,21 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
}
/**
+ * Sets the content merge strategy to use if the
+ * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+ * "recursive".
+ *
+ * @param strategy
+ * the {@link ContentMergeStrategy} to be used
+ * @return {@code this}
+ * @since 5.12
+ */
+ public RebaseCommand setContentMergeStrategy(ContentMergeStrategy strategy) {
+ this.contentStrategy = strategy;
+ return this;
+ }
+
+ /**
* Whether to preserve merges during rebase
*
* @param preserve
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
index 56b3992fcd..1004d3e50f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012, 2017 GitHub Inc. and others
+ * Copyright (C) 2012, 2021 GitHub Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -38,7 +38,9 @@ import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.merge.ContentMergeStrategy;
import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.Merger;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
@@ -71,6 +73,8 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
private MergeStrategy strategy = MergeStrategy.RECURSIVE;
+ private ContentMergeStrategy contentStrategy;
+
/**
* Create command to apply the changes of a stashed commit
*
@@ -166,16 +170,25 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
if (restoreUntracked && stashCommit.getParentCount() == 3)
untrackedCommit = revWalk.parseCommit(stashCommit.getParent(2));
- ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo);
- merger.setCommitNames(new String[] { "stashed HEAD", "HEAD", //$NON-NLS-1$ //$NON-NLS-2$
- "stash" }); //$NON-NLS-1$
- merger.setBase(stashHeadCommit);
- merger.setWorkingTreeIterator(new FileTreeIterator(repo));
- boolean mergeSucceeded = merger.merge(headCommit, stashCommit);
- List<String> modifiedByMerge = merger.getModifiedFiles();
- if (!modifiedByMerge.isEmpty()) {
- repo.fireEvent(
- new WorkingTreeModifiedEvent(modifiedByMerge, null));
+ Merger merger = strategy.newMerger(repo);
+ boolean mergeSucceeded;
+ if (merger instanceof ResolveMerger) {
+ ResolveMerger resolveMerger = (ResolveMerger) merger;
+ resolveMerger
+ .setCommitNames(new String[] { "stashed HEAD", "HEAD", //$NON-NLS-1$ //$NON-NLS-2$
+ "stash" }); //$NON-NLS-1$
+ resolveMerger.setBase(stashHeadCommit);
+ resolveMerger
+ .setWorkingTreeIterator(new FileTreeIterator(repo));
+ resolveMerger.setContentMergeStrategy(contentStrategy);
+ mergeSucceeded = resolveMerger.merge(headCommit, stashCommit);
+ List<String> modifiedByMerge = resolveMerger.getModifiedFiles();
+ if (!modifiedByMerge.isEmpty()) {
+ repo.fireEvent(new WorkingTreeModifiedEvent(modifiedByMerge,
+ null));
+ }
+ } else {
+ mergeSucceeded = merger.merge(headCommit, stashCommit);
}
if (mergeSucceeded) {
DirCache dc = repo.lockDirCache();
@@ -184,11 +197,14 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
dco.setFailOnConflict(true);
dco.checkout(); // Ignoring failed deletes....
if (restoreIndex) {
- ResolveMerger ixMerger = (ResolveMerger) strategy
- .newMerger(repo, true);
- ixMerger.setCommitNames(new String[] { "stashed HEAD", //$NON-NLS-1$
- "HEAD", "stashed index" }); //$NON-NLS-1$//$NON-NLS-2$
- ixMerger.setBase(stashHeadCommit);
+ Merger ixMerger = strategy.newMerger(repo, true);
+ if (ixMerger instanceof ResolveMerger) {
+ ResolveMerger resolveMerger = (ResolveMerger) ixMerger;
+ resolveMerger.setCommitNames(new String[] { "stashed HEAD", //$NON-NLS-1$
+ "HEAD", "stashed index" }); //$NON-NLS-1$//$NON-NLS-2$
+ resolveMerger.setBase(stashHeadCommit);
+ resolveMerger.setContentMergeStrategy(contentStrategy);
+ }
boolean ok = ixMerger.merge(headCommit, stashIndexCommit);
if (ok) {
resetIndex(revWalk
@@ -200,16 +216,20 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
}
if (untrackedCommit != null) {
- ResolveMerger untrackedMerger = (ResolveMerger) strategy
- .newMerger(repo, true);
- untrackedMerger.setCommitNames(new String[] {
- "null", "HEAD", "untracked files" }); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
- // There is no common base for HEAD & untracked files
- // because the commit for untracked files has no parent. If
- // we use stashHeadCommit as common base (as in the other
- // merges) we potentially report conflicts for files
- // which are not even member of untracked files commit
- untrackedMerger.setBase(null);
+ Merger untrackedMerger = strategy.newMerger(repo, true);
+ if (untrackedMerger instanceof ResolveMerger) {
+ ResolveMerger resolveMerger = (ResolveMerger) untrackedMerger;
+ resolveMerger.setCommitNames(new String[] { "null", "HEAD", //$NON-NLS-1$//$NON-NLS-2$
+ "untracked files" }); //$NON-NLS-1$
+ // There is no common base for HEAD & untracked files
+ // because the commit for untracked files has no parent.
+ // If we use stashHeadCommit as common base (as in the
+ // other merges) we potentially report conflicts for
+ // files which are not even member of untracked files
+ // commit.
+ resolveMerger.setBase(null);
+ resolveMerger.setContentMergeStrategy(contentStrategy);
+ }
boolean ok = untrackedMerger.merge(headCommit,
untrackedCommit);
if (ok) {
@@ -279,6 +299,23 @@ public class StashApplyCommand extends GitCommand<ObjectId> {
}
/**
+ * Sets the content merge strategy to use if the
+ * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or
+ * "recursive".
+ *
+ * @param strategy
+ * the {@link ContentMergeStrategy} to be used
+ * @return {@code this}
+ * @since 5.12
+ */
+ public StashApplyCommand setContentMergeStrategy(
+ ContentMergeStrategy strategy) {
+ checkCallable();
+ this.contentStrategy = strategy;
+ return this;
+ }
+
+ /**
* Whether the command should restore untracked files
*
* @param applyUntracked
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java
index 9f4b1fa493..d09da019dd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2009, Google Inc.
- * Copyright (C) 2008-2009, Johannes E. Schindelin <johannes.schindelin@gmx.de> and others
+ * Copyright (C) 2008-2021, Johannes E. Schindelin <johannes.schindelin@gmx.de> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -16,6 +16,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.ByteBuffer;
import org.eclipse.jgit.errors.BinaryBlobException;
import org.eclipse.jgit.errors.LargeObjectException;
@@ -165,6 +166,27 @@ public class RawText extends Sequence {
}
/**
+ * Get the raw text for a single line.
+ *
+ * @param i
+ * index of the line to extract. Note this is 0-based, so line
+ * number 1 is actually index 0.
+ * @return the text for the line, without a trailing LF, as a
+ * {@link ByteBuffer} that is backed by a slice of the
+ * {@link #getRawContent() raw content}, with the buffer's position
+ * on the start of the line and the limit at the end.
+ * @since 5.12
+ */
+ public ByteBuffer getRawString(int i) {
+ int s = getStart(i);
+ int e = getEnd(i);
+ if (e > 0 && content[e - 1] == '\n') {
+ e--;
+ }
+ return ByteBuffer.wrap(content, s, e - s);
+ }
+
+ /**
* Get the text for a region of lines.
*
* @param begin
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
index 80e1b18291..ba1f63b680 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
@@ -12,6 +12,7 @@ package org.eclipse.jgit.diff;
import static org.eclipse.jgit.diff.DiffEntry.Side.NEW;
import static org.eclipse.jgit.diff.DiffEntry.Side.OLD;
+import static org.eclipse.jgit.storage.pack.PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
import java.io.IOException;
import java.util.ArrayList;
@@ -97,6 +98,19 @@ public class RenameDetector {
/** Limit in the number of files to consider for renames. */
private int renameLimit;
+ /**
+ * File size threshold (in bytes) for detecting renames. Files larger
+ * than this size will not be processed for renames.
+ */
+ private int bigFileThreshold = DEFAULT_BIG_FILE_THRESHOLD;
+
+ /**
+ * Skip detecting content renames for binary files. Content renames are
+ * those that are not exact, that is with a slight content modification
+ * between the two files.
+ */
+ private boolean skipContentRenamesForBinaryFiles = false;
+
/** Set if the number of adds or deletes was over the limit. */
private boolean overRenameLimit;
@@ -209,6 +223,46 @@ public class RenameDetector {
}
/**
+ * Get file size threshold for detecting renames. Files larger
+ * than this size will not be processed for rename detection.
+ *
+ * @return threshold in bytes of the file size.
+ * @since 5.12
+ */
+ public int getBigFileThreshold() { return bigFileThreshold; }
+
+ /**
+ * Set the file size threshold for detecting renames. Files larger than this
+ * threshold will be skipped during rename detection computation.
+ *
+ * @param threshold file size threshold in bytes.
+ * @since 5.12
+ */
+ public void setBigFileThreshold(int threshold) {
+ this.bigFileThreshold = threshold;
+ }
+
+ /**
+ * Get skipping detecting content renames for binary files.
+ *
+ * @return true if content renames should be skipped for binary files, false otherwise.
+ * @since 5.12
+ */
+ public boolean getSkipContentRenamesForBinaryFiles() {
+ return skipContentRenamesForBinaryFiles;
+ }
+
+ /**
+ * Sets skipping detecting content renames for binary files.
+ *
+ * @param value true if content renames should be skipped for binary files, false otherwise.
+ * @since 5.12
+ */
+ public void setSkipContentRenamesForBinaryFiles(boolean value) {
+ this.skipContentRenamesForBinaryFiles = value;
+ }
+
+ /**
* Check if the detector is over the rename limit.
* <p>
* This method can be invoked either before or after {@code getEntries} has
@@ -493,6 +547,8 @@ public class RenameDetector {
d = new SimilarityRenameDetector(reader, deleted, added);
d.setRenameScore(getRenameScore());
+ d.setBigFileThreshold(getBigFileThreshold());
+ d.setSkipBinaryFiles(getSkipContentRenamesForBinaryFiles());
d.compute(pm);
overRenameLimit |= d.isTableOverflow();
deleted = d.getLeftOverSources();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java
index fb6e5df589..661369b86a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java
@@ -102,6 +102,15 @@ public class SimilarityIndex {
idGrowAt = growAt(idHashBits);
}
+ static boolean isBinary(ObjectLoader obj) throws IOException {
+ if (obj.isLarge()) {
+ try (ObjectStream in1 = obj.openStream()) {
+ return RawText.isBinary(in1);
+ }
+ }
+ return RawText.isBinary(obj.getCachedBytes());
+ }
+
void hash(ObjectLoader obj) throws MissingObjectException, IOException,
TableFullException {
if (obj.isLarge()) {
@@ -115,9 +124,7 @@ public class SimilarityIndex {
private void hashLargeObject(ObjectLoader obj) throws IOException,
TableFullException {
boolean text;
- try (ObjectStream in1 = obj.openStream()) {
- text = !RawText.isBinary(in1);
- }
+ text = !isBinary(obj);
try (ObjectStream in2 = obj.openStream()) {
hash(in2, in2.getSize(), text);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
index 74a11a024a..5871b4aeea 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
@@ -12,6 +12,7 @@ package org.eclipse.jgit.diff;
import static org.eclipse.jgit.diff.DiffEntry.Side.NEW;
import static org.eclipse.jgit.diff.DiffEntry.Side.OLD;
+import static org.eclipse.jgit.storage.pack.PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
import java.io.IOException;
import java.util.ArrayList;
@@ -25,6 +26,7 @@ import org.eclipse.jgit.errors.CancelledException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ProgressMonitor;
class SimilarityRenameDetector {
@@ -80,6 +82,15 @@ class SimilarityRenameDetector {
/** Score a pair must exceed to be considered a rename. */
private int renameScore = 60;
+ /**
+ * File size threshold (in bytes) for detecting renames. Files larger
+ * than this size will not be processed for renames.
+ */
+ private int bigFileThreshold = DEFAULT_BIG_FILE_THRESHOLD;
+
+ /** Skip content renames for binary files. */
+ private boolean skipBinaryFiles = false;
+
/** Set if any {@link SimilarityIndex.TableFullException} occurs. */
private boolean tableOverflow;
@@ -96,6 +107,14 @@ class SimilarityRenameDetector {
renameScore = score;
}
+ void setBigFileThreshold(int threshold) {
+ bigFileThreshold = threshold;
+ }
+
+ void setSkipBinaryFiles(boolean value) {
+ skipBinaryFiles = value;
+ }
+
void compute(ProgressMonitor pm) throws IOException, CancelledException {
if (pm == null)
pm = NullProgressMonitor.INSTANCE;
@@ -253,9 +272,19 @@ class SimilarityRenameDetector {
continue;
}
+ if (max > bigFileThreshold) {
+ pm.update(1);
+ continue;
+ }
+
if (s == null) {
try {
- s = hash(OLD, srcEnt);
+ ObjectLoader loader = reader.open(OLD, srcEnt);
+ if (skipBinaryFiles && SimilarityIndex.isBinary(loader)) {
+ pm.update(1);
+ continue SRC;
+ }
+ s = hash(loader);
} catch (TableFullException tableFull) {
tableOverflow = true;
continue SRC;
@@ -264,7 +293,12 @@ class SimilarityRenameDetector {
SimilarityIndex d;
try {
- d = hash(NEW, dstEnt);
+ ObjectLoader loader = reader.open(NEW, dstEnt);
+ if (skipBinaryFiles && SimilarityIndex.isBinary(loader)) {
+ pm.update(1);
+ continue;
+ }
+ d = hash(loader);
} catch (TableFullException tableFull) {
if (dstTooLarge == null)
dstTooLarge = new BitSet(dsts.size());
@@ -348,10 +382,10 @@ class SimilarityRenameDetector {
return (((dirScoreLtr + dirScoreRtl) * 25) + (fileScore * 50)) / 100;
}
- private SimilarityIndex hash(DiffEntry.Side side, DiffEntry ent)
+ private SimilarityIndex hash(ObjectLoader objectLoader)
throws IOException, TableFullException {
SimilarityIndex r = new SimilarityIndex();
- r.hash(reader.open(side, ent));
+ r.hash(objectLoader);
r.sort();
return r;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index 671475ed47..c904a782db 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -1610,11 +1610,9 @@ public class DirCacheCheckout {
}
if (rc != 0) {
throw new IOException(new FilterFailedException(rc,
- checkoutMetadata.smudgeFilterCommand,
- path,
+ checkoutMetadata.smudgeFilterCommand, path,
result.getStdout().toByteArray(MAX_EXCEPTION_TEXT_SIZE),
- RawParseUtils.decode(result.getStderr()
- .toByteArray(MAX_EXCEPTION_TEXT_SIZE))));
+ result.getStderr().toString(MAX_EXCEPTION_TEXT_SIZE)));
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
index c039aaffa9..552315d43a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
@@ -12,6 +12,7 @@ package org.eclipse.jgit.gitrepo;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
import static org.eclipse.jgit.lib.Constants.R_REMOTES;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
import java.io.File;
import java.io.FileInputStream;
@@ -79,6 +80,13 @@ import org.eclipse.jgit.util.FileUtils;
* @since 3.4
*/
public class RepoCommand extends GitCommand<RevCommit> {
+ private static final int LOCK_FAILURE_MAX_RETRIES = 5;
+
+ // Retry exponentially with delays in this range
+ private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50;
+
+ private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000;
+
private String manifestPath;
private String baseUri;
private URI targetUri;
@@ -587,8 +595,11 @@ public class RepoCommand extends GitCommand<RevCommit> {
throw new RemoteUnavailableException(url);
}
if (recordRemoteBranch) {
- // can be branch or tag
- cfg.setString("submodule", name, "branch", //$NON-NLS-1$ //$NON-NLS-2$
+ // "branch" field is only for non-tag references.
+ // Keep tags in "ref" field as hint for other tools.
+ String field = proj.getRevision().startsWith(
+ R_TAGS) ? "ref" : "branch"; //$NON-NLS-1$ //$NON-NLS-2$
+ cfg.setString("submodule", name, field, //$NON-NLS-1$
proj.getRevision());
}
@@ -682,50 +693,22 @@ public class RepoCommand extends GitCommand<RevCommit> {
builder.finish();
ObjectId treeId = index.writeTree(inserter);
- // Create a Commit object, populate it and write it
- ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
- if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) {
- // No change. Do nothing.
- return rw.parseCommit(headId);
- }
-
- CommitBuilder commit = new CommitBuilder();
- commit.setTreeId(treeId);
- if (headId != null)
- commit.setParentIds(headId);
- commit.setAuthor(author);
- commit.setCommitter(author);
- commit.setMessage(RepoText.get().repoCommitMessage);
-
- ObjectId commitId = inserter.insert(commit);
- inserter.flush();
-
- RefUpdate ru = repo.updateRef(targetBranch);
- ru.setNewObjectId(commitId);
- ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
- Result rc = ru.update(rw);
-
- switch (rc) {
- case NEW:
- case FORCED:
- case FAST_FORWARD:
- // Successful. Do nothing.
- break;
- case REJECTED:
- case LOCK_FAILURE:
- throw new ConcurrentRefUpdateException(
- MessageFormat.format(
- JGitText.get().cannotLock, targetBranch),
- ru.getRef(),
- rc);
- default:
- throw new JGitInternalException(MessageFormat.format(
- JGitText.get().updatingRefFailed,
- targetBranch, commitId.name(), rc));
+ long prevDelay = 0;
+ for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) {
+ try {
+ return commitTreeOnCurrentTip(
+ inserter, rw, treeId);
+ } catch (ConcurrentRefUpdateException e) {
+ prevDelay = FileUtils.delay(prevDelay,
+ LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS,
+ LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS);
+ Thread.sleep(prevDelay);
+ repo.getRefDatabase().refresh();
+ }
}
-
- return rw.parseCommit(commitId);
- } catch (GitAPIException | IOException e) {
+ // In the last try, just propagate the exceptions
+ return commitTreeOnCurrentTip(inserter, rw, treeId);
+ } catch (GitAPIException | IOException | InterruptedException e) {
throw new ManifestErrorException(e);
}
}
@@ -742,6 +725,51 @@ public class RepoCommand extends GitCommand<RevCommit> {
}
}
+
+ private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter,
+ RevWalk rw, ObjectId treeId)
+ throws IOException, ConcurrentRefUpdateException {
+ ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
+ if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) {
+ // No change. Do nothing.
+ return rw.parseCommit(headId);
+ }
+
+ CommitBuilder commit = new CommitBuilder();
+ commit.setTreeId(treeId);
+ if (headId != null)
+ commit.setParentIds(headId);
+ commit.setAuthor(author);
+ commit.setCommitter(author);
+ commit.setMessage(RepoText.get().repoCommitMessage);
+
+ ObjectId commitId = inserter.insert(commit);
+ inserter.flush();
+
+ RefUpdate ru = repo.updateRef(targetBranch);
+ ru.setNewObjectId(commitId);
+ ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
+ Result rc = ru.update(rw);
+ switch (rc) {
+ case NEW:
+ case FORCED:
+ case FAST_FORWARD:
+ // Successful. Do nothing.
+ break;
+ case REJECTED:
+ case LOCK_FAILURE:
+ throw new ConcurrentRefUpdateException(MessageFormat
+ .format(JGitText.get().cannotLock, targetBranch),
+ ru.getRef(), rc);
+ default:
+ throw new JGitInternalException(MessageFormat.format(
+ JGitText.get().updatingRefFailed,
+ targetBranch, commitId.name(), rc));
+ }
+
+ return rw.parseCommit(commitId);
+ }
+
private void addSubmodule(String name, String url, String path,
String revision, List<CopyFile> copyfiles, List<LinkFile> linkfiles,
Git git) throws GitAPIException, IOException {
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 b942c09fa9..e1fa14435d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -41,6 +41,9 @@ public class JGitText extends TranslationBundle {
/***/ public String aNewObjectIdIsRequired;
/***/ public String anExceptionOccurredWhileTryingToAddTheIdOfHEAD;
/***/ public String anSSHSessionHasBeenAlreadyCreated;
+ /***/ public String applyBinaryBaseOidWrong;
+ /***/ public String applyBinaryOidTooShort;
+ /***/ public String applyBinaryResultOidWrong;
/***/ public String applyingCommit;
/***/ public String archiveFormatAlreadyAbsent;
/***/ public String archiveFormatAlreadyRegistered;
@@ -65,7 +68,19 @@ public class JGitText extends TranslationBundle {
/***/ public String badSectionEntry;
/***/ public String badShallowLine;
/***/ public String bareRepositoryNoWorkdirAndIndex;
+ /***/ public String base85invalidChar;
+ /***/ public String base85length;
+ /***/ public String base85overflow;
+ /***/ public String base85tooLong;
+ /***/ public String base85tooShort;
/***/ public String baseLengthIncorrect;
+ /***/ public String binaryDeltaBaseLengthMismatch;
+ /***/ public String binaryDeltaInvalidOffset;
+ /***/ public String binaryDeltaInvalidResultLength;
+ /***/ public String binaryHunkDecodeError;
+ /***/ public String binaryHunkInvalidLength;
+ /***/ public String binaryHunkLineTooShort;
+ /***/ public String binaryHunkMissingNewline;
/***/ public String bitmapMissingObject;
/***/ public String bitmapsMustBePrepared;
/***/ public String blameNotCommittedYet;
@@ -167,6 +182,7 @@ public class JGitText extends TranslationBundle {
/***/ public String connectionFailed;
/***/ public String connectionTimeOut;
/***/ public String contextMustBeNonNegative;
+ /***/ public String cookieFilePathRelative;
/***/ public String corruptionDetectedReReadingAt;
/***/ public String corruptObjectBadDate;
/***/ public String corruptObjectBadEmail;
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 9ffff9f662..40c075ec5e 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
@@ -344,10 +344,10 @@ public class GC {
&& repo.getFS()
.lastModifiedInstant(oldPack.getPackFile())
.toEpochMilli() < packExpireDate) {
- oldPack.close();
if (shouldLoosen) {
loosen(inserter, reader, oldPack, ids);
}
+ oldPack.close();
prunePack(oldPack.getPackFile());
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
index c514270f5b..6fbb4c5a07 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2008, 2017, Google Inc.
- * Copyright (C) 2017, 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2017, 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -21,7 +21,8 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
-import java.util.LinkedHashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -82,12 +83,6 @@ import org.eclipse.jgit.util.SystemReader;
*/
public class OpenSshConfigFile implements SshConfigStore {
- /**
- * "Host" name of the HostEntry for the default options before the first
- * host block in a config file.
- */
- private static final String DEFAULT_NAME = ""; //$NON-NLS-1$
-
/** The user's home directory, as key files may be relative to here. */
private final File home;
@@ -105,11 +100,9 @@ public class OpenSshConfigFile implements SshConfigStore {
* fully resolved entries created from that.
*/
private static class State {
- // Keyed by pattern; if a "Host" line has multiple patterns, we generate
- // duplicate HostEntry objects
- Map<String, HostEntry> entries = new LinkedHashMap<>();
+ List<HostEntry> entries = new LinkedList<>();
- // Keyed by user@hostname:port
+ // Previous lookups, keyed by user@hostname:port
Map<String, HostEntry> hosts = new HashMap<>();
@Override
@@ -165,14 +158,16 @@ public class OpenSshConfigFile implements SshConfigStore {
return h;
}
HostEntry fullConfig = new HostEntry();
- // Initialize with default entries at the top of the file, before the
- // first Host block.
- fullConfig.merge(cache.entries.get(DEFAULT_NAME));
- for (Map.Entry<String, HostEntry> e : cache.entries.entrySet()) {
- String pattern = e.getKey();
- if (isHostMatch(pattern, hostName)) {
- fullConfig.merge(e.getValue());
- }
+ Iterator<HostEntry> entries = cache.entries.iterator();
+ if (entries.hasNext()) {
+ // Should always have at least the first top entry containing
+ // key-value pairs before the first Host block
+ fullConfig.merge(entries.next());
+ entries.forEachRemaining(entry -> {
+ if (entry.matches(hostName)) {
+ fullConfig.merge(entry);
+ }
+ });
}
fullConfig.substitute(hostName, port, userName, localUserName, home);
cache.hosts.put(cacheKey, fullConfig);
@@ -208,20 +203,19 @@ public class OpenSshConfigFile implements SshConfigStore {
return state;
}
- private Map<String, HostEntry> parse(BufferedReader reader)
+ private List<HostEntry> parse(BufferedReader reader)
throws IOException {
- final Map<String, HostEntry> entries = new LinkedHashMap<>();
- final List<HostEntry> current = new ArrayList<>(4);
- String line;
+ final List<HostEntry> entries = new LinkedList<>();
// The man page doesn't say so, but the openssh parser (readconf.c)
// starts out in active mode and thus always applies any lines that
// occur before the first host block. We gather those options in a
// HostEntry for DEFAULT_NAME.
HostEntry defaults = new HostEntry();
- current.add(defaults);
- entries.put(DEFAULT_NAME, defaults);
+ HostEntry current = defaults;
+ entries.add(defaults);
+ String line;
while ((line = reader.readLine()) != null) {
// OpenSsh ignores trailing comments on a line. Anything after the
// first # on a line is trimmed away (yes, even if the hash is
@@ -246,38 +240,17 @@ public class OpenSshConfigFile implements SshConfigStore {
String argValue = parts.length > 1 ? parts[1].trim() : ""; //$NON-NLS-1$
if (StringUtils.equalsIgnoreCase(SshConstants.HOST, keyword)) {
- current.clear();
- for (String name : parseList(argValue)) {
- if (name == null || name.isEmpty()) {
- // null should not occur, but better be safe than sorry.
- continue;
- }
- HostEntry c = entries.get(name);
- if (c == null) {
- c = new HostEntry();
- entries.put(name, c);
- }
- current.add(c);
- }
- continue;
- }
-
- if (current.isEmpty()) {
- // We received an option outside of a Host block. We
- // don't know who this should match against, so skip.
+ current = new HostEntry(parseList(argValue));
+ entries.add(current);
continue;
}
if (HostEntry.isListKey(keyword)) {
List<String> args = validate(keyword, parseList(argValue));
- for (HostEntry entry : current) {
- entry.setValue(keyword, args);
- }
+ current.setValue(keyword, args);
} else if (!argValue.isEmpty()) {
argValue = validate(keyword, dequote(argValue));
- for (HostEntry entry : current) {
- entry.setValue(keyword, argValue);
- }
+ current.setValue(keyword, argValue);
}
}
@@ -300,7 +273,7 @@ public class OpenSshConfigFile implements SshConfigStore {
int length = argument.length();
while (start < length) {
// Skip whitespace
- if (Character.isSpaceChar(argument.charAt(start))) {
+ if (Character.isWhitespace(argument.charAt(start))) {
start++;
continue;
}
@@ -315,7 +288,7 @@ public class OpenSshConfigFile implements SshConfigStore {
} else {
int stop = start + 1;
while (stop < length
- && !Character.isSpaceChar(argument.charAt(stop))) {
+ && !Character.isWhitespace(argument.charAt(stop))) {
stop++;
}
result.add(argument.substring(start, stop));
@@ -358,13 +331,6 @@ public class OpenSshConfigFile implements SshConfigStore {
return value;
}
- private static boolean isHostMatch(String pattern, String name) {
- if (pattern.startsWith("!")) { //$NON-NLS-1$
- return !patternMatchesHost(pattern.substring(1), name);
- }
- return patternMatchesHost(pattern, name);
- }
-
private static boolean patternMatchesHost(String pattern, String name) {
if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
final FileNameMatcher fn;
@@ -389,9 +355,12 @@ public class OpenSshConfigFile implements SshConfigStore {
private static String stripWhitespace(String value) {
final StringBuilder b = new StringBuilder();
- for (int i = 0; i < value.length(); i++) {
- if (!Character.isSpaceChar(value.charAt(i)))
- b.append(value.charAt(i));
+ int length = value.length();
+ for (int i = 0; i < length; i++) {
+ char ch = value.charAt(i);
+ if (!Character.isWhitespace(ch)) {
+ b.append(ch);
+ }
}
return b.toString();
}
@@ -511,6 +480,32 @@ public class OpenSshConfigFile implements SshConfigStore {
private Map<String, List<String>> listOptions;
+ private final List<String> patterns;
+
+ // Constructor used to build the merged entry; never matches anything
+ HostEntry() {
+ this.patterns = Collections.emptyList();
+ }
+
+ HostEntry(List<String> patterns) {
+ this.patterns = patterns;
+ }
+
+ boolean matches(String hostName) {
+ boolean doesMatch = false;
+ for (String pattern : patterns) {
+ if (pattern.startsWith("!")) { //$NON-NLS-1$
+ if (patternMatchesHost(pattern.substring(1), hostName)) {
+ return false;
+ }
+ } else if (!doesMatch
+ && patternMatchesHost(pattern, hostName)) {
+ doesMatch = true;
+ }
+ }
+ return doesMatch;
+ }
+
private static String toKey(String key) {
String k = ALIASES.get(key);
return k != null ? k : key;
@@ -708,10 +703,10 @@ public class OpenSshConfigFile implements SshConfigStore {
}
private List<String> substitute(List<String> values, String allowed,
- Replacer r) {
+ Replacer r, boolean withEnv) {
List<String> result = new ArrayList<>(values.size());
for (String value : values) {
- result.add(r.substitute(value, allowed));
+ result.add(r.substitute(value, allowed, withEnv));
}
return result;
}
@@ -743,7 +738,7 @@ public class OpenSshConfigFile implements SshConfigStore {
if (hostName == null || hostName.isEmpty()) {
options.put(SshConstants.HOST_NAME, originalHostName);
} else {
- hostName = r.substitute(hostName, "h"); //$NON-NLS-1$
+ hostName = r.substitute(hostName, "h", false); //$NON-NLS-1$
options.put(SshConstants.HOST_NAME, hostName);
r.update('h', hostName);
}
@@ -752,13 +747,13 @@ public class OpenSshConfigFile implements SshConfigStore {
List<String> values = multiOptions
.get(SshConstants.IDENTITY_FILE);
if (values != null) {
- values = substitute(values, "dhlru", r); //$NON-NLS-1$
+ values = substitute(values, "dhlru", r, true); //$NON-NLS-1$
values = replaceTilde(values, home);
multiOptions.put(SshConstants.IDENTITY_FILE, values);
}
values = multiOptions.get(SshConstants.CERTIFICATE_FILE);
if (values != null) {
- values = substitute(values, "dhlru", r); //$NON-NLS-1$
+ values = substitute(values, "dhlru", r, true); //$NON-NLS-1$
values = replaceTilde(values, home);
multiOptions.put(SshConstants.CERTIFICATE_FILE, values);
}
@@ -775,29 +770,29 @@ public class OpenSshConfigFile implements SshConfigStore {
// HOSTNAME already done above
String value = options.get(SshConstants.IDENTITY_AGENT);
if (value != null) {
- value = r.substitute(value, "dhlru"); //$NON-NLS-1$
+ value = r.substitute(value, "dhlru", true); //$NON-NLS-1$
value = toFile(value, home).getPath();
options.put(SshConstants.IDENTITY_AGENT, value);
}
value = options.get(SshConstants.CONTROL_PATH);
if (value != null) {
- value = r.substitute(value, "ChLlnpru"); //$NON-NLS-1$
+ value = r.substitute(value, "ChLlnpru", true); //$NON-NLS-1$
value = toFile(value, home).getPath();
options.put(SshConstants.CONTROL_PATH, value);
}
value = options.get(SshConstants.LOCAL_COMMAND);
if (value != null) {
- value = r.substitute(value, "CdhlnprTu"); //$NON-NLS-1$
+ value = r.substitute(value, "CdhlnprTu", false); //$NON-NLS-1$
options.put(SshConstants.LOCAL_COMMAND, value);
}
value = options.get(SshConstants.REMOTE_COMMAND);
if (value != null) {
- value = r.substitute(value, "Cdhlnpru"); //$NON-NLS-1$
+ value = r.substitute(value, "Cdhlnpru", false); //$NON-NLS-1$
options.put(SshConstants.REMOTE_COMMAND, value);
}
value = options.get(SshConstants.PROXY_COMMAND);
if (value != null) {
- value = r.substitute(value, "hpr"); //$NON-NLS-1$
+ value = r.substitute(value, "hpr", false); //$NON-NLS-1$
options.put(SshConstants.PROXY_COMMAND, value);
}
}
@@ -871,7 +866,7 @@ public class OpenSshConfigFile implements SshConfigStore {
replacements.put(Character.valueOf('r'), user == null ? "" : user); //$NON-NLS-1$
replacements.put(Character.valueOf('u'), localUserName);
replacements.put(Character.valueOf('C'),
- substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
+ substitute("%l%h%p%r", "hlpr", false)); //$NON-NLS-1$ //$NON-NLS-2$
replacements.put(Character.valueOf('T'), "NONE"); //$NON-NLS-1$
}
@@ -879,36 +874,63 @@ public class OpenSshConfigFile implements SshConfigStore {
replacements.put(Character.valueOf(key), value);
if ("lhpr".indexOf(key) >= 0) { //$NON-NLS-1$
replacements.put(Character.valueOf('C'),
- substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
+ substitute("%l%h%p%r", "hlpr", false)); //$NON-NLS-1$ //$NON-NLS-2$
}
}
- public String substitute(String input, String allowed) {
+ public String substitute(String input, String allowed,
+ boolean withEnv) {
if (input == null || input.length() <= 1
- || input.indexOf('%') < 0) {
+ || (input.indexOf('%') < 0
+ && (!withEnv || input.indexOf("${") < 0))) { //$NON-NLS-1$
return input;
}
StringBuilder builder = new StringBuilder();
int start = 0;
int length = input.length();
while (start < length) {
- int percent = input.indexOf('%', start);
- if (percent < 0 || percent + 1 >= length) {
- builder.append(input.substring(start));
+ char ch = input.charAt(start);
+ switch (ch) {
+ case '%':
+ if (start + 1 >= length) {
+ break;
+ }
+ String replacement = null;
+ ch = input.charAt(start + 1);
+ if (ch == '%' || allowed.indexOf(ch) >= 0) {
+ replacement = replacements.get(Character.valueOf(ch));
+ }
+ if (replacement == null) {
+ builder.append('%').append(ch);
+ } else {
+ builder.append(replacement);
+ }
+ start += 2;
+ continue;
+ case '$':
+ if (!withEnv || start + 2 >= length) {
+ break;
+ }
+ ch = input.charAt(start + 1);
+ if (ch == '{') {
+ int close = input.indexOf('}', start + 2);
+ if (close > start + 2) {
+ String variable = SystemReader.getInstance()
+ .getenv(input.substring(start + 2, close));
+ if (!StringUtils.isEmptyOrNull(variable)) {
+ builder.append(variable);
+ }
+ start = close + 1;
+ continue;
+ }
+ }
+ ch = '$';
+ break;
+ default:
break;
}
- String replacement = null;
- char ch = input.charAt(percent + 1);
- if (ch == '%' || allowed.indexOf(ch) >= 0) {
- replacement = replacements.get(Character.valueOf(ch));
- }
- if (replacement == null) {
- builder.append(input.substring(start, percent + 2));
- } else {
- builder.append(input.substring(start, percent))
- .append(replacement);
- }
- start = percent + 2;
+ builder.append(ch);
+ start++;
}
return builder.toString();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index 03c1ef904c..3e3d9b5694 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -398,7 +398,15 @@ public final class ConfigConstants {
public static final String CONFIG_KEY_FF = "ff";
/**
+ * The "conflictStyle" key.
+ *
+ * @since 5.12
+ */
+ public static final String CONFIG_KEY_CONFLICTSTYLE = "conflictStyle";
+
+ /**
* The "checkstat" key
+ *
* @since 3.0
*/
public static final String CONFIG_KEY_CHECKSTAT = "checkstat";
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java
new file mode 100644
index 0000000000..6d568643d5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.merge;
+
+/**
+ * How to handle content conflicts.
+ *
+ * @since 5.12
+ */
+public enum ContentMergeStrategy {
+
+ /** Produce a conflict. */
+ CONFLICT,
+
+ /** Resolve the conflict hunk using the ours version. */
+ OURS,
+
+ /** Resolve the conflict hunk using the theirs version. */
+ THEIRS
+} \ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
index 27141c12c4..80607351ae 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
@@ -14,6 +14,7 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.diff.DiffAlgorithm;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.EditList;
@@ -28,8 +29,12 @@ import org.eclipse.jgit.merge.MergeChunk.ConflictState;
* diff algorithm.
*/
public final class MergeAlgorithm {
+
private final DiffAlgorithm diffAlg;
+ @NonNull
+ private ContentMergeStrategy strategy = ContentMergeStrategy.CONFLICT;
+
/**
* Creates a new MergeAlgorithm which uses
* {@link org.eclipse.jgit.diff.HistogramDiff} as diff algorithm
@@ -48,6 +53,30 @@ public final class MergeAlgorithm {
this.diffAlg = diff;
}
+ /**
+ * Retrieves the {@link ContentMergeStrategy}.
+ *
+ * @return the {@link ContentMergeStrategy} in effect
+ * @since 5.12
+ */
+ @NonNull
+ public ContentMergeStrategy getContentMergeStrategy() {
+ return strategy;
+ }
+
+ /**
+ * Sets the {@link ContentMergeStrategy}.
+ *
+ * @param strategy
+ * {@link ContentMergeStrategy} to set; if {@code null}, set
+ * {@link ContentMergeStrategy#CONFLICT}
+ * @since 5.12
+ */
+ public void setContentMergeStrategy(ContentMergeStrategy strategy) {
+ this.strategy = strategy == null ? ContentMergeStrategy.CONFLICT
+ : strategy;
+ }
+
// An special edit which acts as a sentinel value by marking the end the
// list of edits
private static final Edit END_EDIT = new Edit(Integer.MAX_VALUE,
@@ -79,29 +108,54 @@ public final class MergeAlgorithm {
if (theirs.size() != 0) {
EditList theirsEdits = diffAlg.diff(cmp, base, theirs);
if (!theirsEdits.isEmpty()) {
- // we deleted, they modified -> Let their complete content
- // conflict with empty text
- result.add(1, 0, 0, ConflictState.FIRST_CONFLICTING_RANGE);
- result.add(2, 0, theirs.size(),
- ConflictState.NEXT_CONFLICTING_RANGE);
- } else
+ // we deleted, they modified
+ switch (strategy) {
+ case OURS:
+ result.add(1, 0, 0, ConflictState.NO_CONFLICT);
+ break;
+ case THEIRS:
+ result.add(2, 0, theirs.size(),
+ ConflictState.NO_CONFLICT);
+ break;
+ default:
+ // Let their complete content conflict with empty text
+ result.add(1, 0, 0,
+ ConflictState.FIRST_CONFLICTING_RANGE);
+ result.add(2, 0, theirs.size(),
+ ConflictState.NEXT_CONFLICTING_RANGE);
+ break;
+ }
+ } else {
// we deleted, they didn't modify -> Let our deletion win
result.add(1, 0, 0, ConflictState.NO_CONFLICT);
- } else
+ }
+ } else {
// we and they deleted -> return a single chunk of nothing
result.add(1, 0, 0, ConflictState.NO_CONFLICT);
+ }
return result;
} else if (theirs.size() == 0) {
EditList oursEdits = diffAlg.diff(cmp, base, ours);
if (!oursEdits.isEmpty()) {
- // we modified, they deleted -> Let our complete content
- // conflict with empty text
- result.add(1, 0, ours.size(),
- ConflictState.FIRST_CONFLICTING_RANGE);
- result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE);
- } else
+ // we modified, they deleted
+ switch (strategy) {
+ case OURS:
+ result.add(1, 0, ours.size(), ConflictState.NO_CONFLICT);
+ break;
+ case THEIRS:
+ result.add(2, 0, 0, ConflictState.NO_CONFLICT);
+ break;
+ default:
+ // Let our complete content conflict with empty text
+ result.add(1, 0, ours.size(),
+ ConflictState.FIRST_CONFLICTING_RANGE);
+ result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE);
+ break;
+ }
+ } else {
// they deleted, we didn't modify -> Let their deletion win
result.add(2, 0, 0, ConflictState.NO_CONFLICT);
+ }
return result;
}
@@ -249,12 +303,26 @@ public final class MergeAlgorithm {
// Add the conflict (Only if there is a conflict left to report)
if (minBSize > 0 || BSizeDelta != 0) {
- result.add(1, oursBeginB + commonPrefix, oursEndB
- - commonSuffix,
- ConflictState.FIRST_CONFLICTING_RANGE);
- result.add(2, theirsBeginB + commonPrefix, theirsEndB
- - commonSuffix,
- ConflictState.NEXT_CONFLICTING_RANGE);
+ switch (strategy) {
+ case OURS:
+ result.add(1, oursBeginB + commonPrefix,
+ oursEndB - commonSuffix,
+ ConflictState.NO_CONFLICT);
+ break;
+ case THEIRS:
+ result.add(2, theirsBeginB + commonPrefix,
+ theirsEndB - commonSuffix,
+ ConflictState.NO_CONFLICT);
+ break;
+ default:
+ result.add(1, oursBeginB + commonPrefix,
+ oursEndB - commonSuffix,
+ ConflictState.FIRST_CONFLICTING_RANGE);
+ result.add(2, theirsBeginB + commonPrefix,
+ theirsEndB - commonSuffix,
+ ConflictState.NEXT_CONFLICTING_RANGE);
+ break;
+ }
}
// Add the common lines at end of conflict
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 4bfb38d286..7767662867 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -37,6 +37,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.attributes.Attributes;
import org.eclipse.jgit.diff.DiffAlgorithm;
import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
@@ -268,6 +269,13 @@ public class ResolveMerger extends ThreeWayMerger {
private int inCoreLimit;
/**
+ * The {@link ContentMergeStrategy} to use for "resolve" and "recursive"
+ * merges.
+ */
+ @NonNull
+ private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT;
+
+ /**
* Keeps {@link CheckoutMetadata} for {@link #checkout()} and
* {@link #cleanUp()}.
*/
@@ -344,6 +352,29 @@ public class ResolveMerger extends ThreeWayMerger {
dircache = DirCache.newInCore();
}
+ /**
+ * Retrieves the content merge strategy for content conflicts.
+ *
+ * @return the {@link ContentMergeStrategy} in effect
+ * @since 5.12
+ */
+ @NonNull
+ public ContentMergeStrategy getContentMergeStrategy() {
+ return contentStrategy;
+ }
+
+ /**
+ * Sets the content merge strategy for content conflicts.
+ *
+ * @param strategy
+ * {@link ContentMergeStrategy} to use
+ * @since 5.12
+ */
+ public void setContentMergeStrategy(ContentMergeStrategy strategy) {
+ contentStrategy = strategy == null ? ContentMergeStrategy.CONFLICT
+ : strategy;
+ }
+
/** {@inheritDoc} */
@Override
protected boolean mergeImpl() throws IOException {
@@ -644,15 +675,19 @@ public class ResolveMerger extends ThreeWayMerger {
}
return true;
}
- // FileModes are not mergeable. We found a conflict on modes.
- // For conflicting entries we don't know lastModified and
- // length.
- add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
- add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
- add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
- unmergedPaths.add(tw.getPathString());
- mergeResults.put(tw.getPathString(),
- new MergeResult<>(Collections.<RawText> emptyList()));
+ if (!ignoreConflicts) {
+ // FileModes are not mergeable. We found a conflict on modes.
+ // For conflicting entries we don't know lastModified and
+ // length.
+ // This path can be skipped on ignoreConflicts, so the caller
+ // could use virtual commit.
+ add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
+ add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
+ add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
+ unmergedPaths.add(tw.getPathString());
+ mergeResults.put(tw.getPathString(),
+ new MergeResult<>(Collections.emptyList()));
+ }
return true;
}
@@ -757,6 +792,19 @@ public class ResolveMerger extends ThreeWayMerger {
unmergedPaths.add(tw.getPathString());
return true;
} else if (!attributes.canBeContentMerged()) {
+ // File marked as binary
+ switch (getContentMergeStrategy()) {
+ case OURS:
+ keep(ourDce);
+ return true;
+ case THEIRS:
+ DirCacheEntry theirEntry = add(tw.getRawPath(), theirs,
+ DirCacheEntry.STAGE_0, EPOCH, 0);
+ addToCheckout(tw.getPathString(), theirEntry, attributes);
+ return true;
+ default:
+ break;
+ }
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
@@ -771,8 +819,26 @@ public class ResolveMerger extends ThreeWayMerger {
return false;
}
- MergeResult<RawText> result = contentMerge(base, ours, theirs,
- attributes);
+ MergeResult<RawText> result = null;
+ try {
+ result = contentMerge(base, ours, theirs, attributes,
+ getContentMergeStrategy());
+ } catch (BinaryBlobException e) {
+ switch (getContentMergeStrategy()) {
+ case OURS:
+ keep(ourDce);
+ return true;
+ case THEIRS:
+ DirCacheEntry theirEntry = add(tw.getRawPath(), theirs,
+ DirCacheEntry.STAGE_0, EPOCH, 0);
+ addToCheckout(tw.getPathString(), theirEntry, attributes);
+ return true;
+ default:
+ result = new MergeResult<>(Collections.emptyList());
+ result.setContainsConflicts(true);
+ break;
+ }
+ }
if (ignoreConflicts) {
result.setContainsConflicts(false);
}
@@ -799,9 +865,16 @@ public class ResolveMerger extends ThreeWayMerger {
mergeResults.put(tw.getPathString(), result);
unmergedPaths.add(tw.getPathString());
} else {
- MergeResult<RawText> result = contentMerge(base, ours,
- theirs, attributes);
-
+ // Content merge strategy does not apply to delete-modify
+ // conflicts!
+ MergeResult<RawText> result;
+ try {
+ result = contentMerge(base, ours, theirs, attributes,
+ ContentMergeStrategy.CONFLICT);
+ } catch (BinaryBlobException e) {
+ result = new MergeResult<>(Collections.emptyList());
+ result.setContainsConflicts(true);
+ }
if (ignoreConflicts) {
// In case a conflict is detected the working tree file
// is again filled with new content (containing conflict
@@ -863,32 +936,26 @@ public class ResolveMerger extends ThreeWayMerger {
* @param ours
* @param theirs
* @param attributes
+ * @param strategy
*
* @return the result of the content merge
+ * @throws BinaryBlobException
+ * if any of the blobs looks like a binary blob
* @throws IOException
*/
private MergeResult<RawText> contentMerge(CanonicalTreeParser base,
CanonicalTreeParser ours, CanonicalTreeParser theirs,
- Attributes attributes)
- throws IOException {
- RawText baseText;
- RawText ourText;
- RawText theirsText;
-
- try {
- baseText = base == null ? RawText.EMPTY_TEXT : getRawText(
- base.getEntryObjectId(), attributes);
- ourText = ours == null ? RawText.EMPTY_TEXT : getRawText(
- ours.getEntryObjectId(), attributes);
- theirsText = theirs == null ? RawText.EMPTY_TEXT : getRawText(
- theirs.getEntryObjectId(), attributes);
- } catch (BinaryBlobException e) {
- MergeResult<RawText> r = new MergeResult<>(Collections.<RawText>emptyList());
- r.setContainsConflicts(true);
- return r;
- }
- return (mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText,
- ourText, theirsText));
+ Attributes attributes, ContentMergeStrategy strategy)
+ throws BinaryBlobException, IOException {
+ RawText baseText = base == null ? RawText.EMPTY_TEXT
+ : getRawText(base.getEntryObjectId(), attributes);
+ RawText ourText = ours == null ? RawText.EMPTY_TEXT
+ : getRawText(ours.getEntryObjectId(), attributes);
+ RawText theirsText = theirs == null ? RawText.EMPTY_TEXT
+ : getRawText(theirs.getEntryObjectId(), attributes);
+ mergeAlgorithm.setContentMergeStrategy(strategy);
+ return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText,
+ ourText, theirsText);
}
private boolean isIndexDirty() {
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 631d861c0d..6e29438d09 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
@@ -32,10 +32,13 @@ import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.AsyncObjectLoaderQueue;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.MutableObjectId;
+import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdOwnerMap;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
@@ -181,6 +184,12 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable {
boolean shallowCommitsInitialized;
+ private enum GetMergedIntoStrategy {
+ RETURN_ON_FIRST_FOUND,
+ RETURN_ON_FIRST_NOT_FOUND,
+ EVALUATE_ALL
+ }
+
/**
* Create a new revision walker for a given repository.
*
@@ -425,6 +434,155 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable {
}
/**
+ * Determine the Refs into which a commit is merged.
+ * <p>
+ * A commit is merged into a ref if we can find a path of commits that leads
+ * from that specific ref and ends at <code>commit</code>.
+ * <p>
+ *
+ * @param commit
+ * commit the caller thinks is reachable from <code>refs</code>.
+ * @param refs
+ * refs to start iteration from, and which is most likely a
+ * descendant (child) of <code>commit</code>.
+ * @return list of refs that are reachable from <code>commit</code>.
+ * @throws java.io.IOException
+ * a pack file or loose object could not be read.
+ * @since 5.12
+ */
+ public List<Ref> getMergedInto(RevCommit commit, Collection<Ref> refs)
+ throws IOException{
+ return getMergedInto(commit, refs, NullProgressMonitor.INSTANCE);
+ }
+
+ /**
+ * Determine the Refs into which a commit is merged.
+ * <p>
+ * A commit is merged into a ref if we can find a path of commits that leads
+ * from that specific ref and ends at <code>commit</code>.
+ * <p>
+ *
+ * @param commit
+ * commit the caller thinks is reachable from <code>refs</code>.
+ * @param refs
+ * refs to start iteration from, and which is most likely a
+ * descendant (child) of <code>commit</code>.
+ * @param monitor
+ * the callback for progress and cancellation
+ * @return list of refs that are reachable from <code>commit</code>.
+ * @throws java.io.IOException
+ * a pack file or loose object could not be read.
+ * @since 5.12
+ */
+ public List<Ref> getMergedInto(RevCommit commit, Collection<Ref> refs,
+ ProgressMonitor monitor) throws IOException{
+ return getMergedInto(commit, refs,
+ GetMergedIntoStrategy.EVALUATE_ALL,
+ monitor);
+ }
+
+ /**
+ * Determine if a <code>commit</code> is merged into any of the given
+ * <code>refs</code>.
+ *
+ * @param commit
+ * commit the caller thinks is reachable from <code>refs</code>.
+ * @param refs
+ * refs to start iteration from, and which is most likely a
+ * descendant (child) of <code>commit</code>.
+ * @return true if commit is merged into any of the refs; false otherwise.
+ * @throws java.io.IOException
+ * a pack file or loose object could not be read.
+ * @since 5.12
+ */
+ public boolean isMergedIntoAny(RevCommit commit, Collection<Ref> refs)
+ throws IOException {
+ return getMergedInto(commit, refs,
+ GetMergedIntoStrategy.RETURN_ON_FIRST_FOUND,
+ NullProgressMonitor.INSTANCE).size() > 0;
+ }
+
+ /**
+ * Determine if a <code>commit</code> is merged into all of the given
+ * <code>refs</code>.
+ *
+ * @param commit
+ * commit the caller thinks is reachable from <code>refs</code>.
+ * @param refs
+ * refs to start iteration from, and which is most likely a
+ * descendant (child) of <code>commit</code>.
+ * @return true if commit is merged into all of the refs; false otherwise.
+ * @throws java.io.IOException
+ * a pack file or loose object could not be read.
+ * @since 5.12
+ */
+ public boolean isMergedIntoAll(RevCommit commit, Collection<Ref> refs)
+ throws IOException {
+ return getMergedInto(commit, refs,
+ GetMergedIntoStrategy.RETURN_ON_FIRST_NOT_FOUND,
+ NullProgressMonitor.INSTANCE).size()
+ == refs.size();
+ }
+
+ private List<Ref> getMergedInto(RevCommit needle, Collection<Ref> haystacks,
+ Enum returnStrategy, ProgressMonitor monitor) throws IOException {
+ List<Ref> result = new ArrayList<>();
+ List<RevCommit> uninteresting = new ArrayList<>();
+ List<RevCommit> marked = new ArrayList<>();
+ RevFilter oldRF = filter;
+ TreeFilter oldTF = treeFilter;
+ try {
+ finishDelayedFreeFlags();
+ reset(~freeFlags & APP_FLAGS);
+ filter = RevFilter.ALL;
+ treeFilter = TreeFilter.ALL;
+ for (Ref r: haystacks) {
+ if (monitor.isCancelled()) {
+ return result;
+ }
+ monitor.update(1);
+ RevObject o = peel(parseAny(r.getObjectId()));
+ if (!(o instanceof RevCommit)) {
+ continue;
+ }
+ RevCommit c = (RevCommit) o;
+ reset(UNINTERESTING | TEMP_MARK);
+ markStart(c);
+ boolean commitFound = false;
+ RevCommit next;
+ while ((next = next()) != null) {
+ if (References.isSameObject(next, needle)
+ || (next.flags & TEMP_MARK) != 0) {
+ result.add(r);
+ if (returnStrategy == GetMergedIntoStrategy.RETURN_ON_FIRST_FOUND) {
+ return result;
+ }
+ commitFound = true;
+ c.flags |= TEMP_MARK;
+ marked.add(c);
+ break;
+ }
+ }
+ if(!commitFound){
+ markUninteresting(c);
+ uninteresting.add(c);
+ if (returnStrategy == GetMergedIntoStrategy.RETURN_ON_FIRST_NOT_FOUND) {
+ return result;
+ }
+ }
+ }
+ } finally {
+ roots.addAll(uninteresting);
+ filter = oldRF;
+ treeFilter = oldTF;
+ for (RevCommit c : marked) {
+ c.flags &= ~TEMP_MARK;
+ }
+ }
+ return result;
+ }
+
+ /**
* Pop the next most recent commit.
*
* @return next most recent commit; null if traversal is over.
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 3feb9c5a45..e52e916318 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalkUtils.java
@@ -159,15 +159,12 @@ public final class RevWalkUtils {
// Make sure commit is from the same RevWalk
commit = revWalk.parseCommit(commit.getId());
revWalk.reset();
- List<Ref> result = new ArrayList<>();
+ List<Ref> filteredRefs = 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;
@@ -179,9 +176,9 @@ public final class RevWalkUtils {
if (headCommit.getCommitTime() + SKEW < commit.getCommitTime())
continue;
- if (revWalk.isMergedInto(commit, headCommit))
- result.add(ref);
+ filteredRefs.add(ref);
}
+ List<Ref> result = revWalk.getMergedInto(commit, filteredRefs, monitor);
monitor.endTask();
return result;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java
index be55cd1b81..5cd5b334ab 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java
@@ -118,7 +118,7 @@ public final class SshConstants {
* Key in an ssh config file; defines signature algorithms for public key
* authentication as a comma-separated list.
*
- * @since 5.11
+ * @since 5.11.1
*/
public static final String PUBKEY_ACCEPTED_ALGORITHMS = "PubkeyAcceptedAlgorithms";
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 a5b377366f..405373a0f9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
@@ -35,6 +35,7 @@ import static org.eclipse.jgit.util.HttpSupport.METHOD_POST;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
+import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -53,8 +54,6 @@ import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.InvalidPathException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertPathValidatorException;
@@ -101,6 +100,7 @@ import org.eclipse.jgit.transport.HttpConfig.HttpRedirectMode;
import org.eclipse.jgit.transport.http.HttpConnection;
import org.eclipse.jgit.transport.http.HttpConnectionFactory;
import org.eclipse.jgit.transport.http.HttpConnectionFactory2;
+import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.HttpSupport;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
@@ -1157,17 +1157,28 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
return new TransportException(uri, why);
}
- private static NetscapeCookieFile getCookieFileFromConfig(
+ private NetscapeCookieFile getCookieFileFromConfig(
HttpConfig config) {
- if (!StringUtils.isEmptyOrNull(config.getCookieFile())) {
+ String path = config.getCookieFile();
+ if (!StringUtils.isEmptyOrNull(path)) {
try {
- Path cookieFilePath = Paths.get(config.getCookieFile());
+ FS fs = local != null ? local.getFS() : FS.DETECTED;
+ File f;
+ if (path.startsWith("~/")) { //$NON-NLS-1$
+ f = fs.resolve(fs.userHome(), path.substring(2));
+ } else {
+ f = new File(path);
+ if (!f.isAbsolute()) {
+ f = fs.resolve(null, path);
+ LOG.warn(MessageFormat.format(
+ JGitText.get().cookieFilePathRelative, f));
+ }
+ }
return NetscapeCookieFileCache.getInstance(config)
- .getEntry(cookieFilePath);
+ .getEntry(f.toPath());
} catch (InvalidPathException e) {
LOG.warn(MessageFormat.format(
- JGitText.get().couldNotReadCookieFile,
- config.getCookieFile()), e);
+ JGitText.get().couldNotReadCookieFile, path), e);
}
}
return null;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java
index 979961f2ae..c0de42cb57 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UsernamePasswordCredentialsProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010, Google Inc. and others
+ * Copyright (C) 2010, 2021 Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -58,14 +58,21 @@ public class UsernamePasswordCredentialsProvider extends CredentialsProvider {
@Override
public boolean supports(CredentialItem... items) {
for (CredentialItem i : items) {
- if (i instanceof CredentialItem.Username)
+ if (i instanceof CredentialItem.InformationalMessage) {
continue;
-
- else if (i instanceof CredentialItem.Password)
+ }
+ if (i instanceof CredentialItem.Username) {
continue;
-
- else
- return false;
+ }
+ if (i instanceof CredentialItem.Password) {
+ continue;
+ }
+ if (i instanceof CredentialItem.StringType) {
+ if (i.getPromptText().equals("Password: ")) { //$NON-NLS-1$
+ continue;
+ }
+ }
+ return false;
}
return true;
}
@@ -75,6 +82,9 @@ public class UsernamePasswordCredentialsProvider extends CredentialsProvider {
public boolean get(URIish uri, CredentialItem... items)
throws UnsupportedCredentialItem {
for (CredentialItem i : items) {
+ if (i instanceof CredentialItem.InformationalMessage) {
+ continue;
+ }
if (i instanceof CredentialItem.Username) {
((CredentialItem.Username) i).setValue(username);
continue;
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 55b7d6279a..0b7c0a9e4b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
@@ -502,8 +502,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
throw new IOException(new FilterFailedException(rc,
filterCommand, getEntryPathString(),
result.getStdout().toByteArray(MAX_EXCEPTION_TEXT_SIZE),
- RawParseUtils.decode(result.getStderr()
- .toByteArray(MAX_EXCEPTION_TEXT_SIZE))));
+ result.getStderr().toString(MAX_EXCEPTION_TEXT_SIZE)));
}
return result.getStdout().openInputStreamWithAutoDestroy();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Base85.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Base85.java
new file mode 100644
index 0000000000..54b7cfcaa7
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Base85.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util;
+
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Arrays;
+
+import org.eclipse.jgit.internal.JGitText;
+
+/**
+ * Base-85 encoder/decoder.
+ *
+ * @since 5.12
+ */
+public final class Base85 {
+
+ private static final byte[] ENCODE = ("0123456789" //$NON-NLS-1$
+ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" //$NON-NLS-1$
+ + "abcdefghijklmnopqrstuvwxyz" //$NON-NLS-1$
+ + "!#$%&()*+-;<=>?@^_`{|}~") //$NON-NLS-1$
+ .getBytes(StandardCharsets.US_ASCII);
+
+ private static final int[] DECODE = new int[256];
+
+ static {
+ Arrays.fill(DECODE, -1);
+ for (int i = 0; i < ENCODE.length; i++) {
+ DECODE[ENCODE[i]] = i;
+ }
+ }
+
+ private Base85() {
+ // No instantiation
+ }
+
+ /**
+ * Determines the length of the base-85 encoding for {@code rawLength}
+ * bytes.
+ *
+ * @param rawLength
+ * number of bytes to encode
+ * @return number of bytes needed for the base-85 encoding of
+ * {@code rawLength} bytes
+ */
+ public static int encodedLength(int rawLength) {
+ return (rawLength + 3) / 4 * 5;
+ }
+
+ /**
+ * Encodes the given {@code data} in Base-85.
+ *
+ * @param data
+ * to encode
+ * @return encoded data
+ */
+ public static byte[] encode(byte[] data) {
+ return encode(data, 0, data.length);
+ }
+
+ /**
+ * Encodes {@code length} bytes of {@code data} in Base-85, beginning at the
+ * {@code start} index.
+ *
+ * @param data
+ * to encode
+ * @param start
+ * index of the first byte to encode
+ * @param length
+ * number of bytes to encode
+ * @return encoded data
+ */
+ public static byte[] encode(byte[] data, int start, int length) {
+ byte[] result = new byte[encodedLength(length)];
+ int end = start + length;
+ int in = start;
+ int out = 0;
+ while (in < end) {
+ // Accumulate remaining bytes MSB first as a 32bit value
+ long accumulator = ((long) (data[in++] & 0xFF)) << 24;
+ if (in < end) {
+ accumulator |= (data[in++] & 0xFF) << 16;
+ if (in < end) {
+ accumulator |= (data[in++] & 0xFF) << 8;
+ if (in < end) {
+ accumulator |= (data[in++] & 0xFF);
+ }
+ }
+ }
+ // Write the 32bit value in base-85 encoding, also MSB first
+ for (int i = 4; i >= 0; i--) {
+ result[out + i] = ENCODE[(int) (accumulator % 85)];
+ accumulator /= 85;
+ }
+ out += 5;
+ }
+ return result;
+ }
+
+ /**
+ * Decodes the Base-85 {@code encoded} data into a byte array of
+ * {@code expectedSize} bytes.
+ *
+ * @param encoded
+ * Base-85 encoded data
+ * @param expectedSize
+ * of the result
+ * @return the decoded bytes
+ * @throws IllegalArgumentException
+ * if expectedSize doesn't match, the encoded data has a length
+ * that is not a multiple of 5, or there are invalid characters
+ * in the encoded data
+ */
+ public static byte[] decode(byte[] encoded, int expectedSize) {
+ return decode(encoded, 0, encoded.length, expectedSize);
+ }
+
+ /**
+ * Decodes {@code length} bytes of Base-85 {@code encoded} data, beginning
+ * at the {@code start} index, into a byte array of {@code expectedSize}
+ * bytes.
+ *
+ * @param encoded
+ * Base-85 encoded data
+ * @param start
+ * index at which the data to decode starts in {@code encoded}
+ * @param length
+ * of the Base-85 encoded data
+ * @param expectedSize
+ * of the result
+ * @return the decoded bytes
+ * @throws IllegalArgumentException
+ * if expectedSize doesn't match, {@code length} is not a
+ * multiple of 5, or there are invalid characters in the encoded
+ * data
+ */
+ public static byte[] decode(byte[] encoded, int start, int length,
+ int expectedSize) {
+ if (length % 5 != 0) {
+ throw new IllegalArgumentException(JGitText.get().base85length);
+ }
+ byte[] result = new byte[expectedSize];
+ int end = start + length;
+ int in = start;
+ int out = 0;
+ while (in < end && out < expectedSize) {
+ // Accumulate 5 bytes, "MSB" first
+ long accumulator = 0;
+ for (int i = 4; i >= 0; i--) {
+ int val = DECODE[encoded[in++] & 0xFF];
+ if (val < 0) {
+ throw new IllegalArgumentException(MessageFormat.format(
+ JGitText.get().base85invalidChar,
+ Integer.toHexString(encoded[in - 1] & 0xFF)));
+ }
+ accumulator = accumulator * 85 + val;
+ }
+ if (accumulator > 0xFFFF_FFFFL) {
+ throw new IllegalArgumentException(
+ MessageFormat.format(JGitText.get().base85overflow,
+ Long.toHexString(accumulator)));
+ }
+ // Write remaining bytes, MSB first
+ result[out++] = (byte) (accumulator >>> 24);
+ if (out < expectedSize) {
+ result[out++] = (byte) (accumulator >>> 16);
+ if (out < expectedSize) {
+ result[out++] = (byte) (accumulator >>> 8);
+ if (out < expectedSize) {
+ result[out++] = (byte) accumulator;
+ }
+ }
+ }
+ }
+ // Should have exhausted 'in' and filled 'out' completely
+ if (in < end) {
+ throw new IllegalArgumentException(
+ MessageFormat.format(JGitText.get().base85tooLong,
+ Integer.valueOf(expectedSize)));
+ }
+ if (out < expectedSize) {
+ throw new IllegalArgumentException(
+ MessageFormat.format(JGitText.get().base85tooShort,
+ Integer.valueOf(expectedSize)));
+ }
+ return result;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java
index 562eb05dd9..fb893a66f0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java
@@ -18,6 +18,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.UncheckedIOException;
import java.util.ArrayList;
import org.eclipse.jgit.internal.JGitText;
@@ -213,6 +214,24 @@ public abstract class TemporaryBuffer extends OutputStream {
}
/**
+ * Convert first {@code limit} number of bytes of the buffer content to
+ * String.
+ *
+ * @param limit
+ * the maximum number of bytes to be converted to String
+ * @return first {@code limit} number of bytes of the buffer content
+ * converted to String.
+ * @since 5.12
+ */
+ public String toString(int limit) {
+ try {
+ return RawParseUtils.decode(toByteArray(limit));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /**
* Convert this buffer's contents into a contiguous byte array. If this size
* of the buffer exceeds the limit only return the first {@code limit} bytes
* <p>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryDeltaInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryDeltaInputStream.java
new file mode 100644
index 0000000000..9eceeb8117
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryDeltaInputStream.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util.io;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StreamCorruptedException;
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.internal.JGitText;
+
+/**
+ * An {@link InputStream} that applies a binary delta to a base on the fly.
+ * <p>
+ * Delta application to a base needs random access to the base data. The delta
+ * is expressed as a sequence of copy and insert instructions. A copy
+ * instruction has the form "COPY fromOffset length" and says "copy length bytes
+ * from the base, starting at offset fromOffset, to the result". An insert
+ * instruction has the form "INSERT length" followed by length bytes and says
+ * "copy the next length bytes from the delta to the result".
+ * </p>
+ * <p>
+ * These instructions are generated using a content-defined chunking algorithm
+ * (currently C git uses the standard Rabin variant; but there are others that
+ * could be used) that identifies equal chunks. It is entirely possible that a
+ * later copy instruction has a fromOffset that is before the fromOffset of an
+ * earlier copy instruction.
+ * </p>
+ * <p>
+ * This makes it impossible to stream the base.
+ * </p>
+ * <p>
+ * JGit is limited to 2GB maximum size for the base since array indices are
+ * signed 32bit values.
+ *
+ * @since 5.12
+ */
+public class BinaryDeltaInputStream extends InputStream {
+
+ private final byte[] base;
+
+ private final InputStream delta;
+
+ private long resultLength;
+
+ private long toDeliver = -1;
+
+ private int fromBase;
+
+ private int fromDelta;
+
+ private int baseOffset = -1;
+
+ /**
+ * Creates a new {@link BinaryDeltaInputStream} that applies {@code delta}
+ * to {@code base}.
+ *
+ * @param base
+ * data to apply the delta to
+ * @param delta
+ * {@link InputStream} delivering the delta to apply
+ */
+ public BinaryDeltaInputStream(byte[] base, InputStream delta) {
+ this.base = base;
+ this.delta = delta;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int b = readNext();
+ if (b >= 0) {
+ toDeliver--;
+ }
+ return b;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ return super.read(b, off, len);
+ }
+
+ private void initialize() throws IOException {
+ long baseSize = readVarInt(delta);
+ if (baseSize > Integer.MAX_VALUE || baseSize < 0
+ || (int) baseSize != base.length) {
+ throw new IOException(MessageFormat.format(
+ JGitText.get().binaryDeltaBaseLengthMismatch,
+ Integer.valueOf(base.length), Long.valueOf(baseSize)));
+ }
+ resultLength = readVarInt(delta);
+ if (resultLength < 0) {
+ throw new StreamCorruptedException(
+ JGitText.get().binaryDeltaInvalidResultLength);
+ }
+ toDeliver = resultLength;
+ baseOffset = 0;
+ }
+
+ private int readNext() throws IOException {
+ if (baseOffset < 0) {
+ initialize();
+ }
+ if (fromBase > 0) {
+ fromBase--;
+ return base[baseOffset++] & 0xFF;
+ } else if (fromDelta > 0) {
+ fromDelta--;
+ return delta.read();
+ }
+ int command = delta.read();
+ if (command < 0) {
+ return -1;
+ }
+ if ((command & 0x80) != 0) {
+ // Decode offset and length to read from base
+ long copyOffset = 0;
+ for (int i = 1, shift = 0; i < 0x10; i *= 2, shift += 8) {
+ if ((command & i) != 0) {
+ copyOffset |= ((long) next(delta)) << shift;
+ }
+ }
+ int copySize = 0;
+ for (int i = 0x10, shift = 0; i < 0x80; i *= 2, shift += 8) {
+ if ((command & i) != 0) {
+ copySize |= next(delta) << shift;
+ }
+ }
+ if (copySize == 0) {
+ copySize = 0x10000;
+ }
+ if (copyOffset > base.length - copySize) {
+ throw new StreamCorruptedException(MessageFormat.format(
+ JGitText.get().binaryDeltaInvalidOffset,
+ Long.valueOf(copyOffset), Integer.valueOf(copySize)));
+ }
+ baseOffset = (int) copyOffset;
+ fromBase = copySize;
+ return readNext();
+ } else if (command != 0) {
+ // The next 'command' bytes come from the delta
+ fromDelta = command - 1;
+ return delta.read();
+ } else {
+ // Zero is reserved
+ throw new StreamCorruptedException(
+ JGitText.get().unsupportedCommand0);
+ }
+ }
+
+ private int next(InputStream in) throws IOException {
+ int b = in.read();
+ if (b < 0) {
+ throw new EOFException();
+ }
+ return b;
+ }
+
+ private long readVarInt(InputStream in) throws IOException {
+ long val = 0;
+ int shift = 0;
+ int b;
+ do {
+ b = next(in);
+ val |= ((long) (b & 0x7f)) << shift;
+ shift += 7;
+ } while ((b & 0x80) != 0);
+ return val;
+ }
+
+ /**
+ * Tells the expected size of the final result.
+ *
+ * @return the size
+ * @throws IOException
+ * if the size cannot be determined from {@code delta}
+ */
+ public long getExpectedResultSize() throws IOException {
+ if (baseOffset < 0) {
+ initialize();
+ }
+ return resultLength;
+ }
+
+ /**
+ * Tells whether the delta has been fully consumed, and the expected number
+ * of bytes for the combined result have been read from this
+ * {@link BinaryDeltaInputStream}.
+ *
+ * @return whether delta application was successful
+ */
+ public boolean isFullyConsumed() {
+ try {
+ return toDeliver == 0 && delta.read() < 0;
+ } catch (IOException e) {
+ return toDeliver == 0;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ delta.close();
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java
new file mode 100644
index 0000000000..4f940d77a0
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util.io;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StreamCorruptedException;
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.util.Base85;
+
+/**
+ * A stream that decodes git binary patch data on the fly.
+ *
+ * @since 5.12
+ */
+public class BinaryHunkInputStream extends InputStream {
+
+ private final InputStream in;
+
+ private int lineNumber;
+
+ private byte[] buffer;
+
+ private int pos = 0;
+
+ /**
+ * Creates a new {@link BinaryHunkInputStream}.
+ *
+ * @param in
+ * {@link InputStream} to read the base-85 encoded patch data
+ * from
+ */
+ public BinaryHunkInputStream(InputStream in) {
+ this.in = in;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (pos < 0) {
+ return -1;
+ }
+ if (buffer == null || pos == buffer.length) {
+ fillBuffer();
+ }
+ if (pos >= 0) {
+ return buffer[pos++] & 0xFF;
+ }
+ return -1;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ return super.read(b, off, len);
+ }
+
+ @Override
+ public void close() throws IOException {
+ in.close();
+ buffer = null;
+ }
+
+ private void fillBuffer() throws IOException {
+ int length = in.read();
+ if (length < 0) {
+ pos = length;
+ buffer = null;
+ return;
+ }
+ lineNumber++;
+ // Length is encoded with characters, A..Z for 1..26 and a..z for 27..52
+ if ('A' <= length && length <= 'Z') {
+ length = length - 'A' + 1;
+ } else if ('a' <= length && length <= 'z') {
+ length = length - 'a' + 27;
+ } else {
+ throw new StreamCorruptedException(MessageFormat.format(
+ JGitText.get().binaryHunkInvalidLength,
+ Integer.valueOf(lineNumber), Integer.toHexString(length)));
+ }
+ byte[] encoded = new byte[Base85.encodedLength(length)];
+ for (int i = 0; i < encoded.length; i++) {
+ int b = in.read();
+ if (b < 0 || b == '\n') {
+ throw new EOFException(MessageFormat.format(
+ JGitText.get().binaryHunkInvalidLength,
+ Integer.valueOf(lineNumber)));
+ }
+ encoded[i] = (byte) b;
+ }
+ // Must be followed by a newline; tolerate EOF.
+ int b = in.read();
+ if (b >= 0 && b != '\n') {
+ throw new StreamCorruptedException(MessageFormat.format(
+ JGitText.get().binaryHunkMissingNewline,
+ Integer.valueOf(lineNumber)));
+ }
+ try {
+ buffer = Base85.decode(encoded, length);
+ } catch (IllegalArgumentException e) {
+ StreamCorruptedException ex = new StreamCorruptedException(
+ MessageFormat.format(JGitText.get().binaryHunkDecodeError,
+ Integer.valueOf(lineNumber)));
+ ex.initCause(e);
+ throw ex;
+ }
+ pos = 0;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkOutputStream.java
new file mode 100644
index 0000000000..30551c09fd
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkOutputStream.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.eclipse.jgit.util.Base85;
+
+/**
+ * An {@link OutputStream} that encodes data for a git binary patch.
+ *
+ * @since 5.12
+ */
+public class BinaryHunkOutputStream extends OutputStream {
+
+ private static final int MAX_BYTES = 52;
+
+ private final OutputStream out;
+
+ private final byte[] buffer = new byte[MAX_BYTES];
+
+ private int pos;
+
+ /**
+ * Creates a new {@link BinaryHunkOutputStream}.
+ *
+ * @param out
+ * {@link OutputStream} to write the encoded data to
+ */
+ public BinaryHunkOutputStream(OutputStream out) {
+ this.out = out;
+ }
+
+ /**
+ * Flushes and closes this stream, and closes the underlying
+ * {@link OutputStream}.
+ */
+ @Override
+ public void close() throws IOException {
+ flush();
+ out.close();
+ }
+
+ /**
+ * Writes any buffered output as a binary patch line to the underlying
+ * {@link OutputStream} and flushes that stream, too.
+ */
+ @Override
+ public void flush() throws IOException {
+ if (pos > 0) {
+ encode(buffer, 0, pos);
+ pos = 0;
+ }
+ out.flush();
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ buffer[pos++] = (byte) b;
+ if (pos == buffer.length) {
+ encode(buffer, 0, pos);
+ pos = 0;
+ }
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ if (len == 0) {
+ return;
+ }
+ int toCopy = len;
+ int in = off;
+ if (pos > 0) {
+ // Fill the buffer
+ int chunk = Math.min(toCopy, buffer.length - pos);
+ System.arraycopy(b, in, buffer, pos, chunk);
+ in += chunk;
+ pos += chunk;
+ toCopy -= chunk;
+ if (pos == buffer.length) {
+ encode(buffer, 0, pos);
+ pos = 0;
+ }
+ if (toCopy == 0) {
+ return;
+ }
+ }
+ while (toCopy >= MAX_BYTES) {
+ encode(b, in, MAX_BYTES);
+ toCopy -= MAX_BYTES;
+ in += MAX_BYTES;
+ }
+ if (toCopy > 0) {
+ System.arraycopy(b, in, buffer, 0, toCopy);
+ pos = toCopy;
+ }
+ }
+
+ private void encode(byte[] data, int off, int length) throws IOException {
+ if (length <= 26) {
+ out.write('A' + length - 1);
+ } else {
+ out.write('a' + length - 27);
+ }
+ out.write(Base85.encode(data, off, length));
+ out.write('\n');
+ }
+}