Merge branch 'stable-6.1' into stable-6.2

* stable-6.1:
  Checkout: better directory handling

Change-Id: I2f8e5410f4962d38bc6e822b5cda42df68aef32b
diff --git a/DEPENDENCIES b/DEPENDENCIES
index 93fa850..be7ab17 100644
--- a/DEPENDENCIES
+++ b/DEPENDENCIES
@@ -2,10 +2,10 @@
 maven/mavencentral/com.google.code.gson/gson/2.8.9, Apache-2.0, approved, CQ23496
 maven/mavencentral/com.googlecode.javaewah/JavaEWAH/1.1.13, Apache-2.0, approved, CQ11658
 maven/mavencentral/com.jcraft/jsch/0.1.55, BSD-3-Clause, approved, CQ19435
-maven/mavencentral/com.jcraft/jzlib/1.1.1, BSD-2-Clause, approved, CQ6218
+maven/mavencentral/com.jcraft/jzlib/1.1.3, BSD-2-Clause, approved, CQ6218
 maven/mavencentral/commons-codec/commons-codec/1.11, Apache-2.0 AND BSD-3-Clause, approved, CQ15971
 maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ10162
-maven/mavencentral/javax.servlet/javax.servlet-api/4.0.0, , approved, CQ16125
+maven/mavencentral/javax.servlet/javax.servlet-api/4.0.0, (CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0) AND Apache-2.0, approved, CQ16125
 maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636
 maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.9.0, Apache-2.0, approved, clearlydefined
 maven/mavencentral/net.bytebuddy/byte-buddy/1.9.0, Apache-2.0, approved, clearlydefined
@@ -19,6 +19,8 @@
 maven/mavencentral/org.apache.commons/commons-math3/3.2, Apache-2.0, approved, clearlydefined
 maven/mavencentral/org.apache.httpcomponents/httpclient/4.5.13, Apache-2.0 AND LicenseRef-Public-Domain, approved, CQ23527
 maven/mavencentral/org.apache.httpcomponents/httpcore/4.4.14, Apache-2.0, approved, CQ23528
+maven/mavencentral/org.apache.sshd/sshd-common/2.8.0, Apache-2.0 AND ISC, approved, #2349
+maven/mavencentral/org.apache.sshd/sshd-core/2.8.0, Apache-2.0, approved, #2331
 maven/mavencentral/org.apache.sshd/sshd-osgi/2.8.0, Apache-2.0, approved, CQ23892
 maven/mavencentral/org.apache.sshd/sshd-sftp/2.8.0, Apache-2.0, approved, CQ23893
 maven/mavencentral/org.assertj/assertj-core/3.20.2, Apache-2.0, approved, clearlydefined
@@ -33,29 +35,29 @@
 maven/mavencentral/org.eclipse.jetty/jetty-server/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
 maven/mavencentral/org.eclipse.jetty/jetty-servlet/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
 maven/mavencentral/org.eclipse.jetty/jetty-util/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.agent/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.agent/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
 maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ11429
 maven/mavencentral/org.mockito/mockito-core/2.23.0, Apache-2.0 AND MIT, approved, #958
 maven/mavencentral/org.objenesis/objenesis/2.6, Apache-2.0, approved, CQ15478
diff --git a/WORKSPACE b/WORKSPACE
index 040617e..cce1316 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -68,8 +68,8 @@
 
 maven_jar(
     name = "jzlib",
-    artifact = "com.jcraft:jzlib:1.1.1",
-    sha1 = "a1551373315ffc2f96130a0e5704f74e151777ba",
+    artifact = "com.jcraft:jzlib:1.1.3",
+    sha1 = "c01428efa717624f7aabf4df319939dda9646b2d",
 )
 
 maven_jar(
diff --git a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
index 0997570..e5569e1 100644
--- a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
@@ -5,13 +5,13 @@
 Automatic-Module-Name: org.eclipse.jgit.ant.test
 Bundle-SymbolicName: org.eclipse.jgit.ant.test
 Bundle-Vendor: %Bundle-Vendor
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Import-Package: org.apache.tools.ant,
- org.eclipse.jgit.ant.tasks;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.ant.tasks;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)"
diff --git a/org.eclipse.jgit.ant.test/pom.xml b/org.eclipse.jgit.ant.test/pom.xml
index ba473ef..77060bc 100644
--- a/org.eclipse.jgit.ant.test/pom.xml
+++ b/org.eclipse.jgit.ant.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ant.test</artifactId>
diff --git a/org.eclipse.jgit.ant/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
index 6003a31..155bda4 100644
--- a/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
@@ -3,13 +3,13 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ant
 Bundle-SymbolicName: org.eclipse.jgit.ant
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Import-Package: org.apache.tools.ant,
-  org.eclipse.jgit.storage.file;version="[6.1.1,6.2.0)"
+  org.eclipse.jgit.storage.file;version="[6.2.1,6.3.0)"
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.ant;version="6.1.1",
- org.eclipse.jgit.ant.tasks;version="6.1.1";
+Export-Package: org.eclipse.jgit.ant;version="6.2.1",
+ org.eclipse.jgit.ant.tasks;version="6.2.1";
   uses:="org.apache.tools.ant,
    org.apache.tools.ant.types"
diff --git a/org.eclipse.jgit.ant/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ant/META-INF/SOURCE-MANIFEST.MF
index d0dc5c8..4e3785e 100644
--- a/org.eclipse.jgit.ant/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ant/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ant - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ant.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ant;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ant;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.ant/pom.xml b/org.eclipse.jgit.ant/pom.xml
index a7f258b..c16f983 100644
--- a/org.eclipse.jgit.ant/pom.xml
+++ b/org.eclipse.jgit.ant/pom.xml
@@ -15,7 +15,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ant</artifactId>
diff --git a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
index 3fc8df1..cd4b762 100644
--- a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.archive
 Bundle-SymbolicName: org.eclipse.jgit.archive
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-11
@@ -13,17 +13,17 @@
  org.apache.commons.compress.compressors.bzip2;version="[1.4,2.0)",
  org.apache.commons.compress.compressors.gzip;version="[1.4,2.0)",
  org.apache.commons.compress.compressors.xz;version="[1.4,2.0)",
- org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.osgi.framework;version="[1.3.0,2.0.0)"
 Bundle-ActivationPolicy: lazy
 Bundle-Activator: org.eclipse.jgit.archive.FormatActivator
-Export-Package: org.eclipse.jgit.archive;version="6.1.1";
+Export-Package: org.eclipse.jgit.archive;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.api,
    org.apache.commons.compress.archivers,
    org.osgi.framework",
- org.eclipse.jgit.archive.internal;version="6.1.1";x-internal:=true
+ org.eclipse.jgit.archive.internal;version="6.2.1";x-internal:=true
diff --git a/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
index 8c7fd07..000fce5 100644
--- a/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.archive - Sources
 Bundle-SymbolicName: org.eclipse.jgit.archive.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.archive;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.archive;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.archive/pom.xml b/org.eclipse.jgit.archive/pom.xml
index 12f3e68..3e3b6d2 100644
--- a/org.eclipse.jgit.archive/pom.xml
+++ b/org.eclipse.jgit.archive/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.archive</artifactId>
diff --git a/org.eclipse.jgit.benchmarks/pom.xml b/org.eclipse.jgit.benchmarks/pom.xml
index ac1d6bd..78e19e9 100644
--- a/org.eclipse.jgit.benchmarks/pom.xml
+++ b/org.eclipse.jgit.benchmarks/pom.xml
@@ -14,7 +14,7 @@
   <modelVersion>4.0.0</modelVersion>
 
   <groupId>org.eclipse.jgit</groupId>
-  <version>6.1.1-SNAPSHOT</version>
+  <version>6.2.1-SNAPSHOT</version>
   <artifactId>org.eclipse.jgit.benchmarks</artifactId>
   <packaging>jar</packaging>
 
diff --git a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java
new file mode 100644
index 0000000..52a881b
--- /dev/null
+++ b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2021, Luca Milanesio <luca.milanesio@gmail.com> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.benchmarks;
+
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+@State(Scope.Thread)
+public class GetRefsBenchmark {
+
+	ThreadLocalRandom branchIndex = ThreadLocalRandom.current();
+
+	@State(Scope.Benchmark)
+	public static class BenchmarkState {
+
+		@Param({ "true", "false" })
+		boolean useRefTable;
+
+		@Param({ "100", "2500", "10000", "50000" })
+		int numBranches;
+
+		@Param({ "true", "false" })
+		boolean trustFolderStat;
+
+		List<String> branches = new ArrayList<>(numBranches);
+
+		Path testDir;
+
+		Repository repo;
+
+		@Setup
+		@SuppressWarnings("boxing")
+		public void setupBenchmark() throws IOException, GitAPIException {
+			String firstBranch = "firstbranch";
+			testDir = Files.createDirectory(Paths.get("testrepos"));
+			String repoName = "branches-" + numBranches + "-trustFolderStat-"
+					+ trustFolderStat + "-" + refDatabaseType();
+			Path workDir = testDir.resolve(repoName);
+			Path repoPath = workDir.resolve(".git");
+			Git git = Git.init().setDirectory(workDir.toFile()).call();
+			RevCommit firstCommit = git.commit().setMessage("First commit")
+					.call();
+			git.branchCreate().setName(firstBranch).call();
+
+			StoredConfig cfg = git.getRepository().getConfig();
+			if (useRefTable) {
+				((FileRepository) git.getRepository()).convertRefStorage(
+						ConfigConstants.CONFIG_REF_STORAGE_REFTABLE, false,
+						false);
+			} else {
+				cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+						ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT,
+						trustFolderStat);
+			}
+			cfg.setInt(ConfigConstants.CONFIG_RECEIVE_SECTION, null,
+					"maxCommandBytes", Integer.MAX_VALUE);
+			cfg.save();
+
+			repo = RepositoryCache.open(RepositoryCache.FileKey
+					.lenient(repoPath.toFile(), FS.DETECTED));
+
+			System.out.println("Preparing test");
+			System.out.println("- repository: \t\t" + repoPath);
+			System.out.println("- refDatabase: \t\t" + refDatabaseType());
+			System.out.println("- trustFolderStat: \t" + trustFolderStat);
+			System.out.println("- branches: \t\t" + numBranches);
+
+			BatchRefUpdate u = repo.getRefDatabase().newBatchUpdate();
+
+			branches = IntStream.range(0, numBranches)
+					.mapToObj(i -> "branch/" + i % 100 + "/" + i)
+					.collect(Collectors.toList());
+			for (String branch : branches) {
+				u.addCommand(new ReceiveCommand(ObjectId.zeroId(),
+						firstCommit.toObjectId(), Constants.R_HEADS + branch,
+						CREATE));
+			}
+
+			System.out.println();
+			System.out.print(
+					String.format("Creating %d branches ... ", numBranches));
+
+			try (RevWalk rw = new RevWalk(repo)) {
+				u.execute(rw, new TextProgressMonitor());
+			}
+			System.out.println("DONE");
+		}
+
+		private String refDatabaseType() {
+			return useRefTable ? "reftable" : "refdir";
+		}
+
+		@TearDown
+		public void teardown() throws IOException {
+			repo.close();
+			FileUtils.delete(testDir.toFile(),
+					FileUtils.RECURSIVE | FileUtils.RETRY);
+		}
+	}
+
+	@Benchmark
+	@BenchmarkMode({ Mode.AverageTime })
+	@OutputTimeUnit(TimeUnit.MICROSECONDS)
+	@Warmup(iterations = 2, time = 100, timeUnit = TimeUnit.MILLISECONDS)
+	@Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
+	public void testGetExactRef(Blackhole blackhole, BenchmarkState state)
+			throws IOException {
+		String branchName = state.branches
+				.get(branchIndex.nextInt(state.numBranches));
+		blackhole.consume(state.repo.exactRef(branchName));
+	}
+
+	@Benchmark
+	@BenchmarkMode({ Mode.AverageTime })
+	@OutputTimeUnit(TimeUnit.MICROSECONDS)
+	@Warmup(iterations = 2, time = 100, timeUnit = TimeUnit.MILLISECONDS)
+	@Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
+	public void testGetRefsByPrefix(Blackhole blackhole, BenchmarkState state)
+			throws IOException {
+		String branchPrefix = "refs/heads/branch/" + branchIndex.nextInt(100)
+				+ "/";
+		blackhole.consume(
+				state.repo.getRefDatabase().getRefsByPrefix(branchPrefix));
+	}
+
+	public static void main(String[] args) throws RunnerException {
+		Options opt = new OptionsBuilder()
+				.include(GetRefsBenchmark.class.getSimpleName())
+				// .addProfiler(StackProfiler.class)
+				// .addProfiler(GCProfiler.class)
+				.forks(1).jvmArgs("-ea").build();
+		new Runner(opt).run();
+	}
+}
diff --git a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/LookupFileStoreBenchmark.java b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/LookupFileStoreBenchmark.java
index 393edcb..e9c9ef3 100644
--- a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/LookupFileStoreBenchmark.java
+++ b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/LookupFileStoreBenchmark.java
@@ -24,7 +24,6 @@
 import org.openjdk.jmh.annotations.Setup;
 import org.openjdk.jmh.annotations.State;
 import org.openjdk.jmh.annotations.TearDown;
-import org.openjdk.jmh.profile.StackProfiler;
 import org.openjdk.jmh.runner.Runner;
 import org.openjdk.jmh.runner.RunnerException;
 import org.openjdk.jmh.runner.options.Options;
diff --git a/org.eclipse.jgit.coverage/pom.xml b/org.eclipse.jgit.coverage/pom.xml
index abc37ec..70370a4 100644
--- a/org.eclipse.jgit.coverage/pom.xml
+++ b/org.eclipse.jgit.coverage/pom.xml
@@ -14,7 +14,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
@@ -27,88 +27,88 @@
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ant</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.archive</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.apache</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.server</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.server</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.pgm</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ui</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
 
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.test</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ant.test</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.test</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.pgm.test</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.test</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.server.test</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ssh.apache.test</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
   </dependencies>
 
diff --git a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
index bb9b393..383de17 100644
--- a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.gpg.bc.test
 Bundle-SymbolicName: org.eclipse.jgit.gpg.bc.test
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-11
@@ -12,9 +12,9 @@
  org.bouncycastle.openpgp.operator;version="[1.65.0,2.0.0)",
  org.bouncycastle.openpgp.operator.jcajce;version="[1.65.0,2.0.0)",
  org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)",
- org.eclipse.jgit.gpg.bc.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.gpg.bc.internal.keys;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util.sha1;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.gpg.bc.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.gpg.bc.internal.keys;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util.sha1;version="[6.2.1,6.3.0)",
  org.hamcrest;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.gpg.bc.test/pom.xml b/org.eclipse.jgit.gpg.bc.test/pom.xml
index 9568c68..653c832 100644
--- a/org.eclipse.jgit.gpg.bc.test/pom.xml
+++ b/org.eclipse.jgit.gpg.bc.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.gpg.bc.test</artifactId>
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
index 534568d..454195d 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
@@ -3,10 +3,10 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.gpg.bc
 Bundle-SymbolicName: org.eclipse.jgit.gpg.bc;singleton:=true
-Fragment-Host: org.eclipse.jgit;bundle-version="[6.1.1,6.2.0)"
+Fragment-Host: org.eclipse.jgit;bundle-version="[6.2.1,6.3.0)"
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Import-Package: org.bouncycastle.asn1;version="[1.69.0,2.0.0)",
  org.bouncycastle.asn1.cryptlib;version="[1.69.0,2.0.0)",
@@ -29,9 +29,9 @@
  org.bouncycastle.util;version="[1.69.0,2.0.0)",
  org.bouncycastle.util.encoders;version="[1.69.0,2.0.0)",
  org.bouncycastle.util.io;version="[1.69.0,2.0.0)",
- org.eclipse.jgit.annotations;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api.errors;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.annotations;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api.errors;version="[6.2.1,6.3.0)",
  org.slf4j;version="[1.7.0,2.0.0)"
-Export-Package: org.eclipse.jgit.gpg.bc;version="6.1.1",
- org.eclipse.jgit.gpg.bc.internal;version="6.1.1";x-friends:="org.eclipse.jgit.gpg.bc.test",
- org.eclipse.jgit.gpg.bc.internal.keys;version="6.1.1";x-friends:="org.eclipse.jgit.gpg.bc.test"
+Export-Package: org.eclipse.jgit.gpg.bc;version="6.2.1",
+ org.eclipse.jgit.gpg.bc.internal;version="6.2.1";x-friends:="org.eclipse.jgit.gpg.bc.test",
+ org.eclipse.jgit.gpg.bc.internal.keys;version="6.2.1";x-friends:="org.eclipse.jgit.gpg.bc.test"
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF
index 20e2d01..2d71a5e 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.gpg.bc - Sources
 Bundle-SymbolicName: org.eclipse.jgit.gpg.bc.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.gpg.bc;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.gpg.bc;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.gpg.bc/pom.xml b/org.eclipse.jgit.gpg.bc/pom.xml
index 138f4e8..8df5909 100644
--- a/org.eclipse.jgit.gpg.bc/pom.xml
+++ b/org.eclipse.jgit.gpg.bc/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.gpg.bc</artifactId>
diff --git a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
index afcaa78..bbde3f1 100644
--- a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.http.apache
 Bundle-SymbolicName: org.eclipse.jgit.http.apache
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
@@ -25,11 +25,11 @@
  org.apache.http.impl.conn;version="[4.4.0,5.0.0)",
  org.apache.http.params;version="[4.3.0,5.0.0)",
  org.apache.http.ssl;version="[4.3.0,5.0.0)",
- org.eclipse.jgit.annotations;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.http;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)"
-Export-Package: org.eclipse.jgit.transport.http.apache;version="6.1.1";
+ org.eclipse.jgit.annotations;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.http;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)"
+Export-Package: org.eclipse.jgit.transport.http.apache;version="6.2.1";
   uses:="org.apache.http.client,
    org.eclipse.jgit.transport.http,
    org.apache.http.entity,
diff --git a/org.eclipse.jgit.http.apache/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.http.apache/META-INF/SOURCE-MANIFEST.MF
index 7b8e149..fe928fb 100644
--- a/org.eclipse.jgit.http.apache/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.http.apache - Sources
 Bundle-SymbolicName: org.eclipse.jgit.http.apache.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.http.apache;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.http.apache;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.http.apache/pom.xml b/org.eclipse.jgit.http.apache/pom.xml
index bd35589..6182159 100644
--- a/org.eclipse.jgit.http.apache/pom.xml
+++ b/org.eclipse.jgit.http.apache/pom.xml
@@ -15,7 +15,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.apache</artifactId>
diff --git a/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
index 4bf66a2..2bdf386 100644
--- a/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
@@ -3,13 +3,13 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.http.server
 Bundle-SymbolicName: org.eclipse.jgit.http.server
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.http.server;version="6.1.1",
- org.eclipse.jgit.http.server.glue;version="6.1.1";
+Export-Package: org.eclipse.jgit.http.server;version="6.2.1",
+ org.eclipse.jgit.http.server.glue;version="6.2.1";
   uses:="javax.servlet,javax.servlet.http",
- org.eclipse.jgit.http.server.resolver;version="6.1.1";
+ org.eclipse.jgit.http.server.resolver;version="6.2.1";
   uses:="org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.transport,
@@ -18,14 +18,14 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Import-Package: javax.servlet;version="[2.5.0,5.0.0)",
  javax.servlet.http;version="[2.5.0,5.0.0)",
- org.eclipse.jgit.annotations;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.transport.parser;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.resolver;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)"
+ org.eclipse.jgit.annotations;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.transport.parser;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.resolver;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)"
diff --git a/org.eclipse.jgit.http.server/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.http.server/META-INF/SOURCE-MANIFEST.MF
index 2b47055..bdcba51 100644
--- a/org.eclipse.jgit.http.server/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.http.server/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.http.server - Sources
 Bundle-SymbolicName: org.eclipse.jgit.http.server.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.http.server;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.http.server;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.http.server/pom.xml b/org.eclipse.jgit.http.server/pom.xml
index 6a788e2..156d9f2 100644
--- a/org.eclipse.jgit.http.server/pom.xml
+++ b/org.eclipse.jgit.http.server/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.server</artifactId>
diff --git a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
index 4150bdb..04fd363 100644
--- a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.http.test
 Bundle-SymbolicName: org.eclipse.jgit.http.test
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-11
@@ -26,26 +26,26 @@
  org.eclipse.jetty.util.log;version="[10.0.0,11.0.0)",
  org.eclipse.jetty.util.security;version="[10.0.0,11.0.0)",
  org.eclipse.jetty.util.thread;version="[10.0.0,11.0.0)",
- org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.http.server;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.http.server.glue;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.http.server.resolver;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit.http;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.http;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.http.apache;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.resolver;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.http.server;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.http.server.glue;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.http.server.resolver;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit.http;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.http;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.http.apache;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.resolver;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.hamcrest;version="[1.1.0,3.0.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.http.test/pom.xml b/org.eclipse.jgit.http.test/pom.xml
index 336f43b..3d6aa3d 100644
--- a/org.eclipse.jgit.http.test/pom.xml
+++ b/org.eclipse.jgit.http.test/pom.xml
@@ -18,7 +18,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.test</artifactId>
diff --git a/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
index d035072..3a821d5 100644
--- a/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.junit.http
 Bundle-SymbolicName: org.eclipse.jgit.junit.http
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
@@ -21,17 +21,17 @@
  org.eclipse.jetty.util.log;version="[10.0.0,11.0.0)",
  org.eclipse.jetty.util.security;version="[10.0.0,11.0.0)",
  org.eclipse.jetty.util.ssl;version="[10.0.0,11.0.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.http.server;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.resolver;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.http.server;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.resolver;version="[6.2.1,6.3.0)",
  org.junit;version="[4.13,5.0.0)",
  org.slf4j.helpers;version="[1.7.0,2.0.0)"
-Export-Package: org.eclipse.jgit.junit.http;version="6.1.1";
+Export-Package: org.eclipse.jgit.junit.http;version="6.2.1";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.junit,
    javax.servlet.http,
diff --git a/org.eclipse.jgit.junit.http/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.junit.http/META-INF/SOURCE-MANIFEST.MF
index 19142c5..036ca8a 100644
--- a/org.eclipse.jgit.junit.http/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.junit.http/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.junit.http - Sources
 Bundle-SymbolicName: org.eclipse.jgit.junit.http.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit.http;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit.http;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit.http/pom.xml b/org.eclipse.jgit.junit.http/pom.xml
index d883e03..7a3db92 100644
--- a/org.eclipse.jgit.junit.http/pom.xml
+++ b/org.eclipse.jgit.junit.http/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.http</artifactId>
diff --git a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
index 60858d8..278ed7e 100644
--- a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.junit.ssh
 Bundle-SymbolicName: org.eclipse.jgit.junit.ssh
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
@@ -33,16 +33,16 @@
  org.apache.sshd.server.subsystem;version="[2.8.0,2.9.0)",
  org.apache.sshd.sftp;version="[2.8.0,2.9.0)",
  org.apache.sshd.sftp.server;version="[2.8.0,2.9.0)",
- org.eclipse.jgit.annotations;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.annotations;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
  org.slf4j;version="[1.7.0,2.0.0)"
-Export-Package: org.eclipse.jgit.junit.ssh;version="6.1.1"
+Export-Package: org.eclipse.jgit.junit.ssh;version="6.2.1"
diff --git a/org.eclipse.jgit.junit.ssh/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.junit.ssh/META-INF/SOURCE-MANIFEST.MF
index 41246bc..f77aafd 100644
--- a/org.eclipse.jgit.junit.ssh/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.junit.ssh/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.junit.ssh - Sources
 Bundle-SymbolicName: org.eclipse.jgit.junit.ssh.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit.ssh;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit.ssh;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit.ssh/pom.xml b/org.eclipse.jgit.junit.ssh/pom.xml
index 07d0218..cf1aaf8 100644
--- a/org.eclipse.jgit.junit.ssh/pom.xml
+++ b/org.eclipse.jgit.junit.ssh/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.ssh</artifactId>
diff --git a/org.eclipse.jgit.junit/.settings/.api_filters b/org.eclipse.jgit.junit/.settings/.api_filters
deleted file mode 100644
index a17a72f..0000000
--- a/org.eclipse.jgit.junit/.settings/.api_filters
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<component id="org.eclipse.jgit.junit" version="2">
-    <resource path="src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java" type="org.eclipse.jgit.junit.LocalDiskRepositoryTestCase">
-        <filter id="336658481">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.junit.LocalDiskRepositoryTestCase"/>
-                <message_argument value="currentTest"/>
-            </message_arguments>
-        </filter>
-        <filter id="1142947843">
-            <message_arguments>
-                <message_argument value="6.0.1"/>
-                <message_argument value="currentTest"/>
-            </message_arguments>
-        </filter>
-    </resource>
-</component>
diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
index 7a3b200..a3a2f34 100644
--- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
@@ -3,35 +3,35 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.junit
 Bundle-SymbolicName: org.eclipse.jgit.junit
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-11
-Import-Package: org.eclipse.jgit.annotations;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.dircache;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.pack;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.merge;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="6.1.1",
- org.eclipse.jgit.treewalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk.filter;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util.io;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util.time;version="[6.1.1,6.2.0)",
+Import-Package: org.eclipse.jgit.annotations;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.dircache;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.merge;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="6.2.1",
+ org.eclipse.jgit.treewalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk.filter;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util.io;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util.time;version="[6.2.1,6.3.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.rules;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)",
  org.junit.runners;version="[4.13,5.0.0)",
  org.junit.runners.model;version="[4.13,5.0.0)",
  org.slf4j;version="[1.7.0,2.0.0)"
-Export-Package: org.eclipse.jgit.junit;version="6.1.1";
+Export-Package: org.eclipse.jgit.junit;version="6.2.1";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
@@ -44,4 +44,4 @@
    org.junit.runners.model,
    org.junit.runner,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.junit.time;version="6.1.1";uses:="org.eclipse.jgit.util.time"
+ org.eclipse.jgit.junit.time;version="6.2.1";uses:="org.eclipse.jgit.util.time"
diff --git a/org.eclipse.jgit.junit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/SOURCE-MANIFEST.MF
index 25876c7..6997a51 100644
--- a/org.eclipse.jgit.junit/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.junit/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.junit - Sources
 Bundle-SymbolicName: org.eclipse.jgit.junit.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit/pom.xml b/org.eclipse.jgit.junit/pom.xml
index 2c260e2..a7e0831 100644
--- a/org.eclipse.jgit.junit/pom.xml
+++ b/org.eclipse.jgit.junit/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit</artifactId>
diff --git a/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
index cbb8135..6a9f172 100644
--- a/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.server.test
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server.test
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-11
@@ -26,24 +26,24 @@
  org.eclipse.jetty.util.log;version="[10.0.0,11.0.0)",
  org.eclipse.jetty.util.security;version="[10.0.0,11.0.0)",
  org.eclipse.jetty.util.thread;version="[10.0.0,11.0.0)",
- org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit.http;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.server;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.server.fs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.test;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk.filter;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit.http;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.server;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.test;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk.filter;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.rules;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.lfs.server.test/pom.xml b/org.eclipse.jgit.lfs.server.test/pom.xml
index 1bab620..e306c3a 100644
--- a/org.eclipse.jgit.lfs.server.test/pom.xml
+++ b/org.eclipse.jgit.lfs.server.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.server.test</artifactId>
diff --git a/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF
index cd8bde3..c63d2e4 100644
--- a/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF
@@ -3,19 +3,19 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.server
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.lfs.server;version="6.1.1";
+Export-Package: org.eclipse.jgit.lfs.server;version="6.2.1";
   uses:="javax.servlet.http,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.fs;version="6.1.1";
+ org.eclipse.jgit.lfs.server.fs;version="6.2.1";
   uses:="javax.servlet,
    javax.servlet.http,
    org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.internal;version="6.1.1";x-internal:=true,
- org.eclipse.jgit.lfs.server.s3;version="6.1.1";
+ org.eclipse.jgit.lfs.server.internal;version="6.2.1";x-internal:=true,
+ org.eclipse.jgit.lfs.server.s3;version="6.2.1";
   uses:="org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib"
 Bundle-RequiredExecutionEnvironment: JavaSE-11
@@ -24,15 +24,15 @@
  javax.servlet.annotation;version="[3.1.0,5.0.0)",
  javax.servlet.http;version="[3.1.0,5.0.0)",
  org.apache.http;version="[4.3.0,5.0.0)",
- org.eclipse.jgit.annotations;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.http;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.http.apache;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.annotations;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.http;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.http.apache;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.slf4j;version="[1.7.0,2.0.0)"
diff --git a/org.eclipse.jgit.lfs.server/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.lfs.server/META-INF/SOURCE-MANIFEST.MF
index 5a5b23a..3b783ca 100644
--- a/org.eclipse.jgit.lfs.server/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.lfs.server - Sources
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.lfs.server;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.lfs.server;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.lfs.server/pom.xml b/org.eclipse.jgit.lfs.server/pom.xml
index ae27153..0e5deb7 100644
--- a/org.eclipse.jgit.lfs.server/pom.xml
+++ b/org.eclipse.jgit.lfs.server/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.server</artifactId>
diff --git a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
index e3c0af8..80b8514 100644
--- a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
@@ -3,27 +3,27 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.test
 Bundle-SymbolicName: org.eclipse.jgit.lfs.test
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-11
-Import-Package: org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.attributes;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.http;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk.filter;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+Import-Package: org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.attributes;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.http;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk.filter;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)",
  org.junit.runners;version="[4.13,5.0.0)"
-Export-Package: org.eclipse.jgit.lfs.test;version="6.1.1";x-friends:="org.eclipse.jgit.lfs.server.test"
+Export-Package: org.eclipse.jgit.lfs.test;version="6.2.1";x-friends:="org.eclipse.jgit.lfs.server.test"
diff --git a/org.eclipse.jgit.lfs.test/pom.xml b/org.eclipse.jgit.lfs.test/pom.xml
index 1d782e6..357ada3 100644
--- a/org.eclipse.jgit.lfs.test/pom.xml
+++ b/org.eclipse.jgit.lfs.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.test</artifactId>
diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java
index 98a0712..3ac4157 100644
--- a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java
+++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java
@@ -142,14 +142,6 @@
 	File gitAttributesFile;
 
 	private void createLfsFiles(String lfsPointer) throws Exception {
-		/*
-		 * FileNames ".aaa.txt" and "zzz.txt" seem to be sufficient to get the
-		 * desired checkout order before and after ".lfsconfig", at least in a
-		 * number of manual tries. Since the files to checkout are contained in
-		 * a set (see DirCacheCheckout::doCheckout) the order cannot be
-		 * guaranteed.
-		 */
-
 		//File to be checked out before lfs config
 		String fileNameBefore = ".aaa.txt";
 		fileBefore = writeTrashFile(fileNameBefore, lfsPointer);
diff --git a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
index 09ffffb..bc4f4f4 100644
--- a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
@@ -3,33 +3,32 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs
 Bundle-SymbolicName: org.eclipse.jgit.lfs
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.lfs;version="6.1.1",
- org.eclipse.jgit.lfs.errors;version="6.1.1",
- org.eclipse.jgit.lfs.internal;version="6.1.1";x-friends:="org.eclipse.jgit.lfs.test,org.eclipse.jgit.lfs.server.fs,org.eclipse.jgit.lfs.server",
- org.eclipse.jgit.lfs.lib;version="6.1.1"
+Export-Package: org.eclipse.jgit.lfs;version="6.2.1",
+ org.eclipse.jgit.lfs.errors;version="6.2.1",
+ org.eclipse.jgit.lfs.internal;version="6.2.1";x-friends:="org.eclipse.jgit.lfs.test,org.eclipse.jgit.lfs.server.fs,org.eclipse.jgit.lfs.server",
+ org.eclipse.jgit.lfs.lib;version="6.2.1"
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Import-Package: com.google.gson;version="[2.8.2,3.0.0)",
  com.google.gson.stream;version="[2.8.2,3.0.0)",
- org.eclipse.jgit.annotations;version="[6.1.1,6.2.0)";resolution:=optional,
- org.eclipse.jgit.api.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.attributes;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.diff;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.dircache;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.hooks;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.pack;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.http;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk.filter;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util.io;version="[6.1.1,6.2.0)",
- org.slf4j;version="[1.7.0,2.0.0)"
+ org.eclipse.jgit.annotations;version="[6.2.1,6.3.0)";resolution:=optional,
+ org.eclipse.jgit.api.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.attributes;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.diff;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.dircache;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.hooks;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.pack;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.http;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk.filter;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util.io;version="[6.2.1,6.3.0)"
diff --git a/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF
index 7a2a446..5faa06f 100644
--- a/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.lfs - Sources
 Bundle-SymbolicName: org.eclipse.jgit.lfs.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.lfs;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.lfs;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.lfs/pom.xml b/org.eclipse.jgit.lfs/pom.xml
index 949c568..b458779 100644
--- a/org.eclipse.jgit.lfs/pom.xml
+++ b/org.eclipse.jgit.lfs/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs</artifactId>
diff --git a/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties b/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties
index 642b83d..c4c0dac 100644
--- a/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties
+++ b/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties
@@ -6,14 +6,13 @@
 invalidLongId=Invalid id: {0}
 invalidLongIdLength=Invalid id length {0}; should be {1}
 lfsFailedToGetRepository=failed to get repository {0}
-lfsNoDownloadUrl="Need to download object from LFS server but couldn't determine LFS server URL"
+lfsNoDownloadUrl=Need to download object from LFS server but couldn't determine LFS server URL
 lfsUnauthorized=Not authorized to perform operation {0} on repository {1}
 lfsUnavailable=LFS is not available for repository {0}
-missingLocalObject="Local Object {0} is missing"
+missingLocalObject=Local Object {0} is missing
 protocolError=LFS Protocol Error {0}: {1}
 repositoryNotFound=Repository {0} not found
 repositoryReadOnly=Repository {0} is read-only
 requiredHashFunctionNotAvailable=Required hash function {0} not available.
 serverFailure=When trying to open a connection to {0} the server responded with an error code. rc={1}
-userConfigInvalid="User config file {0} invalid {1}"
 wrongAmountOfDataReceived=While downloading data from the content server {0} {1} bytes have been received while {2} have been expected
\ No newline at end of file
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java
index ebf46e0..9b3d608 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com> and others
+ * Copyright (C) 2017, 2022 Markus Duft <markus.duft@ssi-schaefer.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
@@ -101,8 +101,10 @@
 		}
 		HttpConnection api = LfsConnectionFactory.getLfsConnection(
 				getRepository(), METHOD_POST, OPERATION_UPLOAD);
-		Map<String, LfsPointer> oid2ptr = requestBatchUpload(api, toPush);
-		uploadContents(api, oid2ptr);
+		if (!isDryRun()) {
+			Map<String, LfsPointer> oid2ptr = requestBatchUpload(api, toPush);
+			uploadContents(api, oid2ptr);
+		}
 		return EMPTY;
 
 	}
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java
index 71d395c..857ccbe 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java
@@ -30,21 +30,26 @@
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
 /**
- * Encapsulate access to the .lfsconfig.
+ * Encapsulate access to the {@code .lfsconfig}.
+ * <p>
+ * According to the git lfs documentation the order to find the
+ * {@code .lfsconfig} file is:
+ * </p>
+ * <ol>
+ * <li>in the root of the working tree</li>
+ * <li>in the index</li>
+ * <li>in the HEAD; for bare repositories this is the only place that is
+ * searched</li>
+ * </ol>
+ * <p>
+ * Values from the {@code .lfsconfig} are used only if not specified in another
+ * git config file to allow local override without modifiction of a committed
+ * file.
+ * </p>
  *
- * According to the document
- * https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-config.5.ronn
- * the order to find the .lfsconfig file is:
- *
- * <pre>
- *   1. in the root of the working tree
- *   2. in the index
- *   3. in the HEAD, for bare repositories this is the only place
- *      that is searched
- * </pre>
- *
- * Values from the .lfsconfig are used only if not specified in another git
- * config file to allow local override without modifiction of a committed file.
+ * @see <a href=
+ *      "https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-config.5.ronn">Configuration
+ *      options for git-lfs</a>
  */
 public class LfsConfig {
 	private Repository db;
@@ -55,17 +60,30 @@
 	 *
 	 * @param db
 	 *            the associated repo
+	 */
+	public LfsConfig(Repository db) {
+		this.db = db;
+	}
+
+	/**
+	 * Getter for the delegate to allow lazy initialization.
+	 *
+	 * @return the delegate {@link Config}
 	 * @throws IOException
 	 */
-	public LfsConfig(Repository db) throws IOException {
-		this.db = db;
-		delegate = this.load();
+	private Config getDelegate() throws IOException {
+		if (delegate == null) {
+			delegate = this.load();
+		}
+		return delegate;
 	}
 
 	/**
 	 * Read the .lfsconfig file from the repository
 	 *
-	 * @return The loaded lfs config or null if it does not exist
+	 * An empty config is returned be empty if no lfs config exists.
+	 *
+	 * @return The loaded lfs config
 	 *
 	 * @throws IOException
 	 */
@@ -102,7 +120,7 @@
 			throws IOException {
 		File lfsConfig = db.getFS().resolve(db.getWorkTree(),
 				Constants.DOT_LFS_CONFIG);
-		if (lfsConfig.exists() && lfsConfig.isFile()) {
+		if (lfsConfig.isFile()) {
 			FileBasedConfig config = new FileBasedConfig(lfsConfig, db.getFS());
 			try {
 				config.load();
@@ -188,12 +206,14 @@
 	 * @param name
 	 *            the key name
 	 * @return a String value from the config, <code>null</code> if not found
+	 * @throws IOException
 	 */
+	@Nullable
 	public String getString(final String section, final String subsection,
-			final String name) {
+			final String name) throws IOException {
 		String result = db.getConfig().getString(section, subsection, name);
 		if (result == null) {
-			result = delegate.getString(section, subsection, name);
+			result = getDelegate().getString(section, subsection, name);
 		}
 		return result;
 	}
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java
index 06234c1..8ef8f59 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java
@@ -44,6 +44,5 @@
 	/***/ public String repositoryReadOnly;
 	/***/ public String requiredHashFunctionNotAvailable;
 	/***/ public String serverFailure;
-	/***/ public String userConfigInvalid;
 	/***/ public String wrongAmountOfDataReceived;
 }
diff --git a/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..99f26c0
--- /dev/null
+++ b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
index cd22e24..c886145 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit"
       label="%featureName"
-      version="6.1.1.qualifier"
+      version="6.2.1.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
index 54feb9d..59c5eb9 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..99f26c0
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/feature.xml
index 2061948..b38c59a 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.gpg.bc"
       label="%featureName"
-      version="6.1.1.qualifier"
+      version="6.2.1.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="6.1.1" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="6.2.1" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/pom.xml
index 760903e..eaa0916 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
index e899ad5..01b978a 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.http.apache"
       label="%featureName"
-      version="6.1.1.qualifier"
+      version="6.2.1.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="6.1.1" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="6.2.1" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml
index b725dff..f6f137c 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
index 99073e4..4bf27d4 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.junit"
       label="%featureName"
-      version="6.1.1.qualifier"
+      version="6.2.1.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -24,7 +24,7 @@
 
    <requires>
       <import plugin="com.jcraft.jsch"/>
-      <import plugin="org.eclipse.jgit" version="6.1.1" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="6.2.1" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
index 3566dbb..d81b081 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
index c04051b..4e8385b 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.lfs"
       label="%featureName"
-      version="6.1.1.qualifier"
+      version="6.2.1.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="6.1.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="6.2.1" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml
index 50338a6..7b2b512 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
index 3939f51..38766d2 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.pgm"
       label="%featureName"
-      version="6.1.1.qualifier"
+      version="6.2.1.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -35,9 +35,9 @@
          version="0.0.0"/>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="6.1.1" match="equivalent"/>
-      <import feature="org.eclipse.jgit.lfs" version="6.1.1" match="equivalent"/>
-      <import feature="org.eclipse.jgit.ssh.apache" version="6.1.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="6.2.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit.lfs" version="6.2.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit.ssh.apache" version="6.2.1" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml
index 4b0e26a..f57a3c0 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml
index 9ccf39e..8e8d686 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.repository</artifactId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
index d3e662b..03fc43a 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.source"
       label="%featureName"
-      version="6.1.1.qualifier"
+      version="6.2.1.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="6.1.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="6.2.1" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
index d8b5de2..591d67a 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
@@ -30,7 +30,7 @@
     <dependency>
       <groupId>org.eclipse.jgit.feature</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
-      <version>6.1.1-SNAPSHOT</version>
+      <version>6.2.1-SNAPSHOT</version>
     </dependency>
   </dependencies>
 
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml
index e039647..40d5327 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.ssh.apache"
       label="%featureName"
-      version="6.1.1.qualifier"
+      version="6.2.1.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="6.1.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="6.2.1" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/pom.xml
index 591b448..8497f08 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.apache.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..99f26c0
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/feature.xml
index 3df4a92..0018abe 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.ssh.jsch"
       label="%featureName"
-      version="6.1.1.qualifier"
+      version="6.2.1.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="6.1.1" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="6.2.1" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/pom.xml
index e461bff..6ee46ba 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..99f26c0
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/META-INF/MANIFEST.MF b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/META-INF/MANIFEST.MF
index 5d61eef..91c5228 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/META-INF/MANIFEST.MF
@@ -2,4 +2,4 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: JGit Target Platform Bundle
 Bundle-SymbolicName: org.eclipse.jgit.target
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target
index 4e40232..3d4f237 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.17" sequenceNumber="1646256653">
+<target name="jgit-4.17" sequenceNumber="1654550635">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="jakarta.servlet-api" version="4.0.0"/>
@@ -27,8 +27,8 @@
       <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/>
       <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/>
       <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/>
-      <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/>
       <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/>
@@ -39,8 +39,8 @@
       <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/>
+      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/>
       <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/>
@@ -59,12 +59,12 @@
       <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/>
       <unit id="org.assertj" version="3.20.2.v20210706-1104"/>
       <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/>
-      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/>
       <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/>
@@ -87,7 +87,7 @@
       <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/>
       <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/>
       <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220302172233/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd
index dbb450a..9c824c5 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.17" with source configurePhase
 
 include "projects/jetty-10.0.x.tpd"
-include "orbit/R20220302172233-2022-03.tpd"
+include "orbit/R20220531185310-2022-06.tpd"
 
 location "https://download.eclipse.org/releases/2020-09/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target
index 1628217..603fefb 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.18" sequenceNumber="1646256653">
+<target name="jgit-4.18" sequenceNumber="1654550635">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="jakarta.servlet-api" version="4.0.0"/>
@@ -27,8 +27,8 @@
       <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/>
       <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/>
       <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/>
-      <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/>
       <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/>
@@ -39,8 +39,8 @@
       <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/>
+      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/>
       <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/>
@@ -59,12 +59,12 @@
       <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/>
       <unit id="org.assertj" version="3.20.2.v20210706-1104"/>
       <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/>
-      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/>
       <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/>
@@ -87,7 +87,7 @@
       <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/>
       <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/>
       <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220302172233/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd
index 911c67c..1dcdd9b 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.18" with source configurePhase
 
 include "projects/jetty-10.0.x.tpd"
-include "orbit/R20220302172233-2022-03.tpd"
+include "orbit/R20220531185310-2022-06.tpd"
 
 location "https://download.eclipse.org/releases/2020-12/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target
index ab18f7b..cc418b1 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.19-staging" sequenceNumber="1646256653">
+<target name="jgit-4.19-staging" sequenceNumber="1654550632">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="jakarta.servlet-api" version="4.0.0"/>
@@ -27,8 +27,8 @@
       <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/>
       <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/>
       <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/>
-      <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/>
       <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/>
@@ -39,8 +39,8 @@
       <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/>
+      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/>
       <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/>
@@ -59,12 +59,12 @@
       <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/>
       <unit id="org.assertj" version="3.20.2.v20210706-1104"/>
       <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/>
-      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/>
       <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/>
@@ -87,7 +87,7 @@
       <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/>
       <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/>
       <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220302172233/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd
index fdb8b11..806851c 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.19-staging" with source configurePhase
 
 include "projects/jetty-10.0.x.tpd"
-include "orbit/R20220302172233-2022-03.tpd"
+include "orbit/R20220531185310-2022-06.tpd"
 
 location "https://download.eclipse.org/staging/2021-03/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target
index 4c840dc..efcb591 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.20" sequenceNumber="1646256653">
+<target name="jgit-4.20" sequenceNumber="1654550634">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="jakarta.servlet-api" version="4.0.0"/>
@@ -27,8 +27,8 @@
       <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/>
       <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/>
       <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/>
-      <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/>
       <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/>
@@ -39,8 +39,8 @@
       <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/>
+      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/>
       <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/>
@@ -59,12 +59,12 @@
       <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/>
       <unit id="org.assertj" version="3.20.2.v20210706-1104"/>
       <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/>
-      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/>
       <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/>
@@ -87,7 +87,7 @@
       <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/>
       <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/>
       <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220302172233/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd
index 120ee64..a3ea583 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.20" with source configurePhase
 
 include "projects/jetty-10.0.x.tpd"
-include "orbit/R20220302172233-2022-03.tpd"
+include "orbit/R20220531185310-2022-06.tpd"
 
 location "https://download.eclipse.org/releases/2021-06/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target
index 7e8cd91..85ed31f 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.21" sequenceNumber="1646256653">
+<target name="jgit-4.21" sequenceNumber="1654550635">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="jakarta.servlet-api" version="4.0.0"/>
@@ -27,8 +27,8 @@
       <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/>
       <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/>
       <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/>
-      <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/>
       <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/>
@@ -39,8 +39,8 @@
       <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/>
+      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/>
       <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/>
@@ -59,12 +59,12 @@
       <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/>
       <unit id="org.assertj" version="3.20.2.v20210706-1104"/>
       <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/>
-      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/>
       <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/>
@@ -87,7 +87,7 @@
       <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/>
       <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/>
       <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220302172233/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd
index 0ec2a52..0808601 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.21" with source configurePhase
 
 include "projects/jetty-10.0.x.tpd"
-include "orbit/R20220302172233-2022-03.tpd"
+include "orbit/R20220531185310-2022-06.tpd"
 
 location "https://download.eclipse.org/releases/2021-09/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target
index b229da1..e7b5a31 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.22" sequenceNumber="1646256653">
+<target name="jgit-4.22" sequenceNumber="1654550634">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="jakarta.servlet-api" version="4.0.0"/>
@@ -27,8 +27,8 @@
       <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/>
       <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/>
       <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/>
-      <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/>
       <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/>
       <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/>
@@ -39,8 +39,8 @@
       <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/>
       <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/>
+      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/>
       <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/>
       <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/>
@@ -59,12 +59,12 @@
       <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/>
       <unit id="org.assertj" version="3.20.2.v20210706-1104"/>
       <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/>
-      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220105-1522"/>
-      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/>
       <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/>
       <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/>
       <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/>
@@ -87,7 +87,7 @@
       <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/>
       <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/>
       <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220302172233/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd
index eb1723c..5697574 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.22" with source configurePhase
 
 include "projects/jetty-10.0.x.tpd"
-include "orbit/R20220302172233-2022-03.tpd"
+include "orbit/R20220531185310-2022-06.tpd"
 
 location "https://download.eclipse.org/releases/2021-12/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target
new file mode 100644
index 0000000..3c18e4b
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?pde?>
+<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
+<target name="jgit-4.23" sequenceNumber="1654550634">
+  <locations>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="jakarta.servlet-api" version="4.0.0"/>
+      <unit id="jakarta.servlet-api.source" version="4.0.0"/>
+      <unit id="org.eclipse.jetty.http" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.http.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.io" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.io.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.security" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.security.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.server" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.server.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.servlet" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.util" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.util.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="10.0.6"/>
+      <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/>
+    </location>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="com.google.gson" version="2.8.9.v20220111-1409"/>
+      <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/>
+      <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/>
+      <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/>
+      <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/>
+      <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/>
+      <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/>
+      <unit id="com.sun.jna.platform.source" version="5.8.0.v20210406-1004"/>
+      <unit id="javaewah" version="1.1.13.v20211029-0839"/>
+      <unit id="javaewah.source" version="1.1.13.v20211029-0839"/>
+      <unit id="net.bytebuddy.byte-buddy" version="1.9.0.v20181107-1410"/>
+      <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/>
+      <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/>
+      <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/>
+      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/>
+      <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/>
+      <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/>
+      <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/>
+      <unit id="org.apache.commons.codec.source" version="1.14.0.v20200818-1422"/>
+      <unit id="org.apache.commons.compress" version="1.21.0.v20211103-2100"/>
+      <unit id="org.apache.commons.compress.source" version="1.21.0.v20211103-2100"/>
+      <unit id="org.apache.commons.logging" version="1.2.0.v20180409-1502"/>
+      <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/>
+      <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/>
+      <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/>
+      <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/>
+      <unit id="org.assertj" version="3.20.2.v20210706-1104"/>
+      <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/>
+      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/>
+      <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/>
+      <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/>
+      <unit id="org.hamcrest.core.source" version="1.3.0.v20180420-1519"/>
+      <unit id="org.hamcrest.library" version="1.3.0.v20180524-2246"/>
+      <unit id="org.hamcrest.library.source" version="1.3.0.v20180524-2246"/>
+      <unit id="org.junit" version="4.13.2.v20211018-1956"/>
+      <unit id="org.junit.source" version="4.13.2.v20211018-1956"/>
+      <unit id="org.kohsuke.args4j" version="2.33.0.v20160323-2218"/>
+      <unit id="org.kohsuke.args4j.source" version="2.33.0.v20160323-2218"/>
+      <unit id="org.mockito" version="2.23.0.v20200310-1642"/>
+      <unit id="org.mockito.source" version="2.23.0.v20200310-1642"/>
+      <unit id="org.objenesis" version="2.6.0.v20180420-1519"/>
+      <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/>
+      <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/>
+      <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/>
+      <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/>
+      <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/>
+      <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/>
+      <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/>
+    </location>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="org.eclipse.osgi" version="0.0.0"/>
+      <repository location="https://download.eclipse.org/releases/2022-03/"/>
+    </location>
+  </locations>
+</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd
new file mode 100644
index 0000000..7fd421a
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd
@@ -0,0 +1,8 @@
+target "jgit-4.23" with source configurePhase
+
+include "projects/jetty-10.0.x.tpd"
+include "orbit/R20220531185310-2022-06.tpd"
+
+location "https://download.eclipse.org/releases/2022-03/" {
+	org.eclipse.osgi lazy
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target
new file mode 100644
index 0000000..b622b4e
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?pde?>
+<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
+<target name="jgit-4.24" sequenceNumber="1654550621">
+  <locations>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="jakarta.servlet-api" version="4.0.0"/>
+      <unit id="jakarta.servlet-api.source" version="4.0.0"/>
+      <unit id="org.eclipse.jetty.http" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.http.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.io" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.io.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.security" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.security.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.server" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.server.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.servlet" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.util" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.util.source" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="10.0.6"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="10.0.6"/>
+      <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/>
+    </location>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="com.google.gson" version="2.8.9.v20220111-1409"/>
+      <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/>
+      <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/>
+      <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/>
+      <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/>
+      <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/>
+      <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/>
+      <unit id="com.sun.jna.platform.source" version="5.8.0.v20210406-1004"/>
+      <unit id="javaewah" version="1.1.13.v20211029-0839"/>
+      <unit id="javaewah.source" version="1.1.13.v20211029-0839"/>
+      <unit id="net.bytebuddy.byte-buddy" version="1.9.0.v20181107-1410"/>
+      <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/>
+      <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/>
+      <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/>
+      <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/>
+      <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/>
+      <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/>
+      <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/>
+      <unit id="org.apache.commons.codec.source" version="1.14.0.v20200818-1422"/>
+      <unit id="org.apache.commons.compress" version="1.21.0.v20211103-2100"/>
+      <unit id="org.apache.commons.compress.source" version="1.21.0.v20211103-2100"/>
+      <unit id="org.apache.commons.logging" version="1.2.0.v20180409-1502"/>
+      <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/>
+      <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/>
+      <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/>
+      <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/>
+      <unit id="org.assertj" version="3.20.2.v20210706-1104"/>
+      <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/>
+      <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/>
+      <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/>
+      <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/>
+      <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/>
+      <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/>
+      <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/>
+      <unit id="org.hamcrest.core.source" version="1.3.0.v20180420-1519"/>
+      <unit id="org.hamcrest.library" version="1.3.0.v20180524-2246"/>
+      <unit id="org.hamcrest.library.source" version="1.3.0.v20180524-2246"/>
+      <unit id="org.junit" version="4.13.2.v20211018-1956"/>
+      <unit id="org.junit.source" version="4.13.2.v20211018-1956"/>
+      <unit id="org.kohsuke.args4j" version="2.33.0.v20160323-2218"/>
+      <unit id="org.kohsuke.args4j.source" version="2.33.0.v20160323-2218"/>
+      <unit id="org.mockito" version="2.23.0.v20200310-1642"/>
+      <unit id="org.mockito.source" version="2.23.0.v20200310-1642"/>
+      <unit id="org.objenesis" version="2.6.0.v20180420-1519"/>
+      <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/>
+      <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/>
+      <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/>
+      <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/>
+      <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/>
+      <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/>
+      <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/>
+    </location>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="org.eclipse.osgi" version="0.0.0"/>
+      <repository location="https://download.eclipse.org/staging/2022-06/"/>
+    </location>
+  </locations>
+</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd
new file mode 100644
index 0000000..e81eec9
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd
@@ -0,0 +1,8 @@
+target "jgit-4.24" with source configurePhase
+
+include "projects/jetty-10.0.x.tpd"
+include "orbit/R20220531185310-2022-06.tpd"
+
+location "https://download.eclipse.org/staging/2022-06/" {
+	org.eclipse.osgi lazy
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd
new file mode 100644
index 0000000..3c74497
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd
@@ -0,0 +1,69 @@
+target "R20220531185310-2022-06" with source configurePhase
+// see https://download.eclipse.org/tools/orbit/downloads/
+
+location "https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository" {
+	com.google.gson [2.8.9.v20220111-1409,2.8.9.v20220111-1409]
+	com.google.gson.source [2.8.9.v20220111-1409,2.8.9.v20220111-1409]
+	com.jcraft.jsch [0.1.55.v20190404-1902,0.1.55.v20190404-1902]
+	com.jcraft.jsch.source [0.1.55.v20190404-1902,0.1.55.v20190404-1902]
+	com.jcraft.jzlib [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
+	com.jcraft.jzlib.source [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
+	com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
+	com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
+	com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
+	com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
+	javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
+	javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
+	net.bytebuddy.byte-buddy [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
+	net.bytebuddy.byte-buddy-agent [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
+	net.bytebuddy.byte-buddy-agent.source [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
+	net.bytebuddy.byte-buddy.source [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
+	net.i2p.crypto.eddsa [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
+	net.i2p.crypto.eddsa.source [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
+	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
+	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
+	org.apache.commons.codec [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
+	org.apache.commons.codec.source [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
+	org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
+	org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
+	org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
+	org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
+	org.apache.httpcomponents.httpclient [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
+	org.apache.httpcomponents.httpclient.source [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
+	org.apache.httpcomponents.httpcore [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
+	org.apache.httpcomponents.httpcore.source [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
+	org.apache.sshd.osgi [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
+	org.apache.sshd.osgi.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
+	org.apache.sshd.sftp [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
+	org.apache.sshd.sftp.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
+	org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
+	org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
+	org.bouncycastle.bcpg [1.70.0.v20220507-1208,1.70.0.v20220507-1208]
+	org.bouncycastle.bcpg.source [1.70.0.v20220507-1208,1.70.0.v20220507-1208]
+	org.bouncycastle.bcpkix [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
+	org.bouncycastle.bcpkix.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
+	org.bouncycastle.bcprov [1.70.0.v20220507-1208,1.70.0.v20220507-1208]
+	org.bouncycastle.bcprov.source [1.70.0.v20220507-1208,1.70.0.v20220507-1208]
+	org.bouncycastle.bcutil [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
+	org.bouncycastle.bcutil.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
+	org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
+	org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
+	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
+	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
+	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
+	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
+	org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
+	org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
+	org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
+	org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
+	org.mockito [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
+	org.mockito.source [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
+	org.objenesis [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
+	org.objenesis.source [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
+	org.slf4j.api [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
+	org.slf4j.api.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
+	org.slf4j.binding.simple [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
+	org.slf4j.binding.simple.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
+	org.tukaani.xz [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
+	org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml
index 2a95042..8152b85 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml
@@ -16,37 +16,10 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.target</artifactId>
   <packaging>pom</packaging>
   <name>JGit Target Platform</name>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>build-helper-maven-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>attach-artifacts</id>
-            <phase>package</phase>
-            <goals>
-              <goal>attach-artifact</goal>
-            </goals>
-            <configuration>
-              <artifacts>
-                <artifact>
-                  <file>${target-platform}.target</file>
-                  <type>target</type>
-                  <classifier>${target-platform}</classifier>
-                </artifact>
-              </artifacts>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
 </project>
\ No newline at end of file
diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml
index b650598..812916e 100644
--- a/org.eclipse.jgit.packaging/pom.xml
+++ b/org.eclipse.jgit.packaging/pom.xml
@@ -16,14 +16,14 @@
 
   <groupId>org.eclipse.jgit</groupId>
   <artifactId>jgit.tycho.parent</artifactId>
-  <version>6.1.1-SNAPSHOT</version>
+  <version>6.2.1-SNAPSHOT</version>
   <packaging>pom</packaging>
 
   <name>JGit Tycho Parent</name>
 
   <properties>
     <java.version>11</java.version>
-    <tycho-version>2.5.0</tycho-version>
+    <tycho-version>2.6.0</tycho-version>
     <tycho-extras-version>${tycho-version}</tycho-extras-version>
     <target-platform>jgit-4.17</target-platform>
   </properties>
@@ -231,12 +231,7 @@
             <resolver>p2</resolver>
             <pomDependencies>consider</pomDependencies>
             <target>
-              <artifact>
-                <groupId>org.eclipse.jgit</groupId>
-                <artifactId>org.eclipse.jgit.target</artifactId>
-                <version>${project.version}</version>
-                <classifier>${target-platform}</classifier>
-              </artifact>
+              <file>${project.basedir}/../org.eclipse.jgit.target/${target-platform}.target</file>
             </target>
             <environments>
               <environment>
diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
index f5c4de4..dade9a8 100644
--- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
@@ -3,31 +3,31 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.pgm.test
 Bundle-SymbolicName: org.eclipse.jgit.pgm.test
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-11
-Import-Package: org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.diff;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.dircache;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.diffmergetool;version="6.1.1",
- org.eclipse.jgit.internal.storage.file;version="6.1.1",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.merge;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.pgm;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.pgm.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.pgm.opt;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util.io;version="[6.1.1,6.2.0)",
- org.hamcrest.core;bundle-version="[2.2.0,3.0.0)",
+Import-Package: org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.diff;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.dircache;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="6.2.1",
+ org.eclipse.jgit.internal.storage.file;version="6.2.1",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.merge;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.pgm;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.pgm.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.pgm.opt;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util.io;version="[6.2.1,6.3.0)",
+ org.hamcrest.core;bundle-version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.rules;version="[4.13,5.0.0)",
  org.kohsuke.args4j;version="[2.33.0,3.0.0)"
diff --git a/org.eclipse.jgit.pgm.test/pom.xml b/org.eclipse.jgit.pgm.test/pom.xml
index c824788..6748220 100644
--- a/org.eclipse.jgit.pgm.test/pom.xml
+++ b/org.eclipse.jgit.pgm.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm.test</artifactId>
diff --git a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/ToolTestCase.java b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/ToolTestCase.java
new file mode 100644
index 0000000..a3c41f0
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/ToolTestCase.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.pgm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.pgm.opt.CmdLineParser;
+import org.eclipse.jgit.pgm.opt.SubcommandHandler;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Before;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+
+/**
+ * Base test case for the {@code difftool} and {@code mergetool} commands.
+ */
+public abstract class ToolTestCase extends CLIRepositoryTestCase {
+
+	public static class GitCliJGitWrapperParser {
+		@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
+		TextBuiltin subcommand;
+
+		@Argument(index = 1, metaVar = "metaVar_arg")
+		List<String> arguments = new ArrayList<>();
+	}
+
+	protected static final String TOOL_NAME = "some_tool";
+
+	private static final String TEST_BRANCH_NAME = "test_branch";
+
+	private Git git;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		git = new Git(db);
+		git.commit().setMessage("initial commit").call();
+		git.branchCreate().setName(TEST_BRANCH_NAME).call();
+	}
+
+	protected String[] runAndCaptureUsingInitRaw(String... args)
+			throws Exception {
+		InputStream inputStream = null; // no input stream
+		return runAndCaptureUsingInitRaw(inputStream, args);
+	}
+
+	protected String[] runAndCaptureUsingInitRaw(
+			List<String> expectedErrorOutput, String... args) throws Exception {
+		InputStream inputStream = null; // no input stream
+		return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput,
+				args);
+	}
+
+	protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
+			String... args) throws Exception {
+		List<String> expectedErrorOutput = Collections.emptyList();
+		return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput,
+				args);
+	}
+
+	protected String[] runAndCaptureUsingInitRaw(InputStream inputStream,
+			List<String> expectedErrorOutput, String... args)
+			throws CmdLineException, Exception, IOException {
+		CLIGitCommand.Result result = new CLIGitCommand.Result();
+
+		GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
+		CmdLineParser clp = new CmdLineParser(bean);
+		clp.parseArgument(args);
+
+		TextBuiltin cmd = bean.subcommand;
+		cmd.initRaw(db, null, inputStream, result.out, result.err);
+		cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
+		if (cmd.getOutputWriter() != null) {
+			cmd.getOutputWriter().flush();
+		}
+		if (cmd.getErrorWriter() != null) {
+			cmd.getErrorWriter().flush();
+		}
+
+		List<String> errLines = result.errLines().stream()
+				.filter(l -> !l.isBlank()) // we care only about error messages
+				.collect(Collectors.toList());
+		assertEquals("Expected no standard error output from tool",
+				expectedErrorOutput.toString(), errLines.toString());
+
+		return result.outLines().toArray(new String[0]);
+	}
+
+	protected String[] createMergeConflict() throws Exception {
+		// create files on initial branch
+		git.checkout().setName(TEST_BRANCH_NAME).call();
+		writeTrashFile("dir1/a", "Hello world a");
+		writeTrashFile("dir2/b", "Hello world b");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("files a & b added").call();
+		// create another branch and change files
+		git.branchCreate().setName("branch_1").call();
+		git.checkout().setName("branch_1").call();
+		writeTrashFile("dir1/a", "Hello world a 1");
+		writeTrashFile("dir2/b", "Hello world b 1");
+		git.add().addFilepattern(".").call();
+		RevCommit commit1 = git.commit()
+				.setMessage("files a & b modified commit 1").call();
+		// checkout initial branch
+		git.checkout().setName(TEST_BRANCH_NAME).call();
+		// create another branch and change files
+		git.branchCreate().setName("branch_2").call();
+		git.checkout().setName("branch_2").call();
+		writeTrashFile("dir1/a", "Hello world a 2");
+		writeTrashFile("dir2/b", "Hello world b 2");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("files a & b modified commit 2").call();
+		// cherry-pick conflicting changes
+		git.cherryPick().include(commit1).call();
+		String[] conflictingFilenames = { "dir1/a", "dir2/b" };
+		return conflictingFilenames;
+	}
+
+	protected String[] createDeletedConflict() throws Exception {
+		// create files on initial branch
+		git.checkout().setName(TEST_BRANCH_NAME).call();
+		writeTrashFile("dir1/a", "Hello world a");
+		writeTrashFile("dir2/b", "Hello world b");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("files a & b added").call();
+		// create another branch and change files
+		git.branchCreate().setName("branch_1").call();
+		git.checkout().setName("branch_1").call();
+		writeTrashFile("dir1/a", "Hello world a 1");
+		writeTrashFile("dir2/b", "Hello world b 1");
+		git.add().addFilepattern(".").call();
+		RevCommit commit1 = git.commit()
+				.setMessage("files a & b modified commit 1").call();
+		// checkout initial branch
+		git.checkout().setName(TEST_BRANCH_NAME).call();
+		// create another branch and change files
+		git.branchCreate().setName("branch_2").call();
+		git.checkout().setName("branch_2").call();
+		git.rm().addFilepattern("dir1/a").call();
+		git.rm().addFilepattern("dir2/b").call();
+		git.commit().setMessage("files a & b deleted commit 2").call();
+		// cherry-pick conflicting changes
+		git.cherryPick().include(commit1).call();
+		String[] conflictingFilenames = { "dir1/a", "dir2/b" };
+		return conflictingFilenames;
+	}
+
+	protected String[] createUnstagedChanges() throws Exception {
+		writeTrashFile("dir1/a", "Hello world a");
+		writeTrashFile("dir2/b", "Hello world b");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("files a & b").call();
+		writeTrashFile("dir1/a", "New Hello world a");
+		writeTrashFile("dir2/b", "New Hello world b");
+		String[] conflictingFilenames = { "dir1/a", "dir2/b" };
+		return conflictingFilenames;
+	}
+
+	protected String[] createStagedChanges() throws Exception {
+		String[] conflictingFilenames = createUnstagedChanges();
+		git.add().addFilepattern(".").call();
+		return conflictingFilenames;
+	}
+
+	protected List<DiffEntry> getRepositoryChanges(RevCommit commit)
+			throws Exception {
+		TreeWalk tw = new TreeWalk(db);
+		tw.addTree(commit.getTree());
+		FileTreeIterator modifiedTree = new FileTreeIterator(db);
+		tw.addTree(modifiedTree);
+		List<DiffEntry> changes = DiffEntry.scan(tw);
+		return changes;
+	}
+
+	protected Path getFullPath(String repositoryFilename) {
+		Path dotGitPath = db.getDirectory().toPath();
+		Path repositoryRoot = dotGitPath.getParent();
+		Path repositoryFilePath = repositoryRoot.resolve(repositoryFilename);
+		return repositoryFilePath;
+	}
+
+	protected static InputStream createInputStream(String[] inputLines) {
+		return createInputStream(Arrays.asList(inputLines));
+	}
+
+	protected static InputStream createInputStream(List<String> inputLines) {
+		String input = String.join(System.lineSeparator(), inputLines);
+		InputStream inputStream = new ByteArrayInputStream(input.getBytes());
+		return inputStream;
+	}
+
+	protected static void assertArrayOfLinesEquals(String failMessage,
+			String[] expected, String[] actual) {
+		assertEquals(failMessage, toString(expected), toString(actual));
+	}
+
+	protected static void assertArrayOfMatchingLines(String failMessage,
+			Pattern[] expected, String[] actual) {
+		assertEquals(failMessage + System.lineSeparator()
+				+ "Expected and actual lines count don't match. Expected: "
+				+ Arrays.asList(expected) + ", actual: "
+				+ Arrays.asList(actual), expected.length, actual.length);
+		int n = expected.length;
+		for (int i = 0; i < n; ++i) {
+			Pattern expectedPattern = expected[i];
+			String actualLine = actual[i];
+			Matcher matcher = expectedPattern.matcher(actualLine);
+			boolean matches = matcher.matches();
+			assertTrue(failMessage + System.lineSeparator() + "Line " + i + " '"
+					+ actualLine + "' doesn't match expected pattern: "
+					+ expectedPattern + System.lineSeparator() + "Expected: "
+					+ Arrays.asList(expected) + ", actual: "
+					+ Arrays.asList(actual),
+					matches);
+		}
+	}
+}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
index e7bf484..2b50d45 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
+ * Copyright (C) 2021-2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -9,72 +9,145 @@
  */
 package org.eclipse.jgit.pgm;
 
-import static org.junit.Assert.assertEquals;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
+import static org.junit.Assert.fail;
 
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
 
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool;
-import org.eclipse.jgit.lib.CLIRepositoryTestCase;
-import org.eclipse.jgit.pgm.opt.CmdLineParser;
-import org.eclipse.jgit.pgm.opt.SubcommandHandler;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.treewalk.FileTreeIterator;
-import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.internal.diffmergetool.DiffTools;
+import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.junit.Before;
 import org.junit.Test;
-import org.kohsuke.args4j.Argument;
 
 /**
  * Testing the {@code difftool} command.
  */
-public class DiffToolTest extends CLIRepositoryTestCase {
-	public static class GitCliJGitWrapperParser {
-		@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
-		TextBuiltin subcommand;
+public class DiffToolTest extends ToolTestCase {
 
-		@Argument(index = 1, metaVar = "metaVar_arg")
-		List<String> arguments = new ArrayList<>();
-	}
-
-	private String[] runAndCaptureUsingInitRaw(String... args)
-			throws Exception {
-		CLIGitCommand.Result result = new CLIGitCommand.Result();
-
-		GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser();
-		CmdLineParser clp = new CmdLineParser(bean);
-		clp.parseArgument(args);
-
-		TextBuiltin cmd = bean.subcommand;
-		cmd.initRaw(db, null, null, result.out, result.err);
-		cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()]));
-		if (cmd.getOutputWriter() != null) {
-			cmd.getOutputWriter().flush();
-		}
-		if (cmd.getErrorWriter() != null) {
-			cmd.getErrorWriter().flush();
-		}
-		return result.outLines().toArray(new String[0]);
-	}
-
-	private Git git;
+	private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION;
 
 	@Override
 	@Before
 	public void setUp() throws Exception {
 		super.setUp();
-		git = new Git(db);
-		git.commit().setMessage("initial commit").call();
+		configureEchoTool(TOOL_NAME);
+	}
+
+	@Test(expected = Die.class)
+	public void testUndefinedTool() throws Exception {
+		String toolName = "undefined";
+		String[] conflictingFilenames = createUnstagedChanges();
+
+		List<String> expectedErrors = new ArrayList<>();
+		for (String changedFilename : conflictingFilenames) {
+			expectedErrors.add("External diff tool is not defined: " + toolName);
+			expectedErrors.add("compare of " + changedFilename + " failed");
+		}
+
+		runAndCaptureUsingInitRaw(expectedErrors, DIFF_TOOL, "--no-prompt",
+				"--tool", toolName);
+		fail("Expected exception to be thrown due to undefined external tool");
+	}
+
+	@Test(expected = Die.class)
+	public void testUserToolWithCommandNotFoundError() throws Exception {
+		String toolName = "customTool";
+
+		int errorReturnCode = 127; // command not found
+		String command = "exit " + errorReturnCode;
+
+		StoredConfig config = db.getConfig();
+		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+
+		createMergeConflict();
+		runAndCaptureUsingInitRaw(DIFF_TOOL, "--no-prompt", "--tool", toolName);
+
+		fail("Expected exception to be thrown due to external tool exiting with error code: "
+				+ errorReturnCode);
+	}
+
+	@Test(expected = Die.class)
+	public void testEmptyToolName() throws Exception {
+		String emptyToolName = "";
+
+		StoredConfig config = db.getConfig();
+		// the default diff tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
+				emptyToolName);
+
+		createUnstagedChanges();
+
+		String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
+		String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
+		runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput), DIFF_TOOL,
+				"--no-prompt");
+		fail("Expected exception to be thrown due to external tool exiting with an error");
+	}
+
+	@Test
+	public void testToolWithPrompt() throws Exception {
+		String[] inputLines = {
+				"y", // accept launching diff tool
+				"y", // accept launching diff tool
+		};
+
+		String[] conflictingFilenames = createUnstagedChanges();
+		String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						DIFF_TOOL, "--prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testToolAbortLaunch() throws Exception {
+		String[] inputLines = {
+				"y", // accept launching diff tool
+				"n", // don't launch diff tool
+		};
+
+		String[] conflictingFilenames = createUnstagedChanges();
+		int abortIndex = 1;
+		String[] expectedOutput = getExpectedAbortOutput(conflictingFilenames, abortIndex);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput,
+				runAndCaptureUsingInitRaw(inputStream, DIFF_TOOL, "--prompt", option,
+						TOOL_NAME));
+	}
+
+	@Test(expected = Die.class)
+	public void testNotDefinedTool() throws Exception {
+		createUnstagedChanges();
+
+		runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined");
+		fail("Expected exception when trying to run undefined tool");
 	}
 
 	@Test
 	public void testTool() throws Exception {
-		RevCommit commit = createUnstagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedDiffToolOutput(changes);
+		String[] conflictFilenames = createUnstagedChanges();
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames);
 
 		String[] options = {
 				"--tool",
@@ -84,69 +157,88 @@
 		for (String option : options) {
 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
 					expectedOutput,
-					runAndCaptureUsingInitRaw("difftool", option,
-							"some_tool"));
+					runAndCaptureUsingInitRaw(DIFF_TOOL, option,
+							TOOL_NAME));
 		}
 	}
 
 	@Test
 	public void testToolTrustExitCode() throws Exception {
-		RevCommit commit = createUnstagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedDiffToolOutput(changes);
+		String[] conflictingFilenames = createUnstagedChanges();
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
 
 		String[] options = { "--tool", "-t", };
 
 		for (String option : options) {
 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
-					expectedOutput, runAndCaptureUsingInitRaw("difftool",
-							"--trust-exit-code", option, "some_tool"));
+					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
+							"--trust-exit-code", option, TOOL_NAME));
 		}
 	}
 
 	@Test
 	public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception {
-		RevCommit commit = createUnstagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedDiffToolOutput(changes);
+		String[] conflictingFilenames = createUnstagedChanges();
+		String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames);
 
 		String[] options = { "--tool", "-t", };
 
 		for (String option : options) {
 			assertArrayOfLinesEquals("Incorrect output for option: " + option,
-					expectedOutput, runAndCaptureUsingInitRaw("difftool",
+					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
 							"--no-gui", "--no-prompt", "--no-trust-exit-code",
-							option, "some_tool"));
+							option, TOOL_NAME));
 		}
 	}
 
 	@Test
 	public void testToolCached() throws Exception {
-		RevCommit commit = createStagedChanges();
-		List<DiffEntry> changes = getRepositoryChanges(commit);
-		String[] expectedOutput = getExpectedDiffToolOutput(changes);
+		String[] conflictingFilenames = createStagedChanges();
+		Pattern[] expectedOutput = getExpectedCachedToolOutputNoPrompt(conflictingFilenames);
 
 		String[] options = { "--cached", "--staged", };
 
 		for (String option : options) {
-			assertArrayOfLinesEquals("Incorrect output for option: " + option,
-					expectedOutput, runAndCaptureUsingInitRaw("difftool",
-							option, "--tool", "some_tool"));
+			assertArrayOfMatchingLines("Incorrect output for option: " + option,
+					expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL,
+							option, "--tool", TOOL_NAME));
 		}
 	}
 
 	@Test
 	public void testToolHelp() throws Exception {
-		CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
 		List<String> expectedOutput = new ArrayList<>();
-		expectedOutput.add("git difftool --tool=<tool> may be set to one of the following:");
-		for (CommandLineDiffTool defaultTool : defaultTools) {
-			String toolName = defaultTool.name();
+
+		DiffTools diffTools = new DiffTools(db);
+		Map<String, ExternalDiffTool> predefinedTools = diffTools
+				.getPredefinedTools(true);
+		List<ExternalDiffTool> availableTools = new ArrayList<>();
+		List<ExternalDiffTool> notAvailableTools = new ArrayList<>();
+		for (ExternalDiffTool tool : predefinedTools.values()) {
+			if (tool.isAvailable()) {
+				availableTools.add(tool);
+			} else {
+				notAvailableTools.add(tool);
+			}
+		}
+
+		expectedOutput.add(
+				"'git difftool --tool=<tool>' may be set to one of the following:");
+		for (ExternalDiffTool tool : availableTools) {
+			String toolName = tool.getName();
+			expectedOutput.add(toolName);
+		}
+		String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
+				+ getEchoCommand();
+		expectedOutput.add("user-defined:");
+		expectedOutput.add(customToolHelpLine);
+		expectedOutput.add(
+				"The following tools are valid, but not currently available:");
+		for (ExternalDiffTool tool : notAvailableTools) {
+			String toolName = tool.getName();
 			expectedOutput.add(toolName);
 		}
 		String[] userDefinedToolsHelp = {
-				"user-defined:",
-				"The following tools are valid, but not currently available:",
 				"Some of the tools listed above only work in a windowed",
 				"environment. If run in a terminal-only session, they will fail.",
 		};
@@ -154,52 +246,99 @@
 
 		String option = "--tool-help";
 		assertArrayOfLinesEquals("Incorrect output for option: " + option,
-				expectedOutput.toArray(new String[0]), runAndCaptureUsingInitRaw("difftool", option));
+				expectedOutput.toArray(new String[0]),
+				runAndCaptureUsingInitRaw(DIFF_TOOL, option));
 	}
 
-	private RevCommit createUnstagedChanges() throws Exception {
-		writeTrashFile("a", "Hello world a");
-		writeTrashFile("b", "Hello world b");
-		git.add().addFilepattern(".").call();
-		RevCommit commit = git.commit().setMessage("files a & b").call();
-		writeTrashFile("a", "New Hello world a");
-		writeTrashFile("b", "New Hello world b");
-		return commit;
+	private void configureEchoTool(String toolName) {
+		StoredConfig config = db.getConfig();
+		// the default diff tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
+				toolName);
+
+		String command = getEchoCommand();
+
+		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+		/*
+		 * prevent prompts as we are running in tests and there is no user to
+		 * interact with on the command line
+		 */
+		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
+				String.valueOf(false));
 	}
 
-	private RevCommit createStagedChanges() throws Exception {
-		RevCommit commit = createUnstagedChanges();
-		git.add().addFilepattern(".").call();
-		return commit;
-	}
-
-	private List<DiffEntry> getRepositoryChanges(RevCommit commit)
-			throws Exception {
-		TreeWalk tw = new TreeWalk(db);
-		tw.addTree(commit.getTree());
-		FileTreeIterator modifiedTree = new FileTreeIterator(db);
-		tw.addTree(modifiedTree);
-		List<DiffEntry> changes = DiffEntry.scan(tw);
-		return changes;
-	}
-
-	private String[] getExpectedDiffToolOutput(List<DiffEntry> changes) {
-		String[] expectedToolOutput = new String[changes.size()];
-		for (int i = 0; i < changes.size(); ++i) {
-			DiffEntry change = changes.get(i);
-			String newPath = change.getNewPath();
-			String oldPath = change.getOldPath();
-			String newIdName = change.getNewId().name();
-			String oldIdName = change.getOldId().name();
-			String expectedLine = "M\t" + newPath + " (" + newIdName + ")"
-					+ "\t" + oldPath + " (" + oldIdName + ")";
-			expectedToolOutput[i] = expectedLine;
+	private String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) {
+		String[] expectedToolOutput = new String[conflictingFilenames.length];
+		for (int i = 0; i < conflictingFilenames.length; ++i) {
+			String newPath = conflictingFilenames[i];
+			Path fullPath = getFullPath(newPath);
+			expectedToolOutput[i] = fullPath.toString();
 		}
 		return expectedToolOutput;
 	}
 
-	private static void assertArrayOfLinesEquals(String failMessage,
-			String[] expected, String[] actual) {
-		assertEquals(failMessage, toString(expected), toString(actual));
+	private Pattern[] getExpectedCachedToolOutputNoPrompt(String[] conflictingFilenames) {
+		String tmpDir = System.getProperty("java.io.tmpdir");
+		if (tmpDir.endsWith(File.separator)) {
+			tmpDir = tmpDir.substring(0, tmpDir.length() - 1);
+		}
+		Pattern emptyPattern = Pattern.compile("");
+		List<Pattern> expectedToolOutput = new ArrayList<>();
+		for (int i = 0; i < conflictingFilenames.length; ++i) {
+			String changedFilename = conflictingFilenames[i];
+			Path fullPath = getFullPath(changedFilename);
+			String filename = fullPath.getFileName().toString();
+			String regexp = tmpDir + File.separatorChar + filename
+					+ "_REMOTE_.*";
+			Pattern pattern = Pattern.compile(regexp);
+			expectedToolOutput.add(pattern);
+			expectedToolOutput.add(emptyPattern);
+		}
+		expectedToolOutput.add(emptyPattern);
+		return expectedToolOutput.toArray(new Pattern[0]);
+	}
+
+	private String[] getExpectedCompareOutput(String[] conflictingFilenames) {
+		List<String> expected = new ArrayList<>();
+		int n = conflictingFilenames.length;
+		for (int i = 0; i < n; ++i) {
+			String changedFilename = conflictingFilenames[i];
+			expected.add(
+					"Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
+							+ "'");
+			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
+			Path fullPath = getFullPath(changedFilename);
+			expected.add(fullPath.toString());
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private String[] getExpectedAbortOutput(String[] conflictingFilenames,
+			int abortIndex) {
+		List<String> expected = new ArrayList<>();
+		int n = conflictingFilenames.length;
+		for (int i = 0; i < n; ++i) {
+			String changedFilename = conflictingFilenames[i];
+			expected.add(
+					"Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename
+							+ "'");
+			expected.add("Launch '" + TOOL_NAME + "' [Y/n]?");
+			if (i == abortIndex) {
+				break;
+			}
+			Path fullPath = getFullPath(changedFilename);
+			expected.add(fullPath.toString());
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private static String getEchoCommand() {
+		/*
+		 * use 'REMOTE' placeholder, as it will be replaced by a file path
+		 * within the repository.
+		 */
+		return "(echo \"$REMOTE\")";
 	}
 }
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
new file mode 100644
index 0000000..1236dd3
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.pgm;
+
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
+import static org.junit.Assert.fail;
+
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
+import org.eclipse.jgit.internal.diffmergetool.MergeTools;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Testing the {@code mergetool} command.
+ */
+public class MergeToolTest extends ToolTestCase {
+
+	private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		configureEchoTool(TOOL_NAME);
+	}
+
+	@Test
+	public void testUndefinedTool() throws Exception {
+		String toolName = "undefined";
+		String[] conflictingFilenames = createMergeConflict();
+
+		List<String> expectedErrors = new ArrayList<>();
+		for (String conflictingFilename : conflictingFilenames) {
+			expectedErrors.add("External merge tool is not defined: " + toolName);
+			expectedErrors.add("merge of " + conflictingFilename + " failed");
+		}
+
+		runAndCaptureUsingInitRaw(expectedErrors, MERGE_TOOL,
+				"--no-prompt", "--tool", toolName);
+	}
+
+	@Test(expected = Die.class)
+	public void testUserToolWithCommandNotFoundError() throws Exception {
+		String toolName = "customTool";
+
+		int errorReturnCode = 127; // command not found
+		String command = "exit " + errorReturnCode;
+
+		StoredConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+
+		createMergeConflict();
+		runAndCaptureUsingInitRaw(MERGE_TOOL, "--no-prompt", "--tool",
+				toolName);
+
+		fail("Expected exception to be thrown due to external tool exiting with error code: "
+				+ errorReturnCode);
+	}
+
+	@Test
+	public void testEmptyToolName() throws Exception {
+		String emptyToolName = "";
+
+		StoredConfig config = db.getConfig();
+		// the default merge tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
+				emptyToolName);
+
+		createMergeConflict();
+
+		String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
+		String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
+		runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput),
+				MERGE_TOOL, "--no-prompt");
+	}
+
+	@Test
+	public void testAbortMerge() throws Exception {
+		String[] inputLines = {
+				"y", // start tool for merge resolution
+				"n", // don't accept merge tool result
+				"n", // don't continue resolution
+		};
+		String[] conflictingFilenames = createMergeConflict();
+		int abortIndex = 1;
+		String[] expectedOutput = getExpectedAbortMergeOutput(
+				conflictingFilenames,
+				abortIndex);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						MERGE_TOOL, "--prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testAbortLaunch() throws Exception {
+		String[] inputLines = {
+				"n", // abort merge tool launch
+		};
+		String[] conflictingFilenames = createMergeConflict();
+		String[] expectedOutput = getExpectedAbortLaunchOutput(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						MERGE_TOOL, "--prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testMergeConflict() throws Exception {
+		String[] inputLines = {
+				"y", // start tool for merge resolution
+				"y", // accept merge result as successful
+				"y", // start tool for merge resolution
+				"y", // accept merge result as successful
+		};
+		String[] conflictingFilenames = createMergeConflict();
+		String[] expectedOutput = getExpectedMergeConflictOutput(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						MERGE_TOOL, "--prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testDeletedConflict() throws Exception {
+		String[] inputLines = {
+				"d", // choose delete option to resolve conflict
+				"m", // choose merge option to resolve conflict
+		};
+		String[] conflictingFilenames = createDeletedConflict();
+		String[] expectedOutput = getExpectedDeletedConflictOutput(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		InputStream inputStream = createInputStream(inputLines);
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(inputStream,
+						MERGE_TOOL, "--prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testNoConflict() throws Exception {
+		createStagedChanges();
+		String[] expectedOutput = { "No files need merging" };
+
+		String[] options = { "--tool", "-t", };
+
+		for (String option : options) {
+			assertArrayOfLinesEquals("Incorrect output for option: " + option,
+					expectedOutput,
+					runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
+		}
+	}
+
+	@Test
+	public void testMergeConflictNoPrompt() throws Exception {
+		String[] conflictingFilenames = createMergeConflict();
+		String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput,
+				runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME));
+	}
+
+	@Test
+	public void testMergeConflictNoGuiNoPrompt() throws Exception {
+		String[] conflictingFilenames = createMergeConflict();
+		String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt(
+				conflictingFilenames);
+
+		String option = "--tool";
+
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL,
+						"--no-gui", "--no-prompt", option, TOOL_NAME));
+	}
+
+	@Test
+	public void testToolHelp() throws Exception {
+		List<String> expectedOutput = new ArrayList<>();
+
+		MergeTools diffTools = new MergeTools(db);
+		Map<String, ExternalMergeTool> predefinedTools = diffTools
+				.getPredefinedTools(true);
+		List<ExternalMergeTool> availableTools = new ArrayList<>();
+		List<ExternalMergeTool> notAvailableTools = new ArrayList<>();
+		for (ExternalMergeTool tool : predefinedTools.values()) {
+			if (tool.isAvailable()) {
+				availableTools.add(tool);
+			} else {
+				notAvailableTools.add(tool);
+			}
+		}
+
+		expectedOutput.add(
+				"'git mergetool --tool=<tool>' may be set to one of the following:");
+		for (ExternalMergeTool tool : availableTools) {
+			String toolName = tool.getName();
+			expectedOutput.add(toolName);
+		}
+		String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " "
+				+ getEchoCommand();
+		expectedOutput.add("user-defined:");
+		expectedOutput.add(customToolHelpLine);
+		expectedOutput.add(
+				"The following tools are valid, but not currently available:");
+		for (ExternalMergeTool tool : notAvailableTools) {
+			String toolName = tool.getName();
+			expectedOutput.add(toolName);
+		}
+		String[] userDefinedToolsHelp = {
+				"Some of the tools listed above only work in a windowed",
+				"environment. If run in a terminal-only session, they will fail.", };
+		expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp));
+
+		String option = "--tool-help";
+		assertArrayOfLinesEquals("Incorrect output for option: " + option,
+				expectedOutput.toArray(new String[0]),
+				runAndCaptureUsingInitRaw(MERGE_TOOL, option));
+	}
+
+	private void configureEchoTool(String toolName) {
+		StoredConfig config = db.getConfig();
+		// the default merge tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
+				toolName);
+
+		String command = getEchoCommand();
+
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+		/*
+		 * prevent prompts as we are running in tests and there is no user to
+		 * interact with on the command line
+		 */
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PROMPT,
+				String.valueOf(false));
+	}
+
+	private String[] getExpectedMergeConflictOutputNoPrompt(
+			String[] conflictFilenames) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String conflictFilename : conflictFilenames) {
+			expected.add(conflictFilename);
+		}
+		for (String conflictFilename : conflictFilenames) {
+			expected.add("Normal merge conflict for '" + conflictFilename
+					+ "':");
+			expected.add("{local}: modified file");
+			expected.add("{remote}: modified file");
+			Path filePath = getFullPath(conflictFilename);
+			expected.add(filePath.toString());
+			expected.add(conflictFilename + " seems unchanged.");
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private static String[] getExpectedAbortLaunchOutput(
+			String[] conflictFilenames) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String conflictFilename : conflictFilenames) {
+			expected.add(conflictFilename);
+		}
+		if (conflictFilenames.length > 1) {
+			String conflictFilename = conflictFilenames[0];
+			expected.add(
+					"Normal merge conflict for '" + conflictFilename + "':");
+			expected.add("{local}: modified file");
+			expected.add("{remote}: modified file");
+			expected.add("Hit return to start merge resolution tool ("
+					+ TOOL_NAME + "):");
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private String[] getExpectedAbortMergeOutput(
+			String[] conflictFilenames, int abortIndex) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String conflictFilename : conflictFilenames) {
+			expected.add(conflictFilename);
+		}
+		for (int i = 0; i < conflictFilenames.length; ++i) {
+			if (i == abortIndex) {
+				break;
+			}
+
+			String conflictFilename = conflictFilenames[i];
+			expected.add(
+					"Normal merge conflict for '" + conflictFilename + "':");
+			expected.add("{local}: modified file");
+			expected.add("{remote}: modified file");
+			Path fullPath = getFullPath(conflictFilename);
+			expected.add("Hit return to start merge resolution tool ("
+					+ TOOL_NAME + "): " + fullPath);
+			expected.add(conflictFilename + " seems unchanged.");
+			expected.add("Was the merge successful [y/n]?");
+			if (i < conflictFilenames.length - 1) {
+				expected.add(
+						"\tContinue merging other unresolved paths [y/n]?");
+			}
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private String[] getExpectedMergeConflictOutput(
+			String[] conflictFilenames) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String conflictFilename : conflictFilenames) {
+			expected.add(conflictFilename);
+		}
+		for (int i = 0; i < conflictFilenames.length; ++i) {
+			String conflictFilename = conflictFilenames[i];
+			expected.add("Normal merge conflict for '" + conflictFilename
+					+ "':");
+			expected.add("{local}: modified file");
+			expected.add("{remote}: modified file");
+			Path filePath = getFullPath(conflictFilename);
+			expected.add("Hit return to start merge resolution tool ("
+					+ TOOL_NAME + "): " + filePath);
+			expected.add(conflictFilename + " seems unchanged.");
+			expected.add("Was the merge successful [y/n]?");
+			if (i < conflictFilenames.length - 1) {
+				// expected.add(
+				// "\tContinue merging other unresolved paths [y/n]?");
+			}
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private static String[] getExpectedDeletedConflictOutput(
+			String[] conflictFilenames) {
+		List<String> expected = new ArrayList<>();
+		expected.add("Merging:");
+		for (String mergeConflictFilename : conflictFilenames) {
+			expected.add(mergeConflictFilename);
+		}
+		for (int i = 0; i < conflictFilenames.length; ++i) {
+			String conflictFilename = conflictFilenames[i];
+			expected.add(conflictFilename + " seems unchanged.");
+			expected.add("{local}: deleted");
+			expected.add("{remote}: modified file");
+			expected.add("Use (m)odified or (d)eleted file, or (a)bort?");
+		}
+		return expected.toArray(new String[0]);
+	}
+
+	private static String getEchoCommand() {
+		/*
+		 * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be
+		 * replaced with full paths to a temporary file during some of the tests
+		 */
+		return "(echo \"$MERGED\")";
+	}
+}
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index ed980b5..5c351e5 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.pgm
 Bundle-SymbolicName: org.eclipse.jgit.pgm
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-11
@@ -14,49 +14,49 @@
  org.eclipse.jetty.servlet;version="[10.0.0,11.0.0)",
  org.eclipse.jetty.util;version="[10.0.0,11.0.0)",
  org.eclipse.jetty.util.component;version="[10.0.0,11.0.0)",
- org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.archive;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.awtui;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.blame;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.diff;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.dircache;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.gitrepo;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.diffmergetool;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.io;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.pack;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.server;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.server.fs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs.server.s3;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.merge;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.notes;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revplot;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk.filter;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.pack;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.http.apache;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.resolver;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.ssh.jsch;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.sshd;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk.filter;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util.io;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.archive;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.awtui;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.blame;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.diff;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.dircache;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.gitrepo;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.io;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.server;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs.server.s3;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.merge;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.notes;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revplot;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk.filter;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.pack;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.http.apache;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.resolver;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.ssh.jsch;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.sshd;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk.filter;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util.io;version="[6.2.1,6.3.0)",
  org.kohsuke.args4j;version="[2.33.0,3.0.0)",
  org.kohsuke.args4j.spi;version="[2.33.0,3.0.0)"
-Export-Package: org.eclipse.jgit.console;version="6.1.1";
+Export-Package: org.eclipse.jgit.console;version="6.2.1";
  uses:="org.eclipse.jgit.transport,
   org.eclipse.jgit.util",
- org.eclipse.jgit.pgm;version="6.1.1";
+ org.eclipse.jgit.pgm;version="6.2.1";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.util.io,
    org.eclipse.jgit.awtui,
@@ -68,14 +68,14 @@
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.api,
    javax.swing",
- org.eclipse.jgit.pgm.debug;version="6.1.1";
+ org.eclipse.jgit.pgm.debug;version="6.2.1";
   uses:="org.eclipse.jgit.util.io,
    org.eclipse.jgit.pgm,
    org.eclipse.jetty.servlet",
- org.eclipse.jgit.pgm.internal;version="6.1.1";
+ org.eclipse.jgit.pgm.internal;version="6.2.1";
   x-friends:="org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.test",
- org.eclipse.jgit.pgm.opt;version="6.1.1";
+ org.eclipse.jgit.pgm.opt;version="6.2.1";
   uses:="org.kohsuke.args4j,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
diff --git a/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
index 0a5dd88..616f01b 100644
--- a/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.pgm - Sources
 Bundle-SymbolicName: org.eclipse.jgit.pgm.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
index 8c44764..ea1d1e3 100644
--- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
+++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
@@ -25,6 +25,7 @@
 org.eclipse.jgit.pgm.LsTree
 org.eclipse.jgit.pgm.Merge
 org.eclipse.jgit.pgm.MergeBase
+org.eclipse.jgit.pgm.MergeTool
 org.eclipse.jgit.pgm.Push
 org.eclipse.jgit.pgm.ReceivePack
 org.eclipse.jgit.pgm.Reflog
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index c189dc7..0c0f336 100644
--- a/org.eclipse.jgit.pgm/pom.xml
+++ b/org.eclipse.jgit.pgm/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm</artifactId>
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index c364376..74ea867 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -58,9 +58,11 @@
 dateInfo=Date:   {0}
 deletedBranch=Deleted branch {0}
 deletedRemoteBranch=Deleted remote branch {0}
-diffToolHelpSetToFollowing='git difftool --tool=<tool>' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
-diffToolLaunch=Viewing ({0}/{1}): '{2}'\nLaunch '{3}' [Y/n]?
-diffToolDied=external diff died, stopping at {0}
+diffToolHelpSetToFollowing=''git difftool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
+diffToolLaunch=Viewing ({0}/{1}): ''{2}''\nLaunch ''{3}'' [Y/n]?
+diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1}
+diffToolPromptToolName=This message is displayed because 'diff.tool' is not configured.\nSee 'git difftool --tool-help' or 'git help config' for more details.\n'git difftool' will now attempt to use one of the following tools:\n{0}\n
+diffToolUnknownToolName=Unknown diff tool '{0}'
 doesNotExist={0} does not exist
 dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge:
 everythingUpToDate=Everything up-to-date
@@ -91,6 +93,24 @@
 logNoSignatureVerifier="No signature verifier available"
 mergeConflict=CONFLICT(content): Merge conflict in {0}
 mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge:
+mergeToolHelpSetToFollowing=''git mergetool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail.
+mergeToolLaunch=Hit return to start merge resolution tool ({0}):
+mergeToolDied=local or remote cannot be found in cache, stopping at {0}
+mergeToolNoFiles=No files need merging
+mergeToolMerging=Merging:\n{0}
+mergeToolUnknownConflict=\nUnknown merge conflict for ''{0}'':
+mergeToolNormalConflict=\nNormal merge conflict for ''{0}'':\n  '{'local'}': modified file\n  '{'remote'}': modified file
+mergeToolMergeFailed=merge of {0} failed
+mergeToolExecutionError=excution error
+mergeToolFileUnchanged=\n{0} seems unchanged.
+mergeToolDeletedConflict=\nDeleted merge conflict for ''{0}'':
+mergeToolDeletedConflictByUs=  {local}: deleted\n  {remote}: modified file
+mergeToolDeletedConflictByThem=  {local}: modified file\n  {remote}: deleted
+mergeToolContinueUnresolvedPaths=\nContinue merging other unresolved paths [y/n]?
+mergeToolWasMergeSuccessfull=Was the merge successful [y/n]?
+mergeToolDeletedMergeDecision=Use (m)odified or (d)eleted file, or (a)bort?
+mergeToolPromptToolName=This message is displayed because 'merge.tool' is not configured.\nSee 'git mergetool --tool-help' or 'git help config' for more details.\n'git mergetool' will now attempt to use one of the following tools:\n{0}\n
+mergeToolUnknownToolName=Unknown merge tool '{0}'
 mergeFailed=Automatic merge failed; fix conflicts and then commit the result
 mergeCheckoutFailed=Please, commit your changes or stash them before you can merge.
 mergeMadeBy=Merge made by the ''{0}'' strategy.
@@ -255,6 +275,7 @@
 usage_Gc=Cleanup unnecessary files and optimize the local repository
 usage_Glog=View commit history as a graph
 usage_DiffGuiTool=When git-difftool is invoked with the -g or --gui option the default diff tool will be read from the configured diff.guitool variable instead of diff.tool.
+usage_MergeGuiTool=When git-mergetool is invoked with the -g or --gui option the default merge tool will be read from the configured merge.guitool variable instead of merge.tool.
 usage_noGui=The --no-gui option can be used to override -g or --gui setting.
 usage_IndexPack=Build pack index file for an existing packed archive
 usage_LFSDirectory=Directory to store large objects
@@ -304,6 +325,7 @@
 usage_StopTrackingAFile=Stop tracking a file
 usage_TextHashFunctions=Scan repository to compute maximum number of collisions for hash functions
 usage_ToolForDiff=Use the diff tool specified by <tool>. Run git difftool --tool-help for the list of valid <tool> settings.\nIf a diff tool is not specified, git difftool will use the configuration variable diff.tool.
+usage_ToolForMerge=Use the merge resolution program specified by <tool>. Run git mergetool --tool-help for the list of valid <tool> settings.\nIf a merge resolution program is not specified, git mergetool will use the configuration variable merge.tool.
 usage_UpdateRemoteRepositoryFromLocalRefs=Update remote repository from local refs
 usage_UseAll=Use all refs found in refs/
 usage_UseTags=Use any tag including lightweight tags
@@ -351,6 +373,7 @@
 usage_detectRenames=detect renamed files
 usage_diffAlgorithm=the diff algorithm to use. Currently supported are: 'myers', 'histogram'
 usage_DiffTool=git difftool is a Git command that allows you to compare and edit files between revisions using common diff tools.\ngit difftool is a frontend to git diff and accepts the same options and arguments.
+usage_MergeTool=git-mergetool - Run merge conflict resolution tools to resolve merge conflicts.\nUse git mergetool to run one of several merge utilities to resolve merge conflicts. It is typically run after git merge.
 usage_directoriesToExport=directories to export
 usage_disableTheServiceInAllRepositories=disable the service in all repositories
 usage_displayAListOfAllRegisteredJgitCommands=Display a list of all registered jgit commands
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
index 1288817..3e6042a 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java
@@ -1,5 +1,6 @@
 /*
- * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -11,25 +12,40 @@
 package org.eclipse.jgit.pgm;
 
 import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
 
 import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
 import java.text.MessageFormat;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jgit.diff.ContentSource;
+import org.eclipse.jgit.diff.ContentSource.Pair;
 import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffEntry.Side;
 import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.NoWorkTreeException;
 import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.internal.diffmergetool.DiffTools;
 import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool;
+import org.eclipse.jgit.internal.diffmergetool.FileElement;
+import org.eclipse.jgit.internal.diffmergetool.PromptContinueHandler;
+import org.eclipse.jgit.internal.diffmergetool.ToolException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
@@ -37,11 +53,16 @@
 import org.eclipse.jgit.lib.internal.BooleanTriState;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
-import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.FS.ExecutionResult;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -57,9 +78,13 @@
 	@Argument(index = 1, metaVar = "metaVar_treeish")
 	private AbstractTreeIterator newTree;
 
+	private Optional<String> toolName = Optional.empty();
+
 	@Option(name = "--tool", aliases = {
 			"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForDiff")
-	private String toolName;
+	void setToolName(String name) {
+		toolName = Optional.of(name);
+	}
 
 	@Option(name = "--cached", aliases = { "--staged" }, usage = "usage_cached")
 	private boolean cached;
@@ -79,16 +104,16 @@
 	@Option(name = "--tool-help", usage = "usage_toolHelp")
 	private boolean toolHelp;
 
-	private BooleanTriState gui = BooleanTriState.UNSET;
+	private boolean gui = false;
 
 	@Option(name = "--gui", aliases = { "-g" }, usage = "usage_DiffGuiTool")
 	void setGui(@SuppressWarnings("unused") boolean on) {
-		gui = BooleanTriState.TRUE;
+		gui = true;
 	}
 
 	@Option(name = "--no-gui", usage = "usage_noGui")
 	void noGui(@SuppressWarnings("unused") boolean on) {
-		gui = BooleanTriState.FALSE;
+		gui = false;
 	}
 
 	private BooleanTriState trustExitCode = BooleanTriState.UNSET;
@@ -106,11 +131,14 @@
 	@Option(name = "--", metaVar = "metaVar_paths", handler = PathTreeFilterHandler.class)
 	private TreeFilter pathFilter = TreeFilter.ALL;
 
+	private BufferedReader inputReader;
+
 	@Override
 	protected void init(Repository repository, String gitDir) {
 		super.init(repository, gitDir);
 		diffFmt = new DiffFormatter(new BufferedOutputStream(outs));
 		diffTools = new DiffTools(repository);
+		inputReader = new BufferedReader(new InputStreamReader(ins, StandardCharsets.UTF_8));
 	}
 
 	@Override
@@ -119,23 +147,12 @@
 			if (toolHelp) {
 				showToolHelp();
 			} else {
-				boolean showPrompt = diffTools.isInteractive();
-				if (prompt != BooleanTriState.UNSET) {
-					showPrompt = prompt == BooleanTriState.TRUE;
-				}
-				String toolNamePrompt = toolName;
-				if (showPrompt) {
-					if (StringUtils.isEmptyOrNull(toolNamePrompt)) {
-						toolNamePrompt = diffTools.getDefaultToolName(gui);
-					}
-				}
 				// get the changed files
 				List<DiffEntry> files = getFiles();
 				if (files.size() > 0) {
-					compare(files, showPrompt, toolNamePrompt);
+					compare(files);
 				}
 			}
-			outw.flush();
 		} catch (RevisionSyntaxException | IOException e) {
 			throw die(e.getMessage(), e);
 		} finally {
@@ -143,77 +160,127 @@
 		}
 	}
 
-	private void compare(List<DiffEntry> files, boolean showPrompt,
-			String toolNamePrompt) throws IOException {
-		for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
-			DiffEntry ent = files.get(fileIndex);
-			String mergedFilePath = ent.getNewPath();
-			if (mergedFilePath.equals(DiffEntry.DEV_NULL)) {
-				mergedFilePath = ent.getOldPath();
+	private void informUserNoTool(List<String> tools) {
+		try {
+			StringBuilder toolNames = new StringBuilder();
+			for (String name : tools) {
+				toolNames.append(name + " "); //$NON-NLS-1$
 			}
-			// check if user wants to launch compare
-			boolean launchCompare = true;
-			if (showPrompt) {
-				launchCompare = isLaunchCompare(fileIndex + 1, files.size(),
-						mergedFilePath, toolNamePrompt);
-			}
-			if (launchCompare) {
-				switch (ent.getChangeType()) {
-				case MODIFY:
-					outw.println("M\t" + ent.getNewPath() //$NON-NLS-1$
-							+ " (" + ent.getNewId().name() + ")" //$NON-NLS-1$ //$NON-NLS-2$
-							+ "\t" + ent.getOldPath() //$NON-NLS-1$
-							+ " (" + ent.getOldId().name() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
-					int ret = diffTools.compare(ent.getNewPath(),
-							ent.getOldPath(), ent.getNewId().name(),
-							ent.getOldId().name(), toolName, prompt, gui,
-							trustExitCode);
-					if (ret != 0) {
-						throw die(MessageFormat.format(
-								CLIText.get().diffToolDied, mergedFilePath));
+			outw.println(MessageFormat.format(
+					CLIText.get().diffToolPromptToolName, toolNames));
+			outw.flush();
+		} catch (IOException e) {
+			throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
+		}
+	}
+
+	private class CountingPromptContinueHandler
+			implements PromptContinueHandler {
+		private final int fileIndex;
+
+		private final int fileCount;
+
+		private final String fileName;
+
+		public CountingPromptContinueHandler(int fileIndex, int fileCount,
+				String fileName) {
+			this.fileIndex = fileIndex;
+			this.fileCount = fileCount;
+			this.fileName = fileName;
+		}
+
+		@SuppressWarnings("boxing")
+		@Override
+		public boolean prompt(String toolToLaunchName) {
+			try {
+				boolean launchCompare = true;
+				outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
+						fileIndex, fileCount, fileName, toolToLaunchName)
+						+ " "); //$NON-NLS-1$
+				outw.flush();
+				BufferedReader br = inputReader;
+				String line = null;
+				if ((line = br.readLine()) != null) {
+					if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
+						launchCompare = false;
 					}
-					break;
-				default:
-					break;
 				}
-			} else {
-				break;
+				return launchCompare;
+			} catch (IOException e) {
+				throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
 			}
 		}
 	}
 
-	@SuppressWarnings("boxing")
-	private boolean isLaunchCompare(int fileIndex, int fileCount,
-			String fileName, String toolNamePrompt) throws IOException {
-		boolean launchCompare = true;
-		outw.println(MessageFormat.format(CLIText.get().diffToolLaunch,
-				fileIndex, fileCount, fileName, toolNamePrompt));
-		outw.flush();
-		BufferedReader br = new BufferedReader(
-				new InputStreamReader(ins, StandardCharsets.UTF_8));
-		String line = null;
-		if ((line = br.readLine()) != null) {
-			if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$
-				launchCompare = false;
+	private void compare(List<DiffEntry> files) throws IOException {
+		ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree),
+				source(newTree));
+		try {
+			for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
+				DiffEntry ent = files.get(fileIndex);
+
+				String filePath = ent.getNewPath();
+				if (filePath.equals(DiffEntry.DEV_NULL)) {
+					filePath = ent.getOldPath();
+				}
+
+				try {
+					FileElement local = createFileElement(
+							FileElement.Type.LOCAL, sourcePair, Side.OLD, ent);
+					FileElement remote = createFileElement(
+							FileElement.Type.REMOTE, sourcePair, Side.NEW, ent);
+
+					PromptContinueHandler promptContinueHandler = new CountingPromptContinueHandler(
+							fileIndex + 1, files.size(), filePath);
+
+					Optional<ExecutionResult> optionalResult = diffTools
+							.compare(local, remote, toolName, prompt, gui,
+									trustExitCode, promptContinueHandler,
+									this::informUserNoTool);
+
+					if (optionalResult.isPresent()) {
+						ExecutionResult result = optionalResult.get();
+						// TODO: check how to return the exit-code of the tool
+						// to jgit / java runtime ?
+						// int rc =...
+						outw.println(
+								new String(result.getStdout().toByteArray()));
+						outw.flush();
+						errw.println(
+								new String(result.getStderr().toByteArray()));
+						errw.flush();
+					}
+				} catch (ToolException e) {
+					outw.println(e.getResultStdout());
+					outw.flush();
+					errw.println(e.getMessage());
+					errw.flush();
+					throw die(MessageFormat.format(
+							CLIText.get().diffToolDied, filePath, e), e);
+				}
 			}
+		} finally {
+			sourcePair.close();
 		}
-		return launchCompare;
 	}
 
 	private void showToolHelp() throws IOException {
+		Map<String, ExternalDiffTool> predefTools = diffTools
+				.getPredefinedTools(true);
 		StringBuilder availableToolNames = new StringBuilder();
-		for (String name : diffTools.getAvailableTools().keySet()) {
-			availableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$
-		}
 		StringBuilder notAvailableToolNames = new StringBuilder();
-		for (String name : diffTools.getNotAvailableTools().keySet()) {
-			notAvailableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$
+		for (String name : predefTools.keySet()) {
+			if (predefTools.get(name).isAvailable()) {
+				availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
+			} else {
+				notAvailableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
+			}
 		}
 		StringBuilder userToolNames = new StringBuilder();
 		Map<String, ExternalDiffTool> userTools = diffTools
 				.getUserDefinedTools();
 		for (String name : userTools.keySet()) {
-			userToolNames.append(String.format("\t\t%s.cmd %s\n", //$NON-NLS-1$
+			userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
 					name, userTools.get(name).getCommand()));
 		}
 		outw.println(MessageFormat.format(
@@ -254,4 +321,54 @@
 		return files;
 	}
 
+	private FileElement createFileElement(FileElement.Type elementType,
+			Pair pair, Side side, DiffEntry entry) throws NoWorkTreeException,
+			CorruptObjectException, IOException, ToolException {
+		String entryPath = side == Side.NEW ? entry.getNewPath()
+				: entry.getOldPath();
+		FileElement fileElement = new FileElement(entryPath, elementType,
+				db.getWorkTree());
+		if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) {
+			try (RevWalk revWalk = new RevWalk(db);
+					TreeWalk treeWalk = new TreeWalk(db,
+							revWalk.getObjectReader())) {
+				treeWalk.setFilter(
+						PathFilterGroup.createFromStrings(entryPath));
+				if (side == Side.NEW) {
+					newTree.reset();
+					treeWalk.addTree(newTree);
+				} else {
+					oldTree.reset();
+					treeWalk.addTree(oldTree);
+				}
+				if (treeWalk.next()) {
+					final EolStreamType eolStreamType = treeWalk
+							.getEolStreamType(CHECKOUT_OP);
+					final String filterCommand = treeWalk.getFilterCommand(
+							Constants.ATTR_FILTER_TYPE_SMUDGE);
+					WorkingTreeOptions opt = db.getConfig()
+							.get(WorkingTreeOptions.KEY);
+					CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
+							eolStreamType, filterCommand);
+					DirCacheCheckout.getContent(db, entryPath,
+							checkoutMetadata, pair.open(side, entry), opt,
+							new FileOutputStream(
+									fileElement.createTempFile(null)));
+				} else {
+					throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$
+							+ "' in staging area!", //$NON-NLS-1$
+							null);
+				}
+			}
+		}
+		return fileElement;
+	}
+
+	private ContentSource source(AbstractTreeIterator iterator) {
+		if (iterator instanceof WorkingTreeIterator) {
+			return ContentSource.create((WorkingTreeIterator) iterator);
+		}
+		return ContentSource.create(db.newObjectReader());
+	}
+
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
new file mode 100644
index 0000000..2a411b8
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.pgm;
+
+import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeMap;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.Status;
+import org.eclipse.jgit.api.StatusCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.diff.ContentSource;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.errors.NoWorkTreeException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool;
+import org.eclipse.jgit.internal.diffmergetool.FileElement;
+import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
+import org.eclipse.jgit.internal.diffmergetool.MergeTools;
+import org.eclipse.jgit.internal.diffmergetool.ToolException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
+import org.eclipse.jgit.lib.IndexDiff.StageState;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeOptions;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
+
+@Command(name = "mergetool", common = true, usage = "usage_MergeTool")
+class MergeTool extends TextBuiltin {
+	private MergeTools mergeTools;
+
+	private Optional<String> toolName = Optional.empty();
+
+	@Option(name = "--tool", aliases = {
+			"-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge")
+	void setToolName(String name) {
+		toolName = Optional.of(name);
+	}
+
+	private BooleanTriState prompt = BooleanTriState.UNSET;
+
+	@Option(name = "--prompt", usage = "usage_prompt")
+	void setPrompt(@SuppressWarnings("unused") boolean on) {
+		prompt = BooleanTriState.TRUE;
+	}
+
+	@Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt")
+	void noPrompt(@SuppressWarnings("unused") boolean on) {
+		prompt = BooleanTriState.FALSE;
+	}
+
+	@Option(name = "--tool-help", usage = "usage_toolHelp")
+	private boolean toolHelp;
+
+	private boolean gui = false;
+
+	@Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool")
+	void setGui(@SuppressWarnings("unused") boolean on) {
+		gui = true;
+	}
+
+	@Option(name = "--no-gui", usage = "usage_noGui")
+	void noGui(@SuppressWarnings("unused") boolean on) {
+		gui = false;
+	}
+
+	@Argument(required = false, index = 0, metaVar = "metaVar_paths")
+	@Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class)
+	protected List<String> filterPaths;
+
+	private BufferedReader inputReader;
+
+	@Override
+	protected void init(Repository repository, String gitDir) {
+		super.init(repository, gitDir);
+		mergeTools = new MergeTools(repository);
+		inputReader = new BufferedReader(new InputStreamReader(ins));
+	}
+
+	enum MergeResult {
+		SUCCESSFUL, FAILED, ABORTED
+	}
+
+	@Override
+	protected void run() {
+		try {
+			if (toolHelp) {
+				showToolHelp();
+			} else {
+				// get the changed files
+				Map<String, StageState> files = getFiles();
+				if (files.size() > 0) {
+					merge(files);
+				} else {
+					outw.println(CLIText.get().mergeToolNoFiles);
+				}
+			}
+			outw.flush();
+		} catch (Exception e) {
+			throw die(e.getMessage(), e);
+		}
+	}
+
+	private void informUserNoTool(List<String> tools) {
+		try {
+			StringBuilder toolNames = new StringBuilder();
+			for (String name : tools) {
+				toolNames.append(name + " "); //$NON-NLS-1$
+			}
+			outw.println(MessageFormat
+					.format(CLIText.get().mergeToolPromptToolName, toolNames));
+			outw.flush();
+		} catch (IOException e) {
+			throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
+		}
+	}
+
+	private void merge(Map<String, StageState> files) throws Exception {
+		// sort file names
+		List<String> mergedFilePaths = new ArrayList<>(files.keySet());
+		Collections.sort(mergedFilePaths);
+		// show the files
+		StringBuilder mergedFiles = new StringBuilder();
+		for (String mergedFilePath : mergedFilePaths) {
+			mergedFiles.append(MessageFormat.format("{0}\n", mergedFilePath)); //$NON-NLS-1$
+		}
+		outw.println(MessageFormat.format(CLIText.get().mergeToolMerging,
+				mergedFiles));
+		outw.flush();
+		boolean showPrompt = mergeTools.isInteractive();
+		if (prompt != BooleanTriState.UNSET) {
+			showPrompt = prompt == BooleanTriState.TRUE;
+		}
+		// merge the files
+		MergeResult mergeResult = MergeResult.SUCCESSFUL;
+		for (String mergedFilePath : mergedFilePaths) {
+			// if last merge failed...
+			if (mergeResult == MergeResult.FAILED) {
+				// check if user wants to continue
+				if (showPrompt && !isContinueUnresolvedPaths()) {
+					mergeResult = MergeResult.ABORTED;
+				}
+			}
+			// aborted ?
+			if (mergeResult == MergeResult.ABORTED) {
+				break;
+			}
+			// get file stage state and merge
+			StageState fileState = files.get(mergedFilePath);
+			if (fileState == StageState.BOTH_MODIFIED) {
+				mergeResult = mergeModified(mergedFilePath, showPrompt);
+			} else if ((fileState == StageState.DELETED_BY_US)
+					|| (fileState == StageState.DELETED_BY_THEM)) {
+				mergeResult = mergeDeleted(mergedFilePath,
+						fileState == StageState.DELETED_BY_US);
+			} else {
+				outw.println(MessageFormat.format(
+						CLIText.get().mergeToolUnknownConflict,
+						mergedFilePath));
+				mergeResult = MergeResult.ABORTED;
+			}
+		}
+	}
+
+	private MergeResult mergeModified(String mergedFilePath, boolean showPrompt)
+			throws Exception {
+		outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict,
+				mergedFilePath));
+		outw.flush();
+		boolean isMergeSuccessful = true;
+		ContentSource baseSource = ContentSource.create(db.newObjectReader());
+		ContentSource localSource = ContentSource.create(db.newObjectReader());
+		ContentSource remoteSource = ContentSource.create(db.newObjectReader());
+		// temporary directory if mergetool.writeToTemp == true
+		File tempDir = mergeTools.createTempDirectory();
+		// the parent directory for temp files (can be same as tempDir or just
+		// the worktree dir)
+		File tempFilesParent = tempDir != null ? tempDir : db.getWorkTree();
+		try {
+			FileElement base = null;
+			FileElement local = null;
+			FileElement remote = null;
+			FileElement merged = new FileElement(mergedFilePath, Type.MERGED,
+					db.getWorkTree());
+			DirCache cache = db.readDirCache();
+			try (RevWalk revWalk = new RevWalk(db);
+					TreeWalk treeWalk = new TreeWalk(db,
+							revWalk.getObjectReader())) {
+				treeWalk.setFilter(
+						PathFilterGroup.createFromStrings(mergedFilePath));
+				DirCacheIterator cacheIter = new DirCacheIterator(cache);
+				treeWalk.addTree(cacheIter);
+				while (treeWalk.next()) {
+					if (treeWalk.isSubtree()) {
+						treeWalk.enterSubtree();
+						continue;
+					}
+					final EolStreamType eolStreamType = treeWalk
+							.getEolStreamType(CHECKOUT_OP);
+					final String filterCommand = treeWalk.getFilterCommand(
+							Constants.ATTR_FILTER_TYPE_SMUDGE);
+					WorkingTreeOptions opt = db.getConfig()
+							.get(WorkingTreeOptions.KEY);
+					CheckoutMetadata checkoutMetadata = new CheckoutMetadata(
+							eolStreamType, filterCommand);
+					DirCacheEntry entry = treeWalk
+							.getTree(DirCacheIterator.class).getDirCacheEntry();
+					if (entry == null) {
+						continue;
+					}
+					ObjectId id = entry.getObjectId();
+					switch (entry.getStage()) {
+					case DirCacheEntry.STAGE_1:
+						base = new FileElement(mergedFilePath, Type.BASE);
+						DirCacheCheckout.getContent(db, mergedFilePath,
+								checkoutMetadata,
+								baseSource.open(mergedFilePath, id), opt,
+								new FileOutputStream(
+										base.createTempFile(tempFilesParent)));
+						break;
+					case DirCacheEntry.STAGE_2:
+						local = new FileElement(mergedFilePath, Type.LOCAL);
+						DirCacheCheckout.getContent(db, mergedFilePath,
+								checkoutMetadata,
+								localSource.open(mergedFilePath, id), opt,
+								new FileOutputStream(
+										local.createTempFile(tempFilesParent)));
+						break;
+					case DirCacheEntry.STAGE_3:
+						remote = new FileElement(mergedFilePath, Type.REMOTE);
+						DirCacheCheckout.getContent(db, mergedFilePath,
+								checkoutMetadata,
+								remoteSource.open(mergedFilePath, id), opt,
+								new FileOutputStream(remote
+										.createTempFile(tempFilesParent)));
+						break;
+					}
+				}
+			}
+			if ((local == null) || (remote == null)) {
+				throw die(MessageFormat.format(CLIText.get().mergeToolDied,
+						mergedFilePath));
+			}
+			long modifiedBefore = merged.getFile().lastModified();
+			try {
+				// TODO: check how to return the exit-code of the
+				// tool to jgit / java runtime ?
+				// int rc =...
+				Optional<ExecutionResult> optionalResult = mergeTools.merge(
+						local, remote, merged, base, tempDir, toolName, prompt,
+						gui, this::promptForLaunch, this::informUserNoTool);
+				if (optionalResult.isPresent()) {
+					ExecutionResult result = optionalResult.get();
+					outw.println(new String(result.getStdout().toByteArray()));
+					outw.flush();
+					errw.println(new String(result.getStderr().toByteArray()));
+					errw.flush();
+				} else {
+					return MergeResult.ABORTED;
+				}
+			} catch (ToolException e) {
+				isMergeSuccessful = false;
+				outw.println(e.getResultStdout());
+				outw.flush();
+				errw.println(e.getMessage());
+				errw.println(MessageFormat.format(
+						CLIText.get().mergeToolMergeFailed, mergedFilePath));
+				errw.flush();
+				if (e.isCommandExecutionError()) {
+					throw die(CLIText.get().mergeToolExecutionError, e);
+				}
+			}
+			// if merge was successful check file modified
+			if (isMergeSuccessful) {
+				long modifiedAfter = merged.getFile().lastModified();
+				if (modifiedBefore == modifiedAfter) {
+					outw.println(MessageFormat.format(
+							CLIText.get().mergeToolFileUnchanged,
+							mergedFilePath));
+					isMergeSuccessful = !showPrompt || isMergeSuccessful();
+				}
+			}
+			// if automatically or manually successful
+			// -> add the file to the index
+			if (isMergeSuccessful) {
+				addFile(mergedFilePath);
+			}
+		} finally {
+			baseSource.close();
+			localSource.close();
+			remoteSource.close();
+		}
+		return isMergeSuccessful ? MergeResult.SUCCESSFUL : MergeResult.FAILED;
+	}
+
+	private MergeResult mergeDeleted(String mergedFilePath, boolean deletedByUs)
+			throws Exception {
+		outw.println(MessageFormat.format(CLIText.get().mergeToolFileUnchanged,
+				mergedFilePath));
+		if (deletedByUs) {
+			outw.println(CLIText.get().mergeToolDeletedConflictByUs);
+		} else {
+			outw.println(CLIText.get().mergeToolDeletedConflictByThem);
+		}
+		int mergeDecision = getDeletedMergeDecision();
+		if (mergeDecision == 1) {
+			// add modified file
+			addFile(mergedFilePath);
+		} else if (mergeDecision == -1) {
+			// remove deleted file
+			rmFile(mergedFilePath);
+		} else {
+			return MergeResult.ABORTED;
+		}
+		return MergeResult.SUCCESSFUL;
+	}
+
+	private void addFile(String fileName) throws Exception {
+		try (Git git = new Git(db)) {
+			git.add().addFilepattern(fileName).call();
+		}
+	}
+
+	private void rmFile(String fileName) throws Exception {
+		try (Git git = new Git(db)) {
+			git.rm().addFilepattern(fileName).call();
+		}
+	}
+
+	private boolean hasUserAccepted(String message) throws IOException {
+		boolean yes = true;
+		outw.print(message + " "); //$NON-NLS-1$
+		outw.flush();
+		BufferedReader br = inputReader;
+		String line = null;
+		while ((line = br.readLine()) != null) {
+			if (line.equalsIgnoreCase("y")) { //$NON-NLS-1$
+				yes = true;
+				break;
+			} else if (line.equalsIgnoreCase("n")) { //$NON-NLS-1$
+				yes = false;
+				break;
+			}
+			outw.print(message);
+			outw.flush();
+		}
+		return yes;
+	}
+
+	private boolean isContinueUnresolvedPaths() throws IOException {
+		return hasUserAccepted(CLIText.get().mergeToolContinueUnresolvedPaths);
+	}
+
+	private boolean isMergeSuccessful() throws IOException {
+		return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull);
+	}
+
+	private boolean promptForLaunch(String toolNamePrompt) {
+		try {
+			boolean launch = true;
+			outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch,
+					toolNamePrompt) + " "); //$NON-NLS-1$
+			outw.flush();
+			BufferedReader br = inputReader;
+			String line = null;
+			if ((line = br.readLine()) != null) {
+				if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$
+					launch = false;
+				}
+			}
+			return launch;
+		} catch (IOException e) {
+			throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$
+		}
+	}
+
+	private int getDeletedMergeDecision() throws IOException {
+		int ret = 0; // abort
+		final String message = CLIText.get().mergeToolDeletedMergeDecision
+				+ " "; //$NON-NLS-1$
+		outw.print(message);
+		outw.flush();
+		BufferedReader br = inputReader;
+		String line = null;
+		while ((line = br.readLine()) != null) {
+			if (line.equalsIgnoreCase("m")) { //$NON-NLS-1$
+				ret = 1; // modified
+				break;
+			} else if (line.equalsIgnoreCase("d")) { //$NON-NLS-1$
+				ret = -1; // deleted
+				break;
+			} else if (line.equalsIgnoreCase("a")) { //$NON-NLS-1$
+				break;
+			}
+			outw.print(message);
+			outw.flush();
+		}
+		return ret;
+	}
+
+	private void showToolHelp() throws IOException {
+		Map<String, ExternalMergeTool> predefTools = mergeTools
+				.getPredefinedTools(true);
+		StringBuilder availableToolNames = new StringBuilder();
+		StringBuilder notAvailableToolNames = new StringBuilder();
+		for (String name : predefTools.keySet()) {
+			if (predefTools.get(name).isAvailable()) {
+				availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
+			} else {
+				notAvailableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$
+			}
+		}
+		StringBuilder userToolNames = new StringBuilder();
+		Map<String, ExternalMergeTool> userTools = mergeTools
+				.getUserDefinedTools();
+		for (String name : userTools.keySet()) {
+			userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$
+					name, userTools.get(name).getCommand()));
+		}
+		outw.println(MessageFormat.format(
+				CLIText.get().mergeToolHelpSetToFollowing, availableToolNames,
+				userToolNames, notAvailableToolNames));
+	}
+
+	private Map<String, StageState> getFiles() throws RevisionSyntaxException,
+			NoWorkTreeException, GitAPIException {
+		Map<String, StageState> files = new TreeMap<>();
+		try (Git git = new Git(db)) {
+			StatusCommand statusCommand = git.status();
+			if (filterPaths != null && filterPaths.size() > 0) {
+				for (String path : filterPaths) {
+					statusCommand.addPath(path);
+				}
+			}
+			Status status = statusCommand.call();
+			files = status.getConflictingStageState();
+		}
+		return files;
+	}
+
+}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
index 7fe5b0f..e06f150 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
@@ -139,6 +139,8 @@
 	/***/ public String diffToolHelpSetToFollowing;
 	/***/ public String diffToolLaunch;
 	/***/ public String diffToolDied;
+	/***/ public String diffToolPromptToolName;
+	/***/ public String diffToolUnknownToolName;
 	/***/ public String doesNotExist;
 	/***/ public String dontOverwriteLocalChanges;
 	/***/ public String everythingUpToDate;
@@ -169,6 +171,24 @@
 	/***/ public String logNoSignatureVerifier;
 	/***/ public String mergeCheckoutConflict;
 	/***/ public String mergeConflict;
+	/***/ public String mergeToolHelpSetToFollowing;
+	/***/ public String mergeToolLaunch;
+	/***/ public String mergeToolDied;
+	/***/ public String mergeToolNoFiles;
+	/***/ public String mergeToolMerging;
+	/***/ public String mergeToolUnknownConflict;
+	/***/ public String mergeToolNormalConflict;
+	/***/ public String mergeToolMergeFailed;
+	/***/ public String mergeToolExecutionError;
+	/***/ public String mergeToolFileUnchanged;
+	/***/ public String mergeToolDeletedConflict;
+	/***/ public String mergeToolDeletedConflictByUs;
+	/***/ public String mergeToolDeletedConflictByThem;
+	/***/ public String mergeToolContinueUnresolvedPaths;
+	/***/ public String mergeToolWasMergeSuccessfull;
+	/***/ public String mergeToolDeletedMergeDecision;
+	/***/ public String mergeToolPromptToolName;
+	/***/ public String mergeToolUnknownToolName;
 	/***/ public String mergeFailed;
 	/***/ public String mergeCheckoutFailed;
 	/***/ public String mergeMadeBy;
diff --git a/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF
index 1a5746d..8c52337 100644
--- a/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF
@@ -2,15 +2,16 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %Bundle-Name
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.agent;singleton:=true
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
+Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Fragment-Host: org.eclipse.jgit.ssh.apache;bundle-version="[6.1.1,6.2.0)"
+Fragment-Host: org.eclipse.jgit.ssh.apache;bundle-version="[6.2.1,6.3.0)"
 Bundle-ActivationPolicy: lazy
 Automatic-Module-Name: org.eclipse.jgit.ssh.apache.agent
 Bundle-RequiredExecutionEnvironment: JavaSE-11
-Import-Package: org.eclipse.jgit.transport.sshd;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)"
+Import-Package: org.eclipse.jgit.transport.sshd;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)"
 Require-Bundle: com.sun.jna;bundle-version="[5.8.0,6.0.0)",
  com.sun.jna.platform;bundle-version="[5.8.0,6.0.0)"
-Export-Package: org.eclipse.jgit.internal.transport.sshd.agent.connector;version="6.1.1";x-internal:=true
+Export-Package: org.eclipse.jgit.internal.transport.sshd.agent.connector;version="6.2.1";x-internal:=true
diff --git a/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF
index d06e6b1..3433630 100644
--- a/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ssh.apache.agent - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.agent.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache.agent;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache.agent;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.apache.agent/pom.xml b/org.eclipse.jgit.ssh.apache.agent/pom.xml
index 77b9c95..5211055 100644
--- a/org.eclipse.jgit.ssh.apache.agent/pom.xml
+++ b/org.eclipse.jgit.ssh.apache.agent/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache.agent</artifactId>
diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java
index 7bad90f..81c6537 100644
--- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java
+++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java
@@ -90,7 +90,7 @@
 			file = libs.kernel.CreateFile(pipeName,
 					WinNT.GENERIC_READ | WinNT.GENERIC_WRITE, FILE_SHARE_NONE,
 					null, WinNT.OPEN_EXISTING, FILE_ATTRIBUTE_NONE, null);
-			if (file == null || file == WinBase.INVALID_HANDLE_VALUE) {
+			if (file == null || WinBase.INVALID_HANDLE_VALUE.equals(file)) {
 				int errorCode = libs.kernel.GetLastError();
 				if (errorCode == WinError.ERROR_FILE_NOT_FOUND
 						&& CANONICAL_PIPE_NAME.equalsIgnoreCase(pipeName)) {
diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
index 411d43c..fe75eee 100644
--- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.apache.test
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.test
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-11
@@ -21,16 +21,16 @@
  org.apache.sshd.core;version="[2.8.0,2.9.0)",
  org.apache.sshd.server;version="[2.8.0,2.9.0)",
  org.apache.sshd.server.forward;version="[2.8.0,2.9.0)",
- org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.transport.sshd.proxy;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit.ssh;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.sshd;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.sshd.agent;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.transport.sshd.proxy;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit.ssh;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.sshd;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.sshd.agent;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.hamcrest;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.ssh.apache.test/pom.xml b/org.eclipse.jgit.ssh.apache.test/pom.xml
index 3e98e33..6486999 100644
--- a/org.eclipse.jgit.ssh.apache.test/pom.xml
+++ b/org.eclipse.jgit.ssh.apache.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache.test</artifactId>
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
index 3d7c765..a8fcca7 100644
--- a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -789,4 +789,76 @@
 			session.disconnect();
 		}
 	}
+
+	private void verifyAuthLog(String message, String first) {
+		assertTrue(message.contains(System.lineSeparator()));
+		String[] lines = message.split(System.lineSeparator());
+		int pubkeyIndex = -1;
+		int passwordIndex = -1;
+		for (int i = 0; i < lines.length; i++) {
+			String line = lines[i];
+			if (i == 0) {
+				assertTrue(line.contains(first));
+			}
+			if (line.contains("publickey:")) {
+				if (pubkeyIndex < 0) {
+					pubkeyIndex = i;
+					assertTrue(line.contains("/userkey"));
+				}
+			} else if (line.contains("password:")) {
+				if (passwordIndex < 0) {
+					passwordIndex = i;
+					assertTrue(line.contains("attempt 1"));
+				}
+			}
+		}
+		assertTrue(pubkeyIndex > 0 && passwordIndex > 0);
+		assertTrue(pubkeyIndex < passwordIndex);
+	}
+
+	@Test
+	public void testAuthFailureMessageCancel() throws Exception {
+		File userKey = new File(getTemporaryDirectory(), "userkey");
+		copyTestResource("id_ed25519", userKey);
+		File publicKey = new File(getTemporaryDirectory(), "userkey.pub");
+		copyTestResource("id_ed25519.pub", publicKey);
+		// Don't set this as the user's key; we do want to try with a wrong key.
+		server.enablePasswordAuthentication();
+		TestCredentialsProvider provider = new TestCredentialsProvider(
+				"wrongpass");
+		TransportException e = assertThrows(TransportException.class,
+				() -> cloneWith("ssh://git/doesntmatter", defaultCloneDir,
+						provider, //
+						"Host git", //
+						"HostName localhost", //
+						"Port " + testPort, //
+						"User " + TEST_USER, //
+						"IdentityFile " + userKey.getAbsolutePath(), //
+						"PreferredAuthentications publickey,password"));
+		verifyAuthLog(e.getMessage(), "canceled");
+	}
+
+	@Test
+	public void testAuthFailureMessage() throws Exception {
+		File userKey = new File(getTemporaryDirectory(), "userkey");
+		copyTestResource("id_ed25519", userKey);
+		File publicKey = new File(getTemporaryDirectory(), "userkey.pub");
+		copyTestResource("id_ed25519.pub", publicKey);
+		// Don't set this as the user's key; we do want to try with a wrong key.
+		server.enablePasswordAuthentication();
+		// Enough passwords not to cancel authentication
+		TestCredentialsProvider provider = new TestCredentialsProvider(
+				"wrongpass", "wrongpass", "wrongpass");
+		TransportException e = assertThrows(TransportException.class,
+				() -> cloneWith("ssh://git/doesntmatter", defaultCloneDir,
+						provider, //
+						"Host git", //
+						"HostName localhost", //
+						"Port " + testPort, //
+						"User " + TEST_USER, //
+						"IdentityFile " + userKey.getAbsolutePath(), //
+						"PreferredAuthentications publickey,password"));
+		verifyAuthLog(e.getMessage(), "log in");
+	}
+
 }
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
index 73feaec..941632d 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
@@ -6,9 +6,9 @@
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-ActivationPolicy: lazy
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-11
-Export-Package: org.eclipse.jgit.internal.transport.sshd;version="6.1.1";x-internal:=true;
+Export-Package: org.eclipse.jgit.internal.transport.sshd;version="6.2.1";x-internal:=true;
   uses:="org.apache.sshd.client,
    org.apache.sshd.client.auth,
    org.apache.sshd.client.auth.keyboard,
@@ -23,17 +23,17 @@
    org.apache.sshd.common.signature,
    org.apache.sshd.common.util.buffer,
    org.eclipse.jgit.transport",
- org.eclipse.jgit.internal.transport.sshd.agent;version="6.1.1";x-internal:=true,
- org.eclipse.jgit.internal.transport.sshd.auth;version="6.1.1";x-internal:=true,
- org.eclipse.jgit.internal.transport.sshd.proxy;version="6.1.1";x-friends:="org.eclipse.jgit.ssh.apache.test",
- org.eclipse.jgit.transport.sshd;version="6.1.1";
+ org.eclipse.jgit.internal.transport.sshd.agent;version="6.2.1";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.auth;version="6.2.1";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.proxy;version="6.2.1";x-friends:="org.eclipse.jgit.ssh.apache.test",
+ org.eclipse.jgit.transport.sshd;version="6.2.1";
   uses:="org.eclipse.jgit.transport,
    org.apache.sshd.client.config.hosts,
    org.apache.sshd.common.keyprovider,
    org.eclipse.jgit.util,
    org.apache.sshd.client.session,
    org.apache.sshd.client.keyverifier",
- org.eclipse.jgit.transport.sshd.agent;version="6.1.1"
+ org.eclipse.jgit.transport.sshd.agent;version="6.2.1"
 Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)",
  org.apache.sshd.agent;version="[2.8.0,2.9.0)",
  org.apache.sshd.client;version="[2.8.0,2.9.0)",
@@ -86,12 +86,12 @@
  org.apache.sshd.sftp;version="[2.8.0,2.9.0)",
  org.apache.sshd.sftp.client;version="[2.8.0,2.9.0)",
  org.apache.sshd.sftp.common;version="[2.8.0,2.9.0)",
- org.eclipse.jgit.annotations;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.fnmatch;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.annotations;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.fnmatch;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.slf4j;version="[1.7.0,2.0.0)"
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/SOURCE-MANIFEST.MF
index 9fefa6e..2c3ac20 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ssh.apache - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.apache/pom.xml b/org.eclipse.jgit.ssh.apache/pom.xml
index 674c7a4..1758431 100644
--- a/org.eclipse.jgit.ssh.apache/pom.xml
+++ b/org.eclipse.jgit.ssh.apache/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
@@ -50,16 +50,6 @@
       <groupId>org.apache.sshd</groupId>
       <artifactId>sshd-sftp</artifactId>
       <version>${apache-sshd-version}</version>
-      <exclusions>
-          <exclusion>
-              <groupId>org.apache.sshd</groupId>
-              <artifactId>sshd-core</artifactId>
-          </exclusion>
-          <exclusion>
-              <groupId>org.apache.sshd</groupId>
-              <artifactId>sshd-common</artifactId>
-          </exclusion>
-      </exclusions>
     </dependency>
 
     <dependency>
diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
index 4f735ba..c676221 100644
--- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
+++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
@@ -1,5 +1,22 @@
 authenticationCanceled=SSH authentication canceled: no password given
 authenticationOnClosedSession=Authentication canceled: session is already closing or closed
+authGssApiAttempt={0}: trying mechanism OID {1}
+authGssApiExhausted={0}: no more mechanisms to try
+authGssApiFailure={0}: server refused authentication; mechanism {1}
+authGssApiNotTried={0}: not tried
+authGssApiPartialSuccess={0}: partial success with mechanism OID {1}, continue with authentication methods {2}
+authPasswordAttempt={0}: attempt {1}
+authPasswordChangeAttempt={0}: attempt {1} with password change
+authPasswordExhausted={0}: no more attempts
+authPasswordFailure={0}: server refused (wrong password)
+authPasswordNotTried={0}: not tried
+authPasswordPartialSuccess={0}: partial success, continue with authentication methods {1}
+authPubkeyAttempt={0}: trying {1} key {2} with signature type {3}
+authPubkeyAttemptAgent={0}: trying {1} key {2} from SSH agent with signature type {3}
+authPubkeyExhausted={0}: no more keys to try
+authPubkeyFailure={0}: server refused {1} key {2}
+authPubkeyNoKeys={0}: no keys to try
+authPubkeyPartialSuccess={0}: partial success for {1} key {2}, continue with authentication methods {3}
 cannotReadPublicKey=Cannot read public key from file {0}
 closeListenerFailed=Ssh session close listener failed
 configInvalidPath=Invalid path in ssh config key {0}: {1}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java
new file mode 100644
index 0000000..add79b3
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.transport.sshd;
+
+import static org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider.getKeyId;
+
+import java.security.KeyPair;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
+import org.apache.sshd.client.auth.password.UserAuthPassword;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
+import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.config.keys.KeyUtils;
+
+/**
+ * Provides a log of authentication attempts for a {@link ClientSession}.
+ */
+public class AuthenticationLogger {
+
+	private final List<String> messages = new ArrayList<>();
+
+	// We're interested in this log only in the failure case, so we don't need
+	// to log authentication success.
+
+	private final PublicKeyAuthenticationReporter pubkeyLogger = new PublicKeyAuthenticationReporter() {
+
+		private boolean hasAttempts;
+
+		@Override
+		public void signalAuthenticationAttempt(ClientSession session,
+				String service, KeyPair identity, String signature)
+				throws Exception {
+			hasAttempts = true;
+			String message;
+			if (identity.getPrivate() == null) {
+				// SSH agent key
+				message = MessageFormat.format(
+						SshdText.get().authPubkeyAttemptAgent,
+						UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
+						getKeyId(session, identity), signature);
+			} else {
+				message = MessageFormat.format(
+						SshdText.get().authPubkeyAttempt,
+						UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
+						getKeyId(session, identity), signature);
+			}
+			messages.add(message);
+		}
+
+		@Override
+		public void signalAuthenticationExhausted(ClientSession session,
+				String service) throws Exception {
+			String message;
+			if (hasAttempts) {
+				message = MessageFormat.format(
+						SshdText.get().authPubkeyExhausted,
+						UserAuthPublicKey.NAME);
+			} else {
+				message = MessageFormat.format(SshdText.get().authPubkeyNoKeys,
+						UserAuthPublicKey.NAME);
+			}
+			messages.add(message);
+			hasAttempts = false;
+		}
+
+		@Override
+		public void signalAuthenticationFailure(ClientSession session,
+				String service, KeyPair identity, boolean partial,
+				List<String> serverMethods) throws Exception {
+			String message;
+			if (partial) {
+				message = MessageFormat.format(
+						SshdText.get().authPubkeyPartialSuccess,
+						UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
+						getKeyId(session, identity), serverMethods);
+			} else {
+				message = MessageFormat.format(
+						SshdText.get().authPubkeyFailure,
+						UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
+						getKeyId(session, identity));
+			}
+			messages.add(message);
+		}
+	};
+
+	private final PasswordAuthenticationReporter passwordLogger = new PasswordAuthenticationReporter() {
+
+		private int attempts;
+
+		@Override
+		public void signalAuthenticationAttempt(ClientSession session,
+				String service, String oldPassword, boolean modified,
+				String newPassword) throws Exception {
+			attempts++;
+			String message;
+			if (modified) {
+				message = MessageFormat.format(
+						SshdText.get().authPasswordChangeAttempt,
+						UserAuthPassword.NAME, Integer.valueOf(attempts));
+			} else {
+				message = MessageFormat.format(
+						SshdText.get().authPasswordAttempt,
+						UserAuthPassword.NAME, Integer.valueOf(attempts));
+			}
+			messages.add(message);
+		}
+
+		@Override
+		public void signalAuthenticationExhausted(ClientSession session,
+				String service) throws Exception {
+			String message;
+			if (attempts > 0) {
+				message = MessageFormat.format(
+						SshdText.get().authPasswordExhausted,
+						UserAuthPassword.NAME);
+			} else {
+				message = MessageFormat.format(
+						SshdText.get().authPasswordNotTried,
+						UserAuthPassword.NAME);
+			}
+			messages.add(message);
+			attempts = 0;
+		}
+
+		@Override
+		public void signalAuthenticationFailure(ClientSession session,
+				String service, String password, boolean partial,
+				List<String> serverMethods) throws Exception {
+			String message;
+			if (partial) {
+				message = MessageFormat.format(
+						SshdText.get().authPasswordPartialSuccess,
+						UserAuthPassword.NAME, serverMethods);
+			} else {
+				message = MessageFormat.format(
+						SshdText.get().authPasswordFailure,
+						UserAuthPassword.NAME);
+			}
+			messages.add(message);
+		}
+	};
+
+	private final GssApiWithMicAuthenticationReporter gssLogger = new GssApiWithMicAuthenticationReporter() {
+
+		private boolean hasAttempts;
+
+		@Override
+		public void signalAuthenticationAttempt(ClientSession session,
+				String service, String mechanism) {
+			hasAttempts = true;
+			String message = MessageFormat.format(
+					SshdText.get().authGssApiAttempt,
+					GssApiWithMicAuthFactory.NAME, mechanism);
+			messages.add(message);
+		}
+
+		@Override
+		public void signalAuthenticationExhausted(ClientSession session,
+				String service) {
+			String message;
+			if (hasAttempts) {
+				message = MessageFormat.format(
+						SshdText.get().authGssApiExhausted,
+						GssApiWithMicAuthFactory.NAME);
+			} else {
+				message = MessageFormat.format(
+						SshdText.get().authGssApiNotTried,
+						GssApiWithMicAuthFactory.NAME);
+			}
+			messages.add(message);
+			hasAttempts = false;
+		}
+
+		@Override
+		public void signalAuthenticationFailure(ClientSession session,
+				String service, String mechanism, boolean partial,
+				List<String> serverMethods) {
+			String message;
+			if (partial) {
+				message = MessageFormat.format(
+						SshdText.get().authGssApiPartialSuccess,
+						GssApiWithMicAuthFactory.NAME, mechanism,
+						serverMethods);
+			} else {
+				message = MessageFormat.format(
+						SshdText.get().authGssApiFailure,
+						GssApiWithMicAuthFactory.NAME, mechanism);
+			}
+			messages.add(message);
+		}
+	};
+
+	/**
+	 * Creates a new {@link AuthenticationLogger} and configures the
+	 * {@link ClientSession} to report authentication attempts through this
+	 * instance.
+	 *
+	 * @param session
+	 *            to configure
+	 */
+	public AuthenticationLogger(ClientSession session) {
+		session.setPublicKeyAuthenticationReporter(pubkeyLogger);
+		session.setPasswordAuthenticationReporter(passwordLogger);
+		session.setAttribute(
+				GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER,
+				gssLogger);
+		// TODO: keyboard-interactive? sshd 2.8.0 has no callback
+		// interface for it.
+	}
+
+	/**
+	 * Retrieves the log messages for the authentication attempts.
+	 *
+	 * @return the messages as an unmodifiable list
+	 */
+	public List<String> getLog() {
+		return Collections.unmodifiableList(messages);
+	}
+
+	/**
+	 * Drops all previously recorded log messages.
+	 */
+	public void clear() {
+		messages.clear();
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java
index 79b3637..cbd6a64 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -11,6 +11,7 @@
 
 import static java.text.MessageFormat.format;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
@@ -19,18 +20,24 @@
 import java.security.InvalidKeyException;
 import java.security.KeyPair;
 import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.concurrent.CancellationException;
 
 import javax.security.auth.DestroyFailedException;
 
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
+import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
 import org.apache.sshd.common.session.SessionContext;
 import org.apache.sshd.common.util.io.resource.IoResource;
@@ -43,6 +50,14 @@
 public class CachingKeyPairProvider extends FileKeyPairProvider
 		implements Iterable<KeyPair> {
 
+	/**
+	 * An attribute set on the {@link SessionContext} recording loaded keys by
+	 * fingerprint. This enables us to provide nicer output by showing key
+	 * paths, if possible. Users can identify key identities used easier by
+	 * filename than by fingerprint.
+	 */
+	public static final AttributeKey<Map<String, Path>> KEY_PATHS_BY_FINGERPRINT = new AttributeKey<>();
+
 	private final KeyCache cache;
 
 	/**
@@ -78,6 +93,33 @@
 		return () -> iterator(session);
 	}
 
+	static String getKeyId(ClientSession session, KeyPair identity) {
+		String fingerprint = KeyUtils.getFingerPrint(identity.getPublic());
+		Map<String, Path> registered = session
+				.getAttribute(KEY_PATHS_BY_FINGERPRINT);
+		if (registered != null) {
+			Path path = registered.get(fingerprint);
+			if (path != null) {
+				Path home = session
+						.resolveAttribute(JGitSshClient.HOME_DIRECTORY);
+				if (home != null && path.startsWith(home)) {
+					try {
+						path = home.relativize(path);
+						String pathString = path.toString();
+						if (!pathString.isEmpty()) {
+							return "~" + File.separator + pathString; //$NON-NLS-1$
+						}
+					} catch (IllegalArgumentException e) {
+						// Cannot be relativized. Ignore, and work with the
+						// original path
+					}
+				}
+				return path.toString();
+			}
+		}
+		return fingerprint;
+	}
+
 	private KeyPair loadKey(SessionContext session, Path path)
 			throws IOException, GeneralSecurityException {
 		if (!Files.exists(path)) {
@@ -123,13 +165,23 @@
 						SshdText.get().identityFileUnsupportedFormat, path));
 			}
 			KeyPair result = keys.next();
+			PublicKey pk = result.getPublic();
+			if (pk != null) {
+				Map<String, Path> registered = session
+						.getAttribute(KEY_PATHS_BY_FINGERPRINT);
+				if (registered == null) {
+					registered = new HashMap<>();
+					session.setAttribute(KEY_PATHS_BY_FINGERPRINT, registered);
+				}
+				registered.put(KeyUtils.getFingerPrint(pk), path);
+			}
 			if (keys.hasNext()) {
 				log.warn(format(SshdText.get().identityFileMultipleKeys, path));
 				keys.forEachRemaining(k -> {
-					PrivateKey pk = k.getPrivate();
-					if (pk != null) {
+					PrivateKey priv = k.getPrivate();
+					if (priv != null) {
 						try {
-							pk.destroy();
+							priv.destroy();
 						} catch (DestroyFailedException e) {
 							// Ignore
 						}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java
index c3cac0c..df01db3 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -18,6 +18,7 @@
 import java.net.UnknownHostException;
 import java.util.Collection;
 import java.util.Iterator;
+import java.util.List;
 
 import org.apache.sshd.client.auth.AbstractUserAuth;
 import org.apache.sshd.client.session.ClientSession;
@@ -71,7 +72,10 @@
 		if (context != null) {
 			close(false);
 		}
+		GssApiWithMicAuthenticationReporter reporter = session.getAttribute(
+				GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER);
 		if (!nextMechanism.hasNext()) {
+			reporter.signalAuthenticationExhausted(session, service);
 			return false;
 		}
 		state = ProtocolState.STARTED;
@@ -79,6 +83,7 @@
 		// RFC 4462 states that SPNEGO must not be used with ssh
 		while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) {
 			if (!nextMechanism.hasNext()) {
+				reporter.signalAuthenticationExhausted(session, service);
 				return false;
 			}
 			currentMechanism = nextMechanism.next();
@@ -102,6 +107,10 @@
 			state = ProtocolState.FAILED;
 			return false;
 		}
+		if (reporter != null) {
+			reporter.signalAuthenticationAttempt(session, service,
+					currentMechanism.toString());
+		}
 		Buffer buffer = session
 				.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
 		buffer.putString(session.getUsername());
@@ -246,4 +255,26 @@
 		return false;
 	}
 
+	@Override
+	public void signalAuthMethodSuccess(ClientSession session, String service,
+			Buffer buffer) throws Exception {
+		GssApiWithMicAuthenticationReporter reporter = session.getAttribute(
+				GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER);
+		if (reporter != null) {
+			reporter.signalAuthenticationSuccess(session, service,
+					currentMechanism.toString());
+		}
+	}
+
+	@Override
+	public void signalAuthMethodFailure(ClientSession session, String service,
+			boolean partial, List<String> serverMethods, Buffer buffer)
+			throws Exception {
+		GssApiWithMicAuthenticationReporter reporter = session.getAttribute(
+				GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER);
+		if (reporter != null) {
+			reporter.signalAuthenticationFailure(session, service,
+					currentMechanism.toString(), partial, serverMethods);
+		}
+	}
 }
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java
new file mode 100644
index 0000000..201a131
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.transport.sshd;
+
+import java.util.List;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
+
+/**
+ * Callback interface for recording authentication state in
+ * {@link GssApiWithMicAuthentication}.
+ */
+public interface GssApiWithMicAuthenticationReporter {
+
+	/**
+	 * An {@link AttributeKey} for a {@link ClientSession} holding the
+	 * {@link GssApiWithMicAuthenticationReporter}.
+	 */
+	static final AttributeKey<GssApiWithMicAuthenticationReporter> GSS_AUTHENTICATION_REPORTER = new AttributeKey<>();
+
+	/**
+	 * Called when a new authentication attempt is made.
+	 *
+	 * @param session
+	 *            the {@link ClientSession}
+	 * @param service
+	 *            the name of the requesting SSH service name
+	 * @param mechanism
+	 *            the OID of the mechanism used
+	 */
+	default void signalAuthenticationAttempt(ClientSession session,
+			String service, String mechanism) {
+		// nothing
+	}
+
+	/**
+	 * Called when there are no more mechanisms to try.
+	 *
+	 * @param session
+	 *            the {@link ClientSession}
+	 * @param service
+	 *            the name of the requesting SSH service name
+	 */
+	default void signalAuthenticationExhausted(ClientSession session,
+			String service) {
+		// nothing
+	}
+
+	/**
+	 * Called when authentication was succeessful.
+	 *
+	 * @param session
+	 *            the {@link ClientSession}
+	 * @param service
+	 *            the name of the requesting SSH service name
+	 * @param mechanism
+	 *            the OID of the mechanism used
+	 */
+	default void signalAuthenticationSuccess(ClientSession session,
+			String service, String mechanism) {
+		// nothing
+	}
+
+	/**
+	 * Called when the authentication was not successful.
+	 *
+	 * @param session
+	 *            the {@link ClientSession}
+	 * @param service
+	 *            the name of the requesting SSH service name
+	 * @param mechanism
+	 *            the OID of the mechanism used
+	 * @param partial
+	 *            {@code true} if authentication was partially successful,
+	 *            meaning one continues with additional authentication methods
+	 *            given by {@code serverMethods}
+	 * @param serverMethods
+	 *            the {@link List} of authentication methods that can continue
+	 */
+	default void signalAuthenticationFailure(ClientSession session,
+			String service, String mechanism, boolean partial,
+			List<String> serverMethods) {
+		// nothing
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java
index ff8caaa..33c3c60 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -11,13 +11,11 @@
 
 import static org.apache.sshd.core.CoreModuleProperties.PASSWORD_PROMPTS;
 
-import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.auth.password.UserAuthPassword;
 import org.apache.sshd.client.session.ClientSession;
 
 /**
- * A password authentication handler that uses the {@link JGitUserInteraction}
- * to ask the user for the password. It also respects the
+ * A password authentication handler that respects the
  * {@code NumberOfPasswordPrompts} ssh config.
  */
 public class JGitPasswordAuthentication extends UserAuthPassword {
@@ -35,30 +33,11 @@
 	}
 
 	@Override
-	protected boolean sendAuthDataRequest(ClientSession session, String service)
-			throws Exception {
+	protected String resolveAttemptedPassword(ClientSession session,
+			String service) throws Exception {
 		if (++attempts > maxAttempts) {
-			return false;
+			return null;
 		}
-		UserInteraction interaction = session.getUserInteraction();
-		if (!interaction.isInteractionAllowed(session)) {
-			return false;
-		}
-		String password = getPassword(session, interaction);
-		if (password == null) {
-			throw new AuthenticationCanceledException();
-		}
-		// sendPassword takes a buffer as first argument, but actually doesn't
-		// use it and creates its own buffer...
-		sendPassword(null, session, password, password);
-		return true;
-	}
-
-	private String getPassword(ClientSession session,
-			UserInteraction interaction) {
-		String[] results = interaction.interactive(session, null, null, "", //$NON-NLS-1$
-				new String[] { SshdText.get().passwordPrompt },
-				new boolean[] { false });
-		return (results == null || results.length == 0) ? null : results[0];
+		return super.resolveAttemptedPassword(session, service);
 	}
 }
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
index 71e8e61..72f0bdb 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -87,6 +87,11 @@
 	public static final AttributeKey<String> PREFERRED_AUTHENTICATIONS = new AttributeKey<>();
 
 	/**
+	 * An attribute key for the home directory.
+	 */
+	public static final AttributeKey<Path> HOME_DIRECTORY = new AttributeKey<>();
+
+	/**
 	 * An attribute key for storing an alternate local address to connect to if
 	 * a local forward from a ProxyJump ssh config is present. If set,
 	 * {@link #connect(HostConfigEntry, AttributeRepository, SocketAddress)}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
index c51a75b..2a725ea 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -120,15 +120,16 @@
 				return null;
 			}).filter(s -> s != null).toArray(String[]::new);
 		}
-		// TODO What to throw to abort the connection/authentication process?
-		// In UserAuthKeyboardInteractive.getUserResponses() it's clear that
-		// returning null is valid and signifies "an error"; we'll try the
-		// next authentication method. But if the user explicitly canceled,
-		// then we don't want to try the next methods...
-		//
-		// Probably not a serious issue with the typical order of public-key,
-		// keyboard-interactive, password.
-		return null;
+		throw new AuthenticationCanceledException();
+	}
+
+	@Override
+	public String resolveAuthPasswordAttempt(ClientSession session)
+			throws Exception {
+		String[] results = interactive(session, null, null, "", //$NON-NLS-1$
+				new String[] { SshdText.get().passwordPrompt },
+				new boolean[] { false });
+		return (results == null || results.length == 0) ? null : results[0];
 	}
 
 	@Override
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
index 19ad85c..39332d9 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -29,6 +29,23 @@
 	// @formatter:off
 	/***/ public String authenticationCanceled;
 	/***/ public String authenticationOnClosedSession;
+	/***/ public String authGssApiAttempt;
+	/***/ public String authGssApiExhausted;
+	/***/ public String authGssApiFailure;
+	/***/ public String authGssApiNotTried;
+	/***/ public String authGssApiPartialSuccess;
+	/***/ public String authPasswordAttempt;
+	/***/ public String authPasswordChangeAttempt;
+	/***/ public String authPasswordExhausted;
+	/***/ public String authPasswordFailure;
+	/***/ public String authPasswordNotTried;
+	/***/ public String authPasswordPartialSuccess;
+	/***/ public String authPubkeyAttempt;
+	/***/ public String authPubkeyAttemptAgent;
+	/***/ public String authPubkeyExhausted;
+	/***/ public String authPubkeyFailure;
+	/***/ public String authPubkeyNoKeys;
+	/***/ public String authPubkeyPartialSuccess;
 	/***/ public String closeListenerFailed;
 	/***/ public String cannotReadPublicKey;
 	/***/ public String configInvalidPath;
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
index b742f5e..b94ccc6 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -52,6 +52,8 @@
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationLogger;
 import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
 import org.eclipse.jgit.internal.transport.sshd.SshdText;
 import org.eclipse.jgit.transport.FtpChannel;
@@ -119,6 +121,7 @@
 		ClientSession resultSession = null;
 		ClientSession proxySession = null;
 		PortForwardingTracker portForward = null;
+		AuthenticationLogger authLog = null;
 		try {
 			if (!hops.isEmpty()) {
 				URIish hop = hops.remove(0);
@@ -165,6 +168,7 @@
 				resultSession.addCloseFutureListener(listener);
 			}
 			// Authentication timeout is by default 2 minutes.
+			authLog = new AuthenticationLogger(resultSession);
 			resultSession.auth().verify(resultSession.getAuthTimeout());
 			return resultSession;
 		} catch (IOException e) {
@@ -173,17 +177,34 @@
 			close(resultSession, e);
 			if (e instanceof SshException && ((SshException) e)
 					.getDisconnectCode() == SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) {
-				// Ensure the user gets to know on which URI the authentication
-				// was denied.
+				String message = format(SshdText.get().loginDenied, host,
+						Integer.toString(port));
 				throw new TransportException(target,
-						format(SshdText.get().loginDenied, host,
-								Integer.toString(port)),
-						e);
+						withAuthLog(message, authLog), e);
+			} else if (e instanceof SshException && e
+					.getCause() instanceof AuthenticationCanceledException) {
+				String message = e.getCause().getMessage();
+				throw new TransportException(target,
+						withAuthLog(message, authLog), e.getCause());
 			}
 			throw e;
+		} finally {
+			if (authLog != null) {
+				authLog.clear();
+			}
 		}
 	}
 
+	private String withAuthLog(String message, AuthenticationLogger authLog) {
+		if (authLog != null) {
+			String log = String.join(System.lineSeparator(), authLog.getLog());
+			if (!log.isEmpty()) {
+				return message + System.lineSeparator() + log;
+			}
+		}
+		return message;
+	}
+
 	private ClientSession connect(HostConfigEntry config,
 			AttributeRepository context, Duration timeout)
 			throws IOException {
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
index 58cf8e1..c792c18 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -13,6 +13,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.security.KeyPair;
 import java.time.Duration;
@@ -34,7 +35,6 @@
 import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
 import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.compression.BuiltinCompressions;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions;
@@ -44,7 +44,6 @@
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
-import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
 import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
 import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
 import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
@@ -243,6 +242,12 @@
 							JGitSshClient.PREFERRED_AUTHENTICATIONS,
 							defaultAuths);
 				}
+				try {
+					jgitClient.setAttribute(JGitSshClient.HOME_DIRECTORY,
+							home.getAbsoluteFile().toPath());
+				} catch (SecurityException | InvalidPathException e) {
+					// Ignore
+				}
 				// Other things?
 				return client;
 			});
@@ -255,13 +260,7 @@
 			if (e instanceof TransportException) {
 				throw (TransportException) e;
 			}
-			Throwable cause = e;
-			if (e instanceof SshException && e
-					.getCause() instanceof AuthenticationCanceledException) {
-				// Results in a nicer error message
-				cause = e.getCause();
-			}
-			throw new TransportException(uri, cause.getMessage(), cause);
+			throw new TransportException(uri, e.getMessage(), e);
 		}
 	}
 
diff --git a/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
index 4a08940..9007e75 100644
--- a/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
@@ -3,18 +3,18 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.jsch.test
 Bundle-SymbolicName: org.eclipse.jgit.ssh.jsch.test
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Import-Package: com.jcraft.jsch;version="[0.1.54,0.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit.ssh;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.ssh.jsch;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit.ssh;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.ssh.jsch;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
  org.hamcrest;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.ssh.jsch.test/pom.xml b/org.eclipse.jgit.ssh.jsch.test/pom.xml
index ca1e742..59db255 100644
--- a/org.eclipse.jgit.ssh.jsch.test/pom.xml
+++ b/org.eclipse.jgit.ssh.jsch.test/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.jsch.test</artifactId>
diff --git a/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
index 332d981..df1ba3b 100644
--- a/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
@@ -3,19 +3,19 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.jsch
 Bundle-SymbolicName: org.eclipse.jgit.ssh.jsch;singleton:=true
-Fragment-Host: org.eclipse.jgit;bundle-version="[6.1.1,6.2.0)"
+Fragment-Host: org.eclipse.jgit;bundle-version="[6.2.1,6.3.0)"
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-ActivationPolicy: lazy
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-11
-Export-Package: org.eclipse.jgit.transport.ssh.jsch;version="6.1.1"
+Export-Package: org.eclipse.jgit.transport.ssh.jsch;version="6.2.1"
 Import-Package: com.jcraft.jsch;version="[0.1.37,0.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util.io;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util.io;version="[6.2.1,6.3.0)",
  org.slf4j;version="[1.7.0,2.0.0)"
diff --git a/org.eclipse.jgit.ssh.jsch/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ssh.jsch/META-INF/SOURCE-MANIFEST.MF
index b56bb7c..c3eb6c8 100644
--- a/org.eclipse.jgit.ssh.jsch/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ssh.jsch - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ssh.jsch.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.jsch;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.jsch;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.jsch/pom.xml b/org.eclipse.jgit.ssh.jsch/pom.xml
index 3b6d66c..f54cc2f 100644
--- a/org.eclipse.jgit.ssh.jsch/pom.xml
+++ b/org.eclipse.jgit.ssh.jsch/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 581395d..381a719 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.test
 Bundle-SymbolicName: org.eclipse.jgit.test
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-RequiredExecutionEnvironment: JavaSE-11
@@ -16,61 +16,61 @@
  org.apache.commons.compress.compressors.gzip;version="[1.15.0,2.0)",
  org.apache.commons.compress.compressors.xz;version="[1.15.0,2.0)",
  org.assertj.core.api;version="[3.14.0,4.0.0)",
- org.eclipse.jgit.annotations;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.api.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.archive;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.attributes;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.awtui;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.blame;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.diff;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.dircache;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.events;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.fnmatch;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.gitrepo;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.hooks;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.ignore;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.ignore.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.diffmergetool;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.fsck;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.io;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.pack;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.transport.connectivity;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.transport.http;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.transport.parser;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.junit.time;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lfs;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.logging;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.merge;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.notes;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.patch;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.pgm;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.pgm.internal;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revplot;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk.filter;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.file;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.storage.pack;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.submodule;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.http;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport.resolver;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.treewalk.filter;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util.io;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util.sha1;version="[6.1.1,6.2.0)",
+ org.eclipse.jgit.annotations;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.api.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.archive;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.attributes;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.awtui;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.blame;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.diff;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.dircache;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.events;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.fnmatch;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.gitrepo;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.hooks;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.ignore;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.ignore.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.fsck;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.io;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.transport.connectivity;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.transport.http;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.transport.parser;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.junit.time;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lfs;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.logging;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.merge;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.notes;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.patch;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.pgm;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.pgm.internal;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revplot;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk.filter;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.file;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.storage.pack;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.submodule;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.http;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport.resolver;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.treewalk.filter;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util.io;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util.sha1;version="[6.2.1,6.3.0)",
  org.hamcrest;version="[1.1.0,3.0.0)",
  org.hamcrest.collection;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index 6b66919..efba3e0 100644
--- a/org.eclipse.jgit.test/pom.xml
+++ b/org.eclipse.jgit.test/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.test</artifactId>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java
index b608afa..3ec454c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java
@@ -116,6 +116,53 @@
 	}
 
 	@Test
+	public void testFetchSimpleNegativeRefSpec() throws Exception {
+		remoteGit.commit().setMessage("commit").call();
+
+		FetchResult res = git.fetch().setRemote("test")
+				.setRefSpecs("refs/heads/master:refs/heads/test",
+						"^:refs/heads/test")
+				.call();
+		assertNull(res.getTrackingRefUpdate("refs/heads/test"));
+
+		res = git.fetch().setRemote("test")
+				.setRefSpecs("refs/heads/master:refs/heads/test",
+						"^refs/heads/master")
+				.call();
+		assertNull(res.getTrackingRefUpdate("refs/heads/test"));
+	}
+
+	@Test
+	public void negativeRefSpecFilterBySource() throws Exception {
+		remoteGit.commit().setMessage("commit").call();
+		remoteGit.branchCreate().setName("test").call();
+		remoteGit.commit().setMessage("commit1").call();
+		remoteGit.branchCreate().setName("dev").call();
+
+		FetchResult res = git.fetch().setRemote("test")
+				.setRefSpecs("refs/*:refs/origins/*", "^refs/*/test")
+				.call();
+		assertNotNull(res.getTrackingRefUpdate("refs/origins/heads/master"));
+		assertNull(res.getTrackingRefUpdate("refs/origins/heads/test"));
+		assertNotNull(res.getTrackingRefUpdate("refs/origins/heads/dev"));
+	}
+
+	@Test
+	public void negativeRefSpecFilterByDestination() throws Exception {
+		remoteGit.commit().setMessage("commit").call();
+		remoteGit.branchCreate().setName("meta").call();
+		remoteGit.commit().setMessage("commit1").call();
+		remoteGit.branchCreate().setName("data").call();
+
+		FetchResult res = git.fetch().setRemote("test")
+				.setRefSpecs("refs/*:refs/secret/*", "^:refs/secret/*/meta")
+				.call();
+		assertNotNull(res.getTrackingRefUpdate("refs/secret/heads/master"));
+		assertNull(res.getTrackingRefUpdate("refs/secret/heads/meta"));
+		assertNotNull(res.getTrackingRefUpdate("refs/secret/heads/data"));
+	}
+
+	@Test
 	public void fetchAddsBranches() throws Exception {
 		final String branch1 = "b1";
 		final String branch2 = "b2";
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java
index 12ec2aa..05af175 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java
@@ -21,6 +21,8 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.util.SystemReader;
 import org.junit.Test;
 
 public class LsRemoteCommandTest extends RepositoryTestCase {
@@ -107,6 +109,20 @@
 	}
 
 	@Test
+	public void testLsRemoteWithoutLocalRepositoryUrlInsteadOf()
+			throws Exception {
+		String uri = fileUri();
+		StoredConfig userConfig = SystemReader.getInstance().getUserConfig();
+		userConfig.load();
+		userConfig.setString("url", uri, "insteadOf", "file:///foo");
+		userConfig.save();
+		Collection<Ref> refs = Git.lsRemoteRepository().setRemote("file:///foo")
+				.setHeads(true).call();
+		assertNotNull(refs);
+		assertEquals(2, refs.size());
+	}
+
+	@Test
 	public void testLsRemoteWithSymRefs() throws Exception {
 		File directory = createTempDirectory("testRepository");
 		CloneCommand command = Git.cloneRepository();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
index 64475f5..917b6c3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
@@ -36,6 +36,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.lib.Sets;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.merge.ContentMergeStrategy;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
@@ -2018,6 +2019,73 @@
 		}
 	}
 
+	@Test
+	public void testMergeConflictWithMessageAndCommentChar() throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "1\na\n3\n");
+			git.add().addFilepattern("a").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+
+			writeTrashFile("a", "1\na(side)\n3\n");
+			git.add().addFilepattern("a").call();
+			git.commit().setMessage("side").call();
+
+			checkoutBranch("refs/heads/master");
+
+			writeTrashFile("a", "1\na(main)\n3\n");
+			git.add().addFilepattern("a").call();
+			git.commit().setMessage("main").call();
+
+			StoredConfig config = db.getConfig();
+			config.setString("core", null, "commentChar", "^");
+
+			Ref sideBranch = db.exactRef("refs/heads/side");
+
+			git.merge().include(sideBranch).setStrategy(MergeStrategy.RESOLVE)
+					.setMessage("user message").call();
+
+			assertEquals("user message\n\n^ Conflicts:\n^\ta\n",
+					db.readMergeCommitMsg());
+		}
+	}
+
+	@Test
+	public void testMergeConflictWithMessageAndCommentCharAuto()
+			throws Exception {
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "1\na\n3\n");
+			git.add().addFilepattern("a").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+
+			writeTrashFile("a", "1\na(side)\n3\n");
+			git.add().addFilepattern("a").call();
+			git.commit().setMessage("side").call();
+
+			checkoutBranch("refs/heads/master");
+
+			writeTrashFile("a", "1\na(main)\n3\n");
+			git.add().addFilepattern("a").call();
+			git.commit().setMessage("main").call();
+
+			StoredConfig config = db.getConfig();
+			config.setString("core", null, "commentChar", "auto");
+
+			Ref sideBranch = db.exactRef("refs/heads/side");
+
+			git.merge().include(sideBranch).setStrategy(MergeStrategy.RESOLVE)
+					.setMessage("#user message").call();
+
+			assertEquals("#user message\n\n; Conflicts:\n;\ta\n",
+					db.readMergeCommitMsg());
+		}
+	}
+
 	private static void setExecutable(Git git, String path, boolean executable) {
 		FS.DETECTED.setExecute(
 				new File(git.getRepository().getWorkTree(), path), executable);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
index c64ff0b..d574e45 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
@@ -30,6 +30,7 @@
 
 import org.eclipse.jgit.api.MergeResult.MergeStatus;
 import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler;
+import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler2;
 import org.eclipse.jgit.api.RebaseCommand.Operation;
 import org.eclipse.jgit.api.RebaseResult.Status;
 import org.eclipse.jgit.api.errors.InvalidRebaseStepException;
@@ -46,6 +47,7 @@
 import org.eclipse.jgit.events.ListenerHandle;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -56,6 +58,7 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevSort;
@@ -3410,6 +3413,99 @@
 
 	}
 
+	@Test
+	public void testInteractiveRebaseSquashFixupSequence() throws Exception {
+		// create file1, add and commit
+		writeTrashFile(FILE1, "file1");
+		git.add().addFilepattern(FILE1).call();
+		git.commit().setMessage("commit1").call();
+
+		// modify file1, add and commit
+		writeTrashFile(FILE1, "modified file1");
+		git.add().addFilepattern(FILE1).call();
+		git.commit().setMessage("commit2").call();
+
+		// modify file1, add and commit
+		writeTrashFile(FILE1, "modified file1 a second time");
+		git.add().addFilepattern(FILE1).call();
+		// Make it difficult; use git standard comment characters in the commit
+		// messages
+		git.commit().setMessage("#commit3").call();
+
+		// modify file1, add and commit
+		writeTrashFile(FILE1, "modified file1 a third time");
+		git.add().addFilepattern(FILE1).call();
+		git.commit().setMessage("@commit4").call();
+
+		// modify file1, add and commit
+		writeTrashFile(FILE1, "modified file1 a fourth time");
+		git.add().addFilepattern(FILE1).call();
+		git.commit().setMessage(";commit5").call();
+
+		StoredConfig config = git.getRepository().getConfig();
+		config.setString("core", null, "commentChar", "auto");
+		// With "auto", we should end up with '@' being used as comment
+		// character (commit4 is skipped, so it should not advance the
+		// character).
+		RebaseResult result = git.rebase().setUpstream("HEAD~4")
+				.runInteractively(new InteractiveHandler2() {
+
+					@Override
+					public void prepareSteps(List<RebaseTodoLine> steps) {
+						try {
+							steps.get(0).setAction(Action.PICK);
+							steps.get(1).setAction(Action.SQUASH);
+							steps.get(2).setAction(Action.FIXUP);
+							steps.get(3).setAction(Action.SQUASH);
+						} catch (IllegalTodoFileModification e) {
+							fail("unexpected exception: " + e);
+						}
+					}
+
+					@Override
+					public String modifyCommitMessage(String commit) {
+						fail("should not be called");
+						return commit;
+					}
+
+					@Override
+					public ModifyResult editCommitMessage(String message,
+							CleanupMode mode, char commentChar) {
+						assertEquals('@', commentChar);
+						assertEquals("@ This is a combination of 4 commits.\n"
+								+ "@ The first commit's message is:\n"
+								+ "commit2\n"
+								+ "@ This is the 2nd commit message:\n"
+								+ "#commit3\n"
+								+ "@ The 3rd commit message will be skipped:\n"
+								+ "@ @commit4\n"
+								+ "@ This is the 4th commit message:\n"
+								+ ";commit5", message);
+						return new ModifyResult() {
+
+							@Override
+							public String getMessage() {
+								return message;
+							}
+
+							@Override
+							public CleanupMode getCleanupMode() {
+								return mode;
+							}
+
+							@Override
+							public boolean shouldAddChangeId() {
+								return false;
+							}
+						};
+					}
+				}).call();
+		assertEquals(Status.OK, result.getStatus());
+		Iterator<RevCommit> logIterator = git.log().all().call().iterator();
+		String actualCommitMsg = logIterator.next().getFullMessage();
+		assertEquals("commit2\n#commit3\n;commit5", actualCommitMsg);
+	}
+
 	private File getTodoFile() {
 		File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO);
 		return todoFile;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
index c9ebec7..f69a179 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java
@@ -10,21 +10,36 @@
 package org.eclipse.jgit.internal.diffmergetool;
 
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS.ExecutionResult;
 import org.junit.Test;
 
 /**
@@ -32,20 +47,119 @@
  */
 public class ExternalDiffToolTest extends ExternalToolTestCase {
 
+	@Test(expected = ToolException.class)
+	public void testUserToolWithError() throws Exception {
+		String toolName = "customTool";
+
+		int errorReturnCode = 1;
+		String command = "exit " + errorReturnCode;
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+
+		invokeCompare(toolName);
+
+		fail("Expected exception to be thrown due to external tool exiting with error code: "
+				+ errorReturnCode);
+	}
+
+	@Test(expected = ToolException.class)
+	public void testUserToolWithCommandNotFoundError() throws Exception {
+		String toolName = "customTool";
+
+		int errorReturnCode = 127; // command not found
+		String command = "exit " + errorReturnCode;
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+
+		invokeCompare(toolName);
+		fail("Expected exception to be thrown due to external tool exiting with error code: "
+				+ errorReturnCode);
+	}
+
 	@Test
-	public void testToolNames() {
+	public void testUserDefinedTool() throws Exception {
+		String command = getEchoCommand();
+
+		FileBasedConfig config = db.getConfig();
+		String customToolName = "customTool";
+		config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+				CONFIG_KEY_CMD, command);
+
 		DiffTools manager = new DiffTools(db);
-		Set<String> actualToolNames = manager.getToolNames();
-		Set<String> expectedToolNames = Collections.emptySet();
-		assertEquals("Incorrect set of external diff tool names",
-				expectedToolNames, actualToolNames);
+
+		Map<String, ExternalDiffTool> tools = manager.getUserDefinedTools();
+		ExternalDiffTool externalTool = tools.get(customToolName);
+		boolean trustExitCode = true;
+		manager.compare(local, remote, externalTool, trustExitCode);
+
+		assertEchoCommandHasCorrectOutput();
+	}
+
+	@Test
+	public void testUserDefinedToolWithPrompt() throws Exception {
+		String command = getEchoCommand();
+
+		FileBasedConfig config = db.getConfig();
+		String customToolName = "customTool";
+		config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+				CONFIG_KEY_CMD, command);
+
+		DiffTools manager = new DiffTools(db);
+
+		PromptHandler promptHandler = PromptHandler.acceptPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		manager.compare(local, remote, Optional.of(customToolName),
+				BooleanTriState.TRUE, false, BooleanTriState.TRUE,
+				promptHandler, noToolHandler);
+
+		assertEchoCommandHasCorrectOutput();
+
+		List<String> actualToolPrompts = promptHandler.toolPrompts;
+		List<String> expectedToolPrompts = Arrays.asList("customTool");
+		assertEquals("Expected a user prompt for custom tool call",
+				expectedToolPrompts, actualToolPrompts);
+
+		assertEquals("Expected to no informing about missing tools",
+				Collections.EMPTY_LIST, noToolHandler.missingTools);
+	}
+
+	@Test
+	public void testUserDefinedToolWithCancelledPrompt() throws Exception {
+		String command = getEchoCommand();
+
+		FileBasedConfig config = db.getConfig();
+		String customToolName = "customTool";
+		config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+				CONFIG_KEY_CMD, command);
+
+		DiffTools manager = new DiffTools(db);
+
+		PromptHandler promptHandler = PromptHandler.cancelPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		Optional<ExecutionResult> result = manager.compare(local, remote,
+				Optional.of(customToolName), BooleanTriState.TRUE, false,
+				BooleanTriState.TRUE, promptHandler, noToolHandler);
+		assertFalse("Expected no result if user cancels the operation",
+				result.isPresent());
 	}
 
 	@Test
 	public void testAllTools() {
+		FileBasedConfig config = db.getConfig();
+		String customToolName = "customTool";
+		config.setString(CONFIG_DIFFTOOL_SECTION, customToolName,
+				CONFIG_KEY_CMD, "echo");
+
 		DiffTools manager = new DiffTools(db);
-		Set<String> actualToolNames = manager.getAvailableTools().keySet();
+		Set<String> actualToolNames = manager.getAllToolNames();
 		Set<String> expectedToolNames = new LinkedHashSet<>();
+		expectedToolNames.add(customToolName);
 		CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
 		for (CommandLineDiffTool defaultTool : defaultTools) {
 			String toolName = defaultTool.name();
@@ -86,11 +200,11 @@
 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
 				CONFIG_KEY_PATH, "/usr/bin/echo");
 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
-				CONFIG_KEY_PROMPT, "--no-prompt");
+				CONFIG_KEY_PROMPT, String.valueOf(false));
 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
-				CONFIG_KEY_GUITOOL, "--no-gui");
+				CONFIG_KEY_GUITOOL, String.valueOf(false));
 		config.setString(CONFIG_DIFFTOOL_SECTION, customToolname,
-				CONFIG_KEY_TRUST_EXIT_CODE, "--no-trust-exit-code");
+				CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false));
 		DiffTools manager = new DiffTools(db);
 		Set<String> actualToolNames = manager.getUserDefinedTools().keySet();
 		Set<String> expectedToolNames = new LinkedHashSet<>();
@@ -100,59 +214,240 @@
 	}
 
 	@Test
-	public void testNotAvailableTools() {
-		DiffTools manager = new DiffTools(db);
-		Set<String> actualToolNames = manager.getNotAvailableTools().keySet();
-		Set<String> expectedToolNames = Collections.emptySet();
-		assertEquals("Incorrect set of not available external diff tools",
-				expectedToolNames, actualToolNames);
-	}
+	public void testCompare() throws ToolException {
+		String toolName = "customTool";
 
-	@Test
-	public void testCompare() {
-		DiffTools manager = new DiffTools(db);
+		FileBasedConfig config = db.getConfig();
+		// the default diff tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
+				toolName);
 
-		String newPath = "";
-		String oldPath = "";
-		String newId = "";
-		String oldId = "";
-		String toolName = "";
-		BooleanTriState prompt = BooleanTriState.UNSET;
-		BooleanTriState gui = BooleanTriState.UNSET;
-		BooleanTriState trustExitCode = BooleanTriState.UNSET;
+		String command = getEchoCommand();
 
+		config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+		Optional<ExecutionResult> result = invokeCompare(toolName);
+		assertTrue("Expected external diff tool result to be available",
+				result.isPresent());
 		int expectedCompareResult = 0;
-		int compareResult = manager.compare(newPath, oldPath, newId, oldId,
-				toolName, prompt, gui, trustExitCode);
 		assertEquals("Incorrect compare result for external diff tool",
-				expectedCompareResult, compareResult);
+				expectedCompareResult, result.get().getRc());
 	}
 
 	@Test
 	public void testDefaultTool() throws Exception {
+		String toolName = "customTool";
+		String guiToolName = "customGuiTool";
+
+		FileBasedConfig config = db.getConfig();
+		// the default diff tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL,
+				toolName);
+
+		DiffTools manager = new DiffTools(db);
+		boolean gui = false;
+		String defaultToolName = manager.getDefaultToolName(gui);
+		assertEquals(
+				"Expected configured difftool to be the default external diff tool",
+				toolName, defaultToolName);
+
+		gui = true;
+		String defaultGuiToolName = manager.getDefaultToolName(gui);
+		assertEquals(
+				"Expected default gui difftool to be the default tool if no gui tool is set",
+				toolName, defaultGuiToolName);
+
+		config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL,
+				guiToolName);
+		manager = new DiffTools(db);
+		defaultGuiToolName = manager.getDefaultToolName(gui);
+		assertEquals(
+				"Expected configured difftool to be the default external diff guitool",
+				guiToolName, defaultGuiToolName);
+	}
+
+	@Test
+	public void testOverridePreDefinedToolPath() {
+		String newToolPath = "/tmp/path/";
+
+		CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values();
+		assertTrue("Expected to find pre-defined external diff tools",
+				defaultTools.length > 0);
+
+		CommandLineDiffTool overridenTool = defaultTools[0];
+		String overridenToolName = overridenTool.name();
+		String overridenToolPath = newToolPath + overridenToolName;
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_DIFFTOOL_SECTION, overridenToolName,
+				CONFIG_KEY_PATH, overridenToolPath);
+
+		DiffTools manager = new DiffTools(db);
+		Map<String, ExternalDiffTool> availableTools = manager
+				.getPredefinedTools(true);
+		ExternalDiffTool externalDiffTool = availableTools
+				.get(overridenToolName);
+		String actualDiffToolPath = externalDiffTool.getPath();
+		assertEquals(
+				"Expected pre-defined external diff tool to have overriden path",
+				overridenToolPath, actualDiffToolPath);
+		String expectedDiffToolCommand = overridenToolPath + " "
+				+ overridenTool.getParameters();
+		String actualDiffToolCommand = externalDiffTool.getCommand();
+		assertEquals(
+				"Expected pre-defined external diff tool to have overriden command",
+				expectedDiffToolCommand, actualDiffToolCommand);
+	}
+
+	@Test(expected = ToolException.class)
+	public void testUndefinedTool() throws Exception {
+		String toolName = "undefined";
+		invokeCompare(toolName);
+		fail("Expected exception to be thrown due to not defined external diff tool");
+	}
+
+	@Test
+	public void testDefaultToolExecutionWithPrompt() throws Exception {
 		FileBasedConfig config = db.getConfig();
 		// the default diff tool is configured without a subsection
 		String subsection = null;
 		config.setString("diff", subsection, "tool", "customTool");
 
+		String command = getEchoCommand();
+
+		config.setString("difftool", "customTool", "cmd", command);
+
 		DiffTools manager = new DiffTools(db);
-		BooleanTriState gui = BooleanTriState.UNSET;
+
+		PromptHandler promptHandler = PromptHandler.acceptPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		manager.compare(local, remote, Optional.empty(), BooleanTriState.TRUE,
+				false, BooleanTriState.TRUE, promptHandler, noToolHandler);
+
+		assertEchoCommandHasCorrectOutput();
+	}
+
+	@Test
+	public void testNoDefaultToolName() {
+		DiffTools manager = new DiffTools(db);
+		boolean gui = false;
 		String defaultToolName = manager.getDefaultToolName(gui);
-		assertEquals(
-				"Expected configured difftool to be the default external diff tool",
-				"my_default_toolname", defaultToolName);
+		assertNull("Expected no default tool when none is configured",
+				defaultToolName);
 
-		gui = BooleanTriState.TRUE;
-		String defaultGuiToolName = manager.getDefaultToolName(gui);
-		assertEquals(
-				"Expected configured difftool to be the default external diff tool",
-				"my_gui_tool", defaultGuiToolName);
+		gui = true;
+		defaultToolName = manager.getDefaultToolName(gui);
+		assertNull("Expected no default tool when none is configured",
+				defaultToolName);
+	}
 
-		config.setString("diff", subsection, "guitool", "customGuiTool");
-		manager = new DiffTools(db);
-		defaultGuiToolName = manager.getDefaultToolName(gui);
-		assertEquals(
-				"Expected configured difftool to be the default external diff guitool",
-				"my_gui_tool", defaultGuiToolName);
+	@Test
+	public void testExternalToolInGitAttributes() throws Exception {
+		String content = "attributes:\n*.txt 		difftool=customTool";
+		File gitattributes = writeTrashFile(".gitattributes", content);
+		gitattributes.deleteOnExit();
+		try (TestRepository<Repository> testRepository = new TestRepository<>(
+				db)) {
+			FileBasedConfig config = db.getConfig();
+			config.setString("difftool", "customTool", "cmd", "echo");
+			testRepository.git().add().addFilepattern(localFile.getName())
+					.call();
+
+			testRepository.git().add().addFilepattern(".gitattributes").call();
+
+			testRepository.branch("master").commit().message("first commit")
+					.create();
+
+			DiffTools manager = new DiffTools(db);
+			Optional<String> tool = manager
+					.getExternalToolFromAttributes(localFile.getName());
+			assertTrue("Failed to find user defined tool", tool.isPresent());
+			assertEquals("Failed to find user defined tool", "customTool",
+					tool.get());
+		} finally {
+			Files.delete(gitattributes.toPath());
+		}
+	}
+
+	@Test
+	public void testNotExternalToolInGitAttributes() throws Exception {
+		String content = "";
+		File gitattributes = writeTrashFile(".gitattributes", content);
+		gitattributes.deleteOnExit();
+		try (TestRepository<Repository> testRepository = new TestRepository<>(
+				db)) {
+			FileBasedConfig config = db.getConfig();
+			config.setString("difftool", "customTool", "cmd", "echo");
+			testRepository.git().add().addFilepattern(localFile.getName())
+					.call();
+
+			testRepository.git().add().addFilepattern(".gitattributes").call();
+
+			testRepository.branch("master").commit().message("first commit")
+					.create();
+
+			DiffTools manager = new DiffTools(db);
+			Optional<String> tool = manager
+					.getExternalToolFromAttributes(localFile.getName());
+			assertFalse(
+					"Expected no external tool if no default tool is specified in .gitattributes",
+					tool.isPresent());
+		} finally {
+			Files.delete(gitattributes.toPath());
+		}
+	}
+
+	@Test(expected = ToolException.class)
+	public void testNullTool() throws Exception {
+		DiffTools manager = new DiffTools(db);
+
+		boolean trustExitCode = true;
+		ExternalDiffTool tool = null;
+		manager.compare(local, remote, tool, trustExitCode);
+	}
+
+	@Test(expected = ToolException.class)
+	public void testNullToolWithPrompt() throws Exception {
+		DiffTools manager = new DiffTools(db);
+
+		PromptHandler promptHandler = PromptHandler.cancelPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		Optional<String> tool = null;
+		manager.compare(local, remote, tool, BooleanTriState.TRUE, false,
+				BooleanTriState.TRUE, promptHandler, noToolHandler);
+	}
+
+	private Optional<ExecutionResult> invokeCompare(String toolName)
+			throws ToolException {
+		DiffTools manager = new DiffTools(db);
+
+		BooleanTriState prompt = BooleanTriState.UNSET;
+		boolean gui = false;
+		BooleanTriState trustExitCode = BooleanTriState.TRUE;
+		PromptHandler promptHandler = PromptHandler.acceptPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		Optional<ExecutionResult> result = manager.compare(local, remote,
+				Optional.of(toolName), prompt, gui, trustExitCode,
+				promptHandler, noToolHandler);
+		return result;
+	}
+
+	private String getEchoCommand() {
+		return "(echo \"$LOCAL\" \"$REMOTE\") > "
+				+ commandResult.getAbsolutePath();
+	}
+
+	private void assertEchoCommandHasCorrectOutput() throws IOException {
+		List<String> actualLines = Files.readAllLines(commandResult.toPath());
+		String actualContent = String.join(System.lineSeparator(), actualLines);
+		actualLines = Arrays.asList(actualContent.split(" "));
+		List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
+				remoteFile.getAbsolutePath());
+		assertEquals("Dummy test tool called with unexpected arguments",
+				expectedLines, actualLines);
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
new file mode 100644
index 0000000..94b67b3
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2020-2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.diffmergetool;
+
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.junit.Test;
+
+/**
+ * Testing external merge tools.
+ */
+public class ExternalMergeToolTest extends ExternalToolTestCase {
+
+	@Test(expected = ToolException.class)
+	public void testUserToolWithError() throws Exception {
+		String toolName = "customTool";
+
+		int errorReturnCode = 1;
+		String command = "exit " + errorReturnCode;
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName,
+				CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE));
+
+		invokeMerge(toolName);
+
+		fail("Expected exception to be thrown due to external tool exiting with error code: "
+				+ errorReturnCode);
+	}
+
+	@Test(expected = ToolException.class)
+	public void testUserToolWithCommandNotFoundError() throws Exception {
+		String toolName = "customTool";
+
+		int errorReturnCode = 127; // command not found
+		String command = "exit " + errorReturnCode;
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+
+		invokeMerge(toolName);
+
+		fail("Expected exception to be thrown due to external tool exiting with error code: "
+				+ errorReturnCode);
+	}
+
+	@Test
+	public void testKdiff3() throws Exception {
+		assumePosixPlatform();
+
+		CommandLineMergeTool autoMergingTool = CommandLineMergeTool.kdiff3;
+		assumeMergeToolIsAvailable(autoMergingTool);
+
+		CommandLineMergeTool tool = autoMergingTool;
+		PreDefinedMergeTool externalTool = new PreDefinedMergeTool(tool.name(),
+				tool.getPath(), tool.getParameters(true),
+				tool.getParameters(false),
+				tool.isExitCodeTrustable() ? BooleanTriState.TRUE
+						: BooleanTriState.FALSE);
+
+		MergeTools manager = new MergeTools(db);
+		ExecutionResult result = manager.merge(local, remote, merged, null,
+				null, externalTool);
+		assertEquals("Expected merge tool to succeed", 0, result.getRc());
+
+		List<String> actualLines = Files.readAllLines(mergedFile.toPath());
+		String actualMergeResult = String.join(System.lineSeparator(),
+				actualLines);
+		String expectedMergeResult = DEFAULT_CONTENT;
+		assertEquals(
+				"Failed to merge equal local and remote versions with pre-defined tool: "
+						+ tool.getPath(),
+				expectedMergeResult, actualMergeResult);
+	}
+
+	@Test
+	public void testUserDefinedTool() throws Exception {
+		String customToolName = "customTool";
+		String command = getEchoCommand();
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
+				CONFIG_KEY_CMD, command);
+
+		MergeTools manager = new MergeTools(db);
+		Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools();
+		ExternalMergeTool externalTool = tools.get(customToolName);
+		manager.merge(local, remote, merged, base, null, externalTool);
+
+		assertEchoCommandHasCorrectOutput();
+	}
+
+	@Test
+	public void testUserDefinedToolWithPrompt() throws Exception {
+		String customToolName = "customTool";
+		String command = getEchoCommand();
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
+				CONFIG_KEY_CMD, command);
+
+		MergeTools manager = new MergeTools(db);
+
+		PromptHandler promptHandler = PromptHandler.acceptPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		manager.merge(local, remote, merged, base, null,
+				Optional.of(customToolName), BooleanTriState.TRUE, false,
+				promptHandler, noToolHandler);
+
+		assertEchoCommandHasCorrectOutput();
+
+		List<String> actualToolPrompts = promptHandler.toolPrompts;
+		List<String> expectedToolPrompts = Arrays.asList("customTool");
+		assertEquals("Expected a user prompt for custom tool call",
+				expectedToolPrompts, actualToolPrompts);
+
+		assertEquals("Expected to no informing about missing tools",
+				Collections.EMPTY_LIST, noToolHandler.missingTools);
+	}
+
+	@Test
+	public void testUserDefinedToolWithCancelledPrompt() throws Exception {
+		MergeTools manager = new MergeTools(db);
+
+		PromptHandler promptHandler = PromptHandler.cancelPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		Optional<ExecutionResult> result = manager.merge(local, remote, merged,
+				base, null, Optional.empty(), BooleanTriState.TRUE, false,
+				promptHandler, noToolHandler);
+		assertFalse("Expected no result if user cancels the operation",
+				result.isPresent());
+	}
+
+	@Test
+	public void testAllTools() {
+		FileBasedConfig config = db.getConfig();
+		String customToolName = "customTool";
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolName,
+				CONFIG_KEY_CMD, "echo");
+
+		MergeTools manager = new MergeTools(db);
+		Set<String> actualToolNames = manager.getAllToolNames();
+		Set<String> expectedToolNames = new LinkedHashSet<>();
+		expectedToolNames.add(customToolName);
+		CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
+		for (CommandLineMergeTool defaultTool : defaultTools) {
+			String toolName = defaultTool.name();
+			expectedToolNames.add(toolName);
+		}
+		assertEquals("Incorrect set of external merge tools", expectedToolNames,
+				actualToolNames);
+	}
+
+	@Test
+	public void testOverridePredefinedToolPath() {
+		String toolName = CommandLineMergeTool.guiffy.name();
+		String customToolPath = "/usr/bin/echo";
+
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				"echo");
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PATH,
+				customToolPath);
+
+		MergeTools manager = new MergeTools(db);
+		Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools();
+		ExternalMergeTool mergeTool = tools.get(toolName);
+		assertNotNull("Expected tool \"" + toolName + "\" to be user defined",
+				mergeTool);
+
+		String toolPath = mergeTool.getPath();
+		assertEquals("Expected external merge tool to have an overriden path",
+				customToolPath, toolPath);
+	}
+
+	@Test
+	public void testUserDefinedTools() {
+		FileBasedConfig config = db.getConfig();
+		String customToolname = "customTool";
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_CMD, "echo");
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_PATH, "/usr/bin/echo");
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_PROMPT, String.valueOf(false));
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_GUITOOL, String.valueOf(false));
+		config.setString(CONFIG_MERGETOOL_SECTION, customToolname,
+				CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false));
+		MergeTools manager = new MergeTools(db);
+		Set<String> actualToolNames = manager.getUserDefinedTools().keySet();
+		Set<String> expectedToolNames = new LinkedHashSet<>();
+		expectedToolNames.add(customToolname);
+		assertEquals("Incorrect set of external merge tools", expectedToolNames,
+				actualToolNames);
+	}
+
+	@Test
+	public void testCompare() throws ToolException {
+		String toolName = "customTool";
+
+		FileBasedConfig config = db.getConfig();
+		// the default merge tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
+				toolName);
+
+		String command = getEchoCommand();
+
+		config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD,
+				command);
+
+		Optional<ExecutionResult> result = invokeMerge(toolName);
+		assertTrue("Expected external merge tool result to be available",
+				result.isPresent());
+		int expectedCompareResult = 0;
+		assertEquals("Incorrect compare result for external merge tool",
+				expectedCompareResult, result.get().getRc());
+	}
+
+	@Test
+	public void testDefaultTool() throws Exception {
+		String toolName = "customTool";
+		String guiToolName = "customGuiTool";
+
+		FileBasedConfig config = db.getConfig();
+		// the default merge tool is configured without a subsection
+		String subsection = null;
+		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL,
+				toolName);
+
+		MergeTools manager = new MergeTools(db);
+		boolean gui = false;
+		String defaultToolName = manager.getDefaultToolName(gui);
+		assertEquals(
+				"Expected configured mergetool to be the default external merge tool",
+				toolName, defaultToolName);
+
+		gui = true;
+		String defaultGuiToolName = manager.getDefaultToolName(gui);
+		assertNull("Expected default mergetool to not be set",
+				defaultGuiToolName);
+
+		config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL,
+				guiToolName);
+		manager = new MergeTools(db);
+		defaultGuiToolName = manager.getDefaultToolName(gui);
+		assertEquals(
+				"Expected configured mergetool to be the default external merge guitool",
+				guiToolName, defaultGuiToolName);
+	}
+
+	@Test
+	public void testOverridePreDefinedToolPath() {
+		String newToolPath = "/tmp/path/";
+
+		CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values();
+		assertTrue("Expected to find pre-defined external merge tools",
+				defaultTools.length > 0);
+
+		CommandLineMergeTool overridenTool = defaultTools[0];
+		String overridenToolName = overridenTool.name();
+		String overridenToolPath = newToolPath + overridenToolName;
+		FileBasedConfig config = db.getConfig();
+		config.setString(CONFIG_MERGETOOL_SECTION, overridenToolName,
+				CONFIG_KEY_PATH, overridenToolPath);
+
+		MergeTools manager = new MergeTools(db);
+		Map<String, ExternalMergeTool> availableTools = manager
+				.getPredefinedTools(true);
+		ExternalMergeTool externalMergeTool = availableTools
+				.get(overridenToolName);
+		String actualMergeToolPath = externalMergeTool.getPath();
+		assertEquals(
+				"Expected pre-defined external merge tool to have overriden path",
+				overridenToolPath, actualMergeToolPath);
+		boolean withBase = true;
+		String expectedMergeToolCommand = overridenToolPath + " "
+				+ overridenTool.getParameters(withBase);
+		String actualMergeToolCommand = externalMergeTool.getCommand();
+		assertEquals(
+				"Expected pre-defined external merge tool to have overriden command",
+				expectedMergeToolCommand, actualMergeToolCommand);
+	}
+
+	@Test(expected = ToolException.class)
+	public void testUndefinedTool() throws Exception {
+		String toolName = "undefined";
+		invokeMerge(toolName);
+		fail("Expected exception to be thrown due to not defined external merge tool");
+	}
+
+	@Test
+	public void testDefaultToolExecutionWithPrompt() throws Exception {
+		FileBasedConfig config = db.getConfig();
+		// the default diff tool is configured without a subsection
+		String subsection = null;
+		config.setString("merge", subsection, "tool", "customTool");
+
+		String command = getEchoCommand();
+
+		config.setString("mergetool", "customTool", "cmd", command);
+
+		MergeTools manager = new MergeTools(db);
+
+		PromptHandler promptHandler = PromptHandler.acceptPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		manager.merge(local, remote, merged, base, null, Optional.empty(),
+				BooleanTriState.TRUE, false, promptHandler, noToolHandler);
+
+		assertEchoCommandHasCorrectOutput();
+	}
+
+	@Test
+	public void testNoDefaultToolName() {
+		MergeTools manager = new MergeTools(db);
+		boolean gui = false;
+		String defaultToolName = manager.getDefaultToolName(gui);
+		assertNull("Expected no default tool when none is configured",
+				defaultToolName);
+
+		gui = true;
+		defaultToolName = manager.getDefaultToolName(gui);
+		assertNull("Expected no default tool when none is configured",
+				defaultToolName);
+	}
+
+	@Test(expected = ToolException.class)
+	public void testNullTool() throws Exception {
+		MergeTools manager = new MergeTools(db);
+
+		PromptHandler promptHandler = null;
+		MissingToolHandler noToolHandler = null;
+
+		Optional<String> tool = null;
+
+		manager.merge(local, remote, merged, base, null, tool,
+				BooleanTriState.TRUE, false, promptHandler, noToolHandler);
+	}
+
+	@Test(expected = ToolException.class)
+	public void testNullToolWithPrompt() throws Exception {
+		MergeTools manager = new MergeTools(db);
+
+		PromptHandler promptHandler = PromptHandler.cancelPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		Optional<String> tool = null;
+
+		manager.merge(local, remote, merged, base, null, tool,
+				BooleanTriState.TRUE, false, promptHandler, noToolHandler);
+	}
+
+	private Optional<ExecutionResult> invokeMerge(String toolName)
+			throws ToolException {
+		BooleanTriState prompt = BooleanTriState.UNSET;
+		boolean gui = false;
+
+		MergeTools manager = new MergeTools(db);
+
+		PromptHandler promptHandler = PromptHandler.acceptPrompt();
+		MissingToolHandler noToolHandler = new MissingToolHandler();
+
+		Optional<ExecutionResult> result = manager.merge(local, remote, merged,
+				base, null, Optional.of(toolName), prompt, gui, promptHandler,
+				noToolHandler);
+		return result;
+	}
+
+	private void assumeMergeToolIsAvailable(
+			CommandLineMergeTool autoMergingTool) {
+		boolean isAvailable = ExternalToolUtils.isToolAvailable(db.getFS(),
+				db.getDirectory(), db.getWorkTree(), autoMergingTool.getPath());
+		assumeTrue("Assuming external tool is available: "
+				+ autoMergingTool.name(), isAvailable);
+	}
+
+	private String getEchoCommand() {
+		return "(echo $LOCAL $REMOTE $MERGED $BASE) > "
+				+ commandResult.getAbsolutePath();
+	}
+
+	private void assertEchoCommandHasCorrectOutput() throws IOException {
+		List<String> actualLines = Files.readAllLines(commandResult.toPath());
+		String actualContent = String.join(System.lineSeparator(), actualLines);
+		actualLines = Arrays.asList(actualContent.split(" "));
+		List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(),
+				remoteFile.getAbsolutePath(), mergedFile.getAbsolutePath(),
+				baseFile.getAbsolutePath());
+		assertEquals("Dummy test tool called with unexpected arguments",
+				expectedLines, actualLines);
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
index 0cc1297..7a6ff46 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java
@@ -11,6 +11,8 @@
 
 import java.io.File;
 import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
 
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.util.FS;
@@ -36,6 +38,14 @@
 
 	protected File commandResult;
 
+	protected FileElement local;
+
+	protected FileElement remote;
+
+	protected FileElement merged;
+
+	protected FileElement base;
+
 	@Before
 	@Override
 	public void setUp() throws Exception {
@@ -51,6 +61,15 @@
 		baseFile.deleteOnExit();
 		commandResult = writeTrashFile("commandResult.txt", "");
 		commandResult.deleteOnExit();
+
+		local = new FileElement(localFile.getAbsolutePath(),
+				FileElement.Type.LOCAL);
+		remote = new FileElement(remoteFile.getAbsolutePath(),
+				FileElement.Type.REMOTE);
+		merged = new FileElement(mergedFile.getAbsolutePath(),
+				FileElement.Type.MERGED);
+		base = new FileElement(baseFile.getAbsolutePath(),
+				FileElement.Type.BASE);
 	}
 
 	@After
@@ -71,4 +90,39 @@
 				"This test can run only in Linux tests",
 				FS.DETECTED instanceof FS_POSIX);
 	}
+
+	protected static class PromptHandler implements PromptContinueHandler {
+
+		private final boolean promptResult;
+
+		final List<String> toolPrompts = new ArrayList<>();
+
+		private PromptHandler(boolean promptResult) {
+			this.promptResult = promptResult;
+		}
+
+		static PromptHandler acceptPrompt() {
+			return new PromptHandler(true);
+		}
+
+		static PromptHandler cancelPrompt() {
+			return new PromptHandler(false);
+		}
+
+		@Override
+		public boolean prompt(String toolName) {
+			toolPrompts.add(toolName);
+			return promptResult;
+		}
+	}
+
+	protected static class MissingToolHandler implements InformNoToolHandler {
+
+		final List<String> missingTools = new ArrayList<>();
+
+		@Override
+		public void inform(List<String> toolNames) {
+			missingTools.addAll(toolNames);
+		}
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java
index d95d781..7066f9d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java
@@ -11,7 +11,10 @@
 package org.eclipse.jgit.lib;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
@@ -169,6 +172,82 @@
 				CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#'));
 	}
 
+	@Test
+	public void testCommentCharDefault() throws Exception {
+		CommitConfig cfg = parse("");
+		assertEquals('#', cfg.getCommentChar());
+		assertFalse(cfg.isAutoCommentChar());
+	}
+
+	@Test
+	public void testCommentCharAuto() throws Exception {
+		CommitConfig cfg = parse("[core]\n\tcommentChar = auto\n");
+		assertEquals('#', cfg.getCommentChar());
+		assertTrue(cfg.isAutoCommentChar());
+	}
+
+	@Test
+	public void testCommentCharEmpty() throws Exception {
+		CommitConfig cfg = parse("[core]\n\tcommentChar =\n");
+		assertEquals('#', cfg.getCommentChar());
+	}
+
+	@Test
+	public void testCommentCharInvalid() throws Exception {
+		CommitConfig cfg = parse("[core]\n\tcommentChar = \" \"\n");
+		assertEquals('#', cfg.getCommentChar());
+	}
+
+	@Test
+	public void testCommentCharNonAscii() throws Exception {
+		CommitConfig cfg = parse("[core]\n\tcommentChar = ö\n");
+		assertEquals('#', cfg.getCommentChar());
+	}
+
+	@Test
+	public void testCommentChar() throws Exception {
+		CommitConfig cfg = parse("[core]\n\tcommentChar = _\n");
+		assertEquals('_', cfg.getCommentChar());
+	}
+
+	@Test
+	public void testDetermineCommentChar() throws Exception {
+		String text = "A commit message\n\nBody\n";
+		assertEquals('#', CommitConfig.determineCommentChar(text));
+	}
+
+	@Test
+	public void testDetermineCommentChar2() throws Exception {
+		String text = "A commit message\n\nBody\n\n# Conflicts:\n#\tfoo.txt\n";
+		char ch = CommitConfig.determineCommentChar(text);
+		assertNotEquals('#', ch);
+		assertTrue(ch > ' ' && ch < 127);
+	}
+
+	@Test
+	public void testDetermineCommentChar3() throws Exception {
+		String text = "A commit message\n\n;Body\n\n# Conflicts:\n#\tfoo.txt\n";
+		char ch = CommitConfig.determineCommentChar(text);
+		assertNotEquals('#', ch);
+		assertNotEquals(';', ch);
+		assertTrue(ch > ' ' && ch < 127);
+	}
+
+	@Test
+	public void testDetermineCommentChar4() throws Exception {
+		String text = "A commit message\n\nBody\n\n  # Conflicts:\n\t #\tfoo.txt\n";
+		char ch = CommitConfig.determineCommentChar(text);
+		assertNotEquals('#', ch);
+		assertTrue(ch > ' ' && ch < 127);
+	}
+
+	@Test
+	public void testDetermineCommentChar5() throws Exception {
+		String text = "A commit message\n\nBody\n\n#a\n;b\n@c\n!d\n$\n%\n^\n&\n|\n:";
+		char ch = CommitConfig.determineCommentChar(text);
+		assertEquals(0, ch);
+	}
+
 	private static CommitConfig parse(String content)
 			throws ConfigInvalidException {
 		Config c = new Config();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
index b56308c..ef0817a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
@@ -443,6 +443,26 @@
 		a.setDestination("refs/remotes/origin/*/*");
 	}
 
+	@Test(expected = IllegalArgumentException.class)
+	public void invalidNegativeAndForce() {
+		assertNotNull(new RefSpec("^+refs/heads/master"));
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void invalidForceAndNegative() {
+		assertNotNull(new RefSpec("+^refs/heads/master"));
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void invalidNegativeNoSrcDest() {
+		assertNotNull(new RefSpec("^"));
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void invalidNegativeBothSrcDest() {
+		assertNotNull(new RefSpec("^refs/heads/*:refs/heads/*"));
+	}
+
 	@Test
 	public void sourceOnlywithWildcard() {
 		RefSpec a = new RefSpec("refs/heads/*",
@@ -480,4 +500,32 @@
 		assertTrue(a.isMatching());
 		assertTrue(a.isForceUpdate());
 	}
+
+	@Test
+	public void negativeRefSpecWithDest() {
+		RefSpec a = new RefSpec("^:refs/readonly/*");
+		assertTrue(a.isNegative());
+		assertNull(a.getSource());
+		assertEquals(a.getDestination(), "refs/readonly/*");
+	}
+
+	// Because of some of the API's existing behavior, without a colon at the
+	// end of the refspec, dest will be null.
+	@Test
+	public void negativeRefSpecWithSrcAndNullDest() {
+		RefSpec a = new RefSpec("^refs/testdata/*");
+		assertTrue(a.isNegative());
+		assertNull(a.getDestination());
+		assertEquals(a.getSource(), "refs/testdata/*");
+	}
+
+	// Because of some of the API's existing behavior, with a colon at the end
+	// of the refspec, dest will be empty.
+	@Test
+	public void negativeRefSpecWithSrcAndEmptyDest() {
+		RefSpec a = new RefSpec("^refs/testdata/*:");
+		assertTrue(a.isNegative());
+		assertTrue(a.getDestination().isEmpty());
+		assertEquals(a.getSource(), "refs/testdata/*");
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandInputStreamTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandInputStreamTest.java
new file mode 100644
index 0000000..7ac8319
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandInputStreamTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class SideBandInputStreamTest {
+
+	private StringWriter messages;
+
+	private SideBandInputStream sideband;
+
+	@Before
+	public void setup() {
+		messages = new StringWriter();
+	}
+
+	@Test
+	public void progressSingleCR() throws IOException {
+		init(packet("message\r"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("message\r", messages.toString());
+	}
+
+	@Test
+	public void progressSingleLF() throws IOException {
+		init(packet("message\n"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("message\n", messages.toString());
+	}
+
+	@Test
+	public void progressSingleCRLF() throws IOException {
+		init(packet("message\r\n"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("message\r\n", messages.toString());
+	}
+
+	@Test
+	public void progressMultiCR() throws IOException {
+		init(packet("message   0%\rmessage 100%\r"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\rmessage 100%\r", messages.toString());
+	}
+
+	@Test
+	public void progressMultiLF() throws IOException {
+		init(packet("message   0%\nmessage 100%\n"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\nmessage 100%\n", messages.toString());
+	}
+
+	@Test
+	public void progressMultiCRLF() throws IOException {
+		init(packet("message   0%\r\nmessage 100%\r\n"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\r\nmessage 100%\r\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartial() throws IOException {
+		init(packet("message"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("", messages.toString());
+		sideband.drainMessages();
+		assertEquals("message\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialTwoCR() throws IOException {
+		init(packet("message") + packet("message\r"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("messagemessage\r", messages.toString());
+	}
+
+	@Test
+	public void progressPartialTwoLF() throws IOException {
+		init(packet("message") + packet("message\n"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("messagemessage\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialTwoCRLF() throws IOException {
+		init(packet("message") + packet("message\r\n"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("messagemessage\r\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialThreeCR() throws IOException {
+		init(packet("message") + packet("message") + packet("message\r"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("messagemessagemessage\r", messages.toString());
+	}
+
+	@Test
+	public void progressPartialThreeLF() throws IOException {
+		init(packet("message") + packet("message") + packet("message\n"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("messagemessagemessage\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialThreeCRLF() throws IOException {
+		init(packet("message") + packet("message") + packet("message\r\n"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("messagemessagemessage\r\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialCR() throws IOException {
+		init(packet("message   0%\rmessage 100%"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\r", messages.toString());
+		sideband.drainMessages();
+		assertEquals("message   0%\rmessage 100%\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialLF() throws IOException {
+		init(packet("message   0%\nmessage 100%"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\n", messages.toString());
+		sideband.drainMessages();
+		assertEquals("message   0%\nmessage 100%\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialCRLF() throws IOException {
+		init(packet("message   0%\r\nmessage 100%"));
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\r\n", messages.toString());
+		sideband.drainMessages();
+		assertEquals("message   0%\r\nmessage 100%\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialSplitCR() throws IOException {
+		init(packet("message") + "0006\001a" + packet("   0%\rmessa")
+				+ packet("ge 100%"));
+		assertEquals('a', sideband.read());
+		assertEquals("", messages.toString());
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\r", messages.toString());
+		sideband.drainMessages();
+		assertEquals("message   0%\rmessage 100%\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialSplitLF() throws IOException {
+		init(packet("message") + "0006\001a" + packet("   0%\nmessa")
+				+ packet("ge 100%"));
+		assertEquals('a', sideband.read());
+		assertEquals("", messages.toString());
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\n", messages.toString());
+		sideband.drainMessages();
+		assertEquals("message   0%\nmessage 100%\n", messages.toString());
+	}
+
+	@Test
+	public void progressPartialSplitCRLF() throws IOException {
+		init(packet("message") + "0006\001a" + packet("   0%\r\nmessa")
+				+ packet("ge 100%"));
+		assertEquals('a', sideband.read());
+		assertEquals("", messages.toString());
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\r\n", messages.toString());
+		sideband.drainMessages();
+		assertEquals("message   0%\r\nmessage 100%\n", messages.toString());
+	}
+
+	@Test
+	public void progressInterleaved() throws IOException {
+		init(packet("message   0%\r") + "0006\001a" + packet("message  10%")
+				+ "0006\001b" + packet("\rmessage 100%\n"));
+		assertEquals('a', sideband.read());
+		assertEquals("message   0%\r", messages.toString());
+		assertEquals('b', sideband.read());
+		assertEquals("message   0%\r", messages.toString());
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\rmessage  10%\rmessage 100%\n",
+				messages.toString());
+	}
+
+	@Test
+	public void progressInterleavedPartial() throws IOException {
+		init(packet("message   0%\r") + "0006\001a" + packet("message  10%")
+				+ "0006\001b" + packet("\rmessage 100%"));
+		assertEquals('a', sideband.read());
+		assertEquals("message   0%\r", messages.toString());
+		assertEquals('b', sideband.read());
+		assertEquals("message   0%\r", messages.toString());
+		assertTrue(sideband.read() < 0);
+		assertEquals("message   0%\rmessage  10%\r", messages.toString());
+		sideband.drainMessages();
+		assertEquals("message   0%\rmessage  10%\rmessage 100%\n",
+				messages.toString());
+	}
+
+	private String packet(String data) {
+		return String.format("%04x\002%s", Integer.valueOf(data.length() + 5),
+				data);
+	}
+
+	private void init(String packets) {
+		InputStream rawIn = new ByteArrayInputStream(
+				(packets + "0000").getBytes(StandardCharsets.UTF_8));
+		sideband = new SideBandInputStream(rawIn, null, messages, null);
+	}
+}
diff --git a/org.eclipse.jgit.ui/META-INF/MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
index 7baa215..17f38b9 100644
--- a/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
@@ -4,14 +4,14 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ui
 Bundle-SymbolicName: org.eclipse.jgit.ui
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-RequiredExecutionEnvironment: JavaSE-11
-Export-Package: org.eclipse.jgit.awtui;version="6.1.1"
-Import-Package: org.eclipse.jgit.errors;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.lib;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.nls;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revplot;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.revwalk;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.transport;version="[6.1.1,6.2.0)",
- org.eclipse.jgit.util;version="[6.1.1,6.2.0)"
+Export-Package: org.eclipse.jgit.awtui;version="6.2.1"
+Import-Package: org.eclipse.jgit.errors;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.lib;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.nls;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revplot;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.revwalk;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.transport;version="[6.2.1,6.3.0)",
+ org.eclipse.jgit.util;version="[6.2.1,6.3.0)"
diff --git a/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF
index 75f3d21..5753bf9 100644
--- a/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ui - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ui.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ui;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ui;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit.ui/pom.xml b/org.eclipse.jgit.ui/pom.xml
index 125e120..ed940c6 100644
--- a/org.eclipse.jgit.ui/pom.xml
+++ b/org.eclipse.jgit.ui/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.1-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ui</artifactId>
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 811c360..8b122a3 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -8,14 +8,6 @@
             </message_arguments>
         </filter>
     </resource>
-    <resource path="src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java" type="org.eclipse.jgit.errors.NoRemoteRepositoryException">
-        <filter id="1142947843">
-            <message_arguments>
-                <message_argument value="5.13.1"/>
-                <message_argument value="NoRemoteRepositoryException(URIish, String, Throwable)"/>
-            </message_arguments>
-        </filter>
-    </resource>
     <resource path="src/org/eclipse/jgit/errors/PackMismatchException.java" type="org.eclipse.jgit.errors.PackMismatchException">
         <filter id="1142947843">
             <message_arguments>
@@ -88,14 +80,6 @@
             </message_arguments>
         </filter>
     </resource>
-    <resource path="src/org/eclipse/jgit/lib/ObjectDatabase.java" type="org.eclipse.jgit.lib.ObjectDatabase">
-        <filter id="336695337">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.lib.ObjectDatabase"/>
-                <message_argument value="getApproximateObjectCount()"/>
-            </message_arguments>
-        </filter>
-    </resource>
     <resource path="src/org/eclipse/jgit/lib/Repository.java" type="org.eclipse.jgit.lib.Repository">
         <filter id="1142947843">
             <message_arguments>
@@ -104,40 +88,6 @@
             </message_arguments>
         </filter>
     </resource>
-    <resource path="src/org/eclipse/jgit/lib/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter">
-        <filter id="403767336">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
-                <message_argument value="UNSET_INT"/>
-            </message_arguments>
-        </filter>
-        <filter id="403804204">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
-                <message_argument value="getIntInRange(Config, String, String, String, int, int, int)"/>
-            </message_arguments>
-        </filter>
-    </resource>
-    <resource path="src/org/eclipse/jgit/merge/ResolveMerger.java" type="org.eclipse.jgit.merge.ResolveMerger">
-        <filter id="338792546">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
-                <message_argument value="addCheckoutMetadata(String, Attributes)"/>
-            </message_arguments>
-        </filter>
-        <filter id="338792546">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
-                <message_argument value="addToCheckout(String, DirCacheEntry, Attributes)"/>
-            </message_arguments>
-        </filter>
-        <filter id="338792546">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
-                <message_argument value="processEntry(CanonicalTreeParser, CanonicalTreeParser, CanonicalTreeParser, DirCacheBuildIterator, WorkingTreeIterator, boolean, Attributes)"/>
-            </message_arguments>
-        </filter>
-    </resource>
     <resource path="src/org/eclipse/jgit/storage/pack/PackConfig.java" type="org.eclipse.jgit.storage.pack.PackConfig">
         <filter id="336658481">
             <message_arguments>
@@ -196,22 +146,6 @@
             </message_arguments>
         </filter>
     </resource>
-    <resource path="src/org/eclipse/jgit/transport/BasePackPushConnection.java" type="org.eclipse.jgit.transport.BasePackPushConnection">
-        <filter id="338792546">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.transport.BasePackPushConnection"/>
-                <message_argument value="noRepository()"/>
-            </message_arguments>
-        </filter>
-    </resource>
-    <resource path="src/org/eclipse/jgit/transport/PushConfig.java" type="org.eclipse.jgit.transport.PushConfig">
-        <filter id="338722907">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.transport.PushConfig"/>
-                <message_argument value="PushConfig()"/>
-            </message_arguments>
-        </filter>
-    </resource>
     <resource path="src/org/eclipse/jgit/util/HttpSupport.java" type="org.eclipse.jgit.util.HttpSupport">
         <filter id="1142947843">
             <message_arguments>
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 808d5d3..1b34912 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -3,12 +3,12 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit
 Bundle-SymbolicName: org.eclipse.jgit
-Bundle-Version: 6.1.1.qualifier
+Bundle-Version: 6.2.1.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Eclipse-ExtensibleAPI: true
-Export-Package: org.eclipse.jgit.annotations;version="6.1.1",
- org.eclipse.jgit.api;version="6.1.1";
+Export-Package: org.eclipse.jgit.annotations;version="6.2.1",
+ org.eclipse.jgit.api;version="6.2.1";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.notes,
    org.eclipse.jgit.dircache,
@@ -23,18 +23,18 @@
    org.eclipse.jgit.revwalk.filter,
    org.eclipse.jgit.blame,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.api.errors;version="6.1.1";
+ org.eclipse.jgit.api.errors;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.errors",
- org.eclipse.jgit.attributes;version="6.1.1";
+ org.eclipse.jgit.attributes;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.blame;version="6.1.1";
+ org.eclipse.jgit.blame;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.diff",
- org.eclipse.jgit.diff;version="6.1.1";
+ org.eclipse.jgit.diff;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.revwalk,
@@ -42,48 +42,49 @@
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.dircache;version="6.1.1";
+ org.eclipse.jgit.dircache;version="6.2.1";
   uses:="org.eclipse.jgit.events,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.errors;version="6.1.1";
+ org.eclipse.jgit.errors;version="6.2.1";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.internal.storage.pack",
- org.eclipse.jgit.events;version="6.1.1";
+ org.eclipse.jgit.events;version="6.2.1";
   uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.fnmatch;version="6.1.1",
- org.eclipse.jgit.gitrepo;version="6.1.1";
+ org.eclipse.jgit.fnmatch;version="6.2.1",
+ org.eclipse.jgit.gitrepo;version="6.2.1";
   uses:="org.xml.sax.helpers,
    org.eclipse.jgit.api,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.xml.sax",
- org.eclipse.jgit.gitrepo.internal;version="6.1.1";x-internal:=true,
- org.eclipse.jgit.hooks;version="6.1.1";uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.ignore;version="6.1.1",
- org.eclipse.jgit.ignore.internal;version="6.1.1";
+ org.eclipse.jgit.gitrepo.internal;version="6.2.1";x-internal:=true,
+ org.eclipse.jgit.hooks;version="6.2.1";uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.ignore;version="6.2.1",
+ org.eclipse.jgit.ignore.internal;version="6.2.1";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal;version="6.1.1";
+ org.eclipse.jgit.internal;version="6.2.1";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.http.test",
- org.eclipse.jgit.internal.diffmergetool;version="6.1.1";
+ org.eclipse.jgit.internal.diffmergetool;version="6.2.1";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.pgm.test,
-   org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.fsck;version="6.1.1";
+   org.eclipse.jgit.pgm,
+   org.eclipse.egit.ui",
+ org.eclipse.jgit.internal.fsck;version="6.2.1";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.revwalk;version="6.1.1";
+ org.eclipse.jgit.internal.revwalk;version="6.2.1";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.storage.dfs;version="6.1.1";
+ org.eclipse.jgit.internal.storage.dfs;version="6.2.1";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.http.server,
    org.eclipse.jgit.http.test,
    org.eclipse.jgit.lfs.test",
- org.eclipse.jgit.internal.storage.file;version="6.1.1";
+ org.eclipse.jgit.internal.storage.file;version="6.2.1";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.junit.http,
@@ -92,32 +93,32 @@
    org.eclipse.jgit.pgm,
    org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.ssh.apache",
- org.eclipse.jgit.internal.storage.io;version="6.1.1";
+ org.eclipse.jgit.internal.storage.io;version="6.2.1";
   x-friends:="org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.pack;version="6.1.1";
+ org.eclipse.jgit.internal.storage.pack;version="6.2.1";
   x-friends:="org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.reftable;version="6.1.1";
+ org.eclipse.jgit.internal.storage.reftable;version="6.2.1";
   x-friends:="org.eclipse.jgit.http.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.submodule;version="6.1.1";x-internal:=true,
- org.eclipse.jgit.internal.transport.connectivity;version="6.1.1";
+ org.eclipse.jgit.internal.submodule;version="6.2.1";x-internal:=true,
+ org.eclipse.jgit.internal.transport.connectivity;version="6.2.1";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.http;version="6.1.1";
+ org.eclipse.jgit.internal.transport.http;version="6.2.1";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.parser;version="6.1.1";
+ org.eclipse.jgit.internal.transport.parser;version="6.2.1";
   x-friends:="org.eclipse.jgit.http.server,
    org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.ssh;version="6.1.1";
+ org.eclipse.jgit.internal.transport.ssh;version="6.2.1";
   x-friends:="org.eclipse.jgit.ssh.apache,
    org.eclipse.jgit.ssh.jsch,
    org.eclipse.jgit.test",
- org.eclipse.jgit.lib;version="6.1.1";
+ org.eclipse.jgit.lib;version="6.2.1";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.util.sha1,
    org.eclipse.jgit.dircache,
@@ -131,11 +132,12 @@
    org.eclipse.jgit.util,
    org.eclipse.jgit.submodule,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.lib.internal;version="6.1.1";
+ org.eclipse.jgit.lib.internal;version="6.2.1";
   x-friends:="org.eclipse.jgit.test,
-   org.eclipse.jgit.pgm",
- org.eclipse.jgit.logging;version="6.1.1",
- org.eclipse.jgit.merge;version="6.1.1";
+   org.eclipse.jgit.pgm,
+   org.eclipse.egit.ui",
+ org.eclipse.jgit.logging;version="6.2.1",
+ org.eclipse.jgit.merge;version="6.2.1";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
@@ -144,40 +146,40 @@
    org.eclipse.jgit.util,
    org.eclipse.jgit.api,
    org.eclipse.jgit.attributes",
- org.eclipse.jgit.nls;version="6.1.1",
- org.eclipse.jgit.notes;version="6.1.1";
+ org.eclipse.jgit.nls;version="6.2.1",
+ org.eclipse.jgit.notes;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.patch;version="6.1.1";
+ org.eclipse.jgit.patch;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.diff",
- org.eclipse.jgit.revplot;version="6.1.1";
+ org.eclipse.jgit.revplot;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk",
- org.eclipse.jgit.revwalk;version="6.1.1";
+ org.eclipse.jgit.revwalk;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.diff,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.revwalk.filter,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.revwalk.filter;version="6.1.1";
+ org.eclipse.jgit.revwalk.filter;version="6.2.1";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.util",
- org.eclipse.jgit.storage.file;version="6.1.1";
+ org.eclipse.jgit.storage.file;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.util",
- org.eclipse.jgit.storage.pack;version="6.1.1";
+ org.eclipse.jgit.storage.pack;version="6.2.1";
   uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.submodule;version="6.1.1";
+ org.eclipse.jgit.submodule;version="6.2.1";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.diff,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.transport;version="6.1.1";
+ org.eclipse.jgit.transport;version="6.2.1";
   uses:="javax.crypto,
    org.eclipse.jgit.util.io,
    org.eclipse.jgit.lib,
@@ -190,21 +192,21 @@
    org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.storage.pack,
    org.eclipse.jgit.errors",
- org.eclipse.jgit.transport.http;version="6.1.1";
+ org.eclipse.jgit.transport.http;version="6.2.1";
   uses:="javax.net.ssl",
- org.eclipse.jgit.transport.resolver;version="6.1.1";
+ org.eclipse.jgit.transport.resolver;version="6.2.1";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.lib",
- org.eclipse.jgit.treewalk;version="6.1.1";
+ org.eclipse.jgit.treewalk;version="6.2.1";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.util",
- org.eclipse.jgit.treewalk.filter;version="6.1.1";
+ org.eclipse.jgit.treewalk.filter;version="6.2.1";
   uses:="org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util;version="6.1.1";
+ org.eclipse.jgit.util;version="6.2.1";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.hooks,
    org.eclipse.jgit.revwalk,
@@ -217,12 +219,12 @@
    org.eclipse.jgit.treewalk,
    javax.net.ssl,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.util.io;version="6.1.1";
+ org.eclipse.jgit.util.io;version="6.2.1";
   uses:="org.eclipse.jgit.attributes,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util.sha1;version="6.1.1",
- org.eclipse.jgit.util.time;version="6.1.1"
+ org.eclipse.jgit.util.sha1;version="6.2.1",
+ org.eclipse.jgit.util.time;version="6.2.1"
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
  javax.crypto,
diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
index 33e6156..4066aea 100644
--- a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit - Sources
 Bundle-SymbolicName: org.eclipse.jgit.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 6.1.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit;version="6.1.1.qualifier";roots="."
+Bundle-Version: 6.2.1.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit;version="6.2.1.qualifier";roots="."
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index f0e92b3..e223644 100644
--- a/org.eclipse.jgit/pom.xml
+++ b/org.eclipse.jgit/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>6.1.1-SNAPSHOT</version>
+    <version>6.2.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 5534a0a..84bffef 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -240,6 +240,9 @@
 deletingNotSupported=Deleting {0} not supported.
 destinationIsNotAWildcard=Destination is not a wildcard.
 detachedHeadDetected=HEAD is detached
+diffToolNotGivenError=No diff tool provided and no defaults configured.
+diffToolNotSpecifiedInGitAttributesError=Diff tool specified in git attributes cannot be found.
+diffToolNullError=Parameter for diff tool cannot be null.
 dirCacheDoesNotHaveABackingFile=DirCache does not have a backing file
 dirCacheFileIsNotLocked=DirCache {0} not locked
 dirCacheIsNotLocked=DirCache is not locked
@@ -399,6 +402,7 @@
 invalidModeFor=Invalid mode {0} for {1} {2} in {3}.
 invalidModeForPath=Invalid mode {0} for path {1}
 invalidNameContainsDotDot=Invalid name (contains ".."): {0}
+invalidNegativeAndForce= RefSpec can't be negative and forceful.
 invalidObject=Invalid {0} {1}: {2}
 invalidOldIdSent=invalid old id sent
 invalidPacketLineHeader=Invalid packet line header: {0}
@@ -462,6 +466,8 @@
 mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4}
 mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1}
 mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n  count {3}"
+mergeToolNotGivenError=No merge tool provided and no defaults configured.
+mergeToolNullError=Parameter for merge tool cannot be null.
 messageAndTaggerNotAllowedInUnannotatedTags = Unannotated tags cannot have a message or tagger
 minutesAgo={0} minutes ago
 mismatchOffset=mismatch offset for object {0}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
index f88179a..ceba89d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java
@@ -30,6 +30,7 @@
 import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitConfig;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
@@ -183,9 +184,13 @@
 
 					String message;
 					if (unmergedPaths != null) {
+						CommitConfig cfg = repo.getConfig()
+								.get(CommitConfig.KEY);
+						message = srcCommit.getFullMessage();
+						char commentChar = cfg.getCommentChar(message);
 						message = new MergeMessageFormatter()
-							.formatWithConflicts(srcCommit.getFullMessage(),
-										unmergedPaths, '#');
+								.formatWithConflicts(message, unmergedPaths,
+										commentChar);
 					} else {
 						message = srcCommit.getFullMessage();
 					}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
index 7a591aa..3b3baf5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -233,11 +233,25 @@
 				config = repo.getConfig().get(CommitConfig.KEY);
 				cleanupMode = config.resolve(cleanupMode, cleanDefaultIsStrip);
 			}
-			char comments;
-			if (commentChar == null) {
-				comments = '#'; // TODO use git config core.commentChar
-			} else {
-				comments = commentChar.charValue();
+			char comments = (char) 0;
+			if (CleanupMode.STRIP.equals(cleanupMode)
+					|| CleanupMode.SCISSORS.equals(cleanupMode)) {
+				if (commentChar == null) {
+					if (config == null) {
+						config = repo.getConfig().get(CommitConfig.KEY);
+					}
+					if (config.isAutoCommentChar()) {
+						// We're supposed to pick a character that isn't used,
+						// but then cleaning up won't remove any lines. So don't
+						// bother.
+						comments = (char) 0;
+						cleanupMode = CleanupMode.WHITESPACE;
+					} else {
+						comments = config.getCommentChar();
+					}
+				} else {
+					comments = commentChar.charValue();
+				}
 			}
 			message = CommitConfig.cleanText(message, cleanupMode, comments);
 
@@ -309,8 +323,14 @@
 	private void sign(CommitBuilder commit) throws ServiceUnavailableException,
 			CanceledException, UnsupportedSigningFormatException {
 		if (gpgSigner == null) {
-			throw new ServiceUnavailableException(
-					JGitText.get().signingServiceUnavailable);
+			gpgSigner = GpgSigner.getDefault();
+			if (gpgSigner == null) {
+				throw new ServiceUnavailableException(
+						JGitText.get().signingServiceUnavailable);
+			}
+		}
+		if (signingKey == null) {
+			signingKey = gpgConfig.getSigningKey();
 		}
 		if (gpgSigner instanceof GpgObjectSigner) {
 			((GpgObjectSigner) gpgSigner).signObject(commit,
@@ -645,12 +665,6 @@
 			signCommit = gpgConfig.isSignCommits() ? Boolean.TRUE
 					: Boolean.FALSE;
 		}
-		if (signingKey == null) {
-			signingKey = gpgConfig.getSigningKey();
-		}
-		if (gpgSigner == null) {
-			gpgSigner = GpgSigner.getDefault();
-		}
 	}
 
 	private boolean isMergeDuringRebase(RepositoryState state) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
index 0c69106..c341558 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, 2020 Christoph Brill <egore911@egore911.de> and others
+ * Copyright (C) 2011, 2022 Christoph Brill <egore911@egore911.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
@@ -9,6 +9,7 @@
  */
 package org.eclipse.jgit.api;
 
+import java.io.IOException;
 import java.net.URISyntaxException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
@@ -20,8 +21,8 @@
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.InvalidRemoteException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.NotSupportedException;
-import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
@@ -30,6 +31,8 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.Transport;
 import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.UrlConfig;
+import org.eclipse.jgit.util.SystemReader;
 
 /**
  * The ls-remote command
@@ -153,7 +156,7 @@
 
 		try (Transport transport = repo != null
 				? Transport.open(repo, remote)
-				: Transport.open(new URIish(remote))) {
+				: Transport.open(new URIish(translate(remote)))) {
 			transport.setOptionUploadPack(uploadPack);
 			configure(transport);
 			Collection<RefSpec> refSpecs = new ArrayList<>(1);
@@ -185,11 +188,16 @@
 			throw new JGitInternalException(
 					JGitText.get().exceptionCaughtDuringExecutionOfLsRemoteCommand,
 					e);
-		} catch (TransportException e) {
+		} catch (IOException | ConfigInvalidException e) {
 			throw new org.eclipse.jgit.api.errors.TransportException(
-					e.getMessage(),
-					e);
+					e.getMessage(), e);
 		}
 	}
 
+	private String translate(String uri)
+			throws IOException, ConfigInvalidException {
+		UrlConfig urls = new UrlConfig(
+				SystemReader.getInstance().getUserConfig());
+		return urls.replace(uri);
+	}
 }
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 ce068b6..ed4a534 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -34,6 +34,7 @@
 import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitConfig;
 import org.eclipse.jgit.lib.Config.ConfigEnum;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -404,8 +405,11 @@
 							MergeStatus.FAILED, mergeStrategy, lowLevelResults,
 							failingPaths, null);
 				}
+				CommitConfig cfg = repo.getConfig().get(CommitConfig.KEY);
+				char commentChar = cfg.getCommentChar(message);
 				String mergeMessageWithConflicts = new MergeMessageFormatter()
-						.formatWithConflicts(mergeMessage, unmergedPaths, '#');
+						.formatWithConflicts(mergeMessage, unmergedPaths,
+								commentChar);
 				repo.writeMergeCommitMsg(mergeMessageWithConflicts);
 				return new MergeResult(null, merger.getBaseCommitId(),
 						new ObjectId[] { headCommit.getId(),
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 2b0d8ce..4e0d9d7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -449,7 +449,8 @@
 			String oldMessage = commitToPick.getFullMessage();
 			CleanupMode mode = commitConfig.resolve(CleanupMode.DEFAULT, true);
 			boolean[] doChangeId = { false };
-			String newMessage = editCommitMessage(doChangeId, oldMessage, mode);
+			String newMessage = editCommitMessage(doChangeId, oldMessage, mode,
+					commitConfig.getCommentChar(oldMessage));
 			try (Git git = new Git(repo)) {
 				newHead = git.commit()
 						.setMessage(newMessage)
@@ -494,12 +495,12 @@
 	}
 
 	private String editCommitMessage(boolean[] doChangeId, String message,
-			@NonNull CleanupMode mode) {
+			@NonNull CleanupMode mode, char commentChar) {
 		String newMessage;
 		CommitConfig.CleanupMode cleanup;
 		if (interactiveHandler instanceof InteractiveHandler2) {
 			InteractiveHandler2.ModifyResult modification = ((InteractiveHandler2) interactiveHandler)
-					.editCommitMessage(message, mode, '#');
+					.editCommitMessage(message, mode, commentChar);
 			newMessage = modification.getMessage();
 			cleanup = modification.getCleanupMode();
 			if (CleanupMode.DEFAULT.equals(cleanup)) {
@@ -511,7 +512,7 @@
 			cleanup = CommitConfig.CleanupMode.STRIP;
 			doChangeId[0] = false;
 		}
-		return CommitConfig.cleanText(newMessage, cleanup, '#');
+		return CommitConfig.cleanText(newMessage, cleanup, commentChar);
 	}
 
 	private RebaseResult cherryPickCommit(RevCommit commitToPick)
@@ -808,8 +809,9 @@
 			if (isLast) {
 				boolean[] doChangeId = { false };
 				if (sequenceContainsSquash) {
+					char commentChar = commitMessage.charAt(0);
 					commitMessage = editCommitMessage(doChangeId, commitMessage,
-							CleanupMode.STRIP);
+							CleanupMode.STRIP, commentChar);
 				}
 				retNewHead = git.commit()
 						.setMessage(commitMessage)
@@ -829,30 +831,60 @@
 	}
 
 	@SuppressWarnings("nls")
-	private static String composeSquashMessage(boolean isSquash,
+	private String composeSquashMessage(boolean isSquash,
 			RevCommit commitToPick, String currSquashMessage, int count) {
 		StringBuilder sb = new StringBuilder();
 		String ordinal = getOrdinal(count);
-		sb.setLength(0);
-		sb.append("# This is a combination of ").append(count)
-				.append(" commits.\n");
-		// Add the previous message without header (i.e first line)
-		sb.append(currSquashMessage
-				.substring(currSquashMessage.indexOf('\n') + 1));
-		sb.append("\n");
-		if (isSquash) {
-			sb.append("# This is the ").append(count).append(ordinal)
-					.append(" commit message:\n");
-			sb.append(commitToPick.getFullMessage());
+		// currSquashMessage is always non-empty here, and the first character
+		// is the comment character used so far.
+		char commentChar = currSquashMessage.charAt(0);
+		String newMessage = commitToPick.getFullMessage();
+		if (!isSquash) {
+			sb.append(commentChar).append(" This is a combination of ")
+					.append(count).append(" commits.\n");
+			// Add the previous message without header (i.e first line)
+			sb.append(currSquashMessage
+					.substring(currSquashMessage.indexOf('\n') + 1));
+			sb.append('\n');
+			sb.append(commentChar).append(" The ").append(count).append(ordinal)
+					.append(" commit message will be skipped:\n")
+					.append(commentChar).append(' ');
+			sb.append(newMessage.replaceAll("([\n\r])",
+					"$1" + commentChar + ' '));
 		} else {
-			sb.append("# The ").append(count).append(ordinal)
-					.append(" commit message will be skipped:\n# ");
-			sb.append(commitToPick.getFullMessage().replaceAll("([\n\r])",
-					"$1# "));
+			String currentMessage = currSquashMessage;
+			if (commitConfig.isAutoCommentChar()) {
+				// Figure out a new comment character taking into account the
+				// new message
+				String cleaned = CommitConfig.cleanText(currentMessage,
+						CommitConfig.CleanupMode.STRIP, commentChar) + '\n'
+						+ newMessage;
+				char newCommentChar = commitConfig.getCommentChar(cleaned);
+				if (newCommentChar != commentChar) {
+					currentMessage = replaceCommentChar(currentMessage,
+							commentChar, newCommentChar);
+					commentChar = newCommentChar;
+				}
+			}
+			sb.append(commentChar).append(" This is a combination of ")
+					.append(count).append(" commits.\n");
+			// Add the previous message without header (i.e first line)
+			sb.append(
+					currentMessage.substring(currentMessage.indexOf('\n') + 1));
+			sb.append('\n');
+			sb.append(commentChar).append(" This is the ").append(count)
+					.append(ordinal).append(" commit message:\n");
+			sb.append(newMessage);
 		}
 		return sb.toString();
 	}
 
+	private String replaceCommentChar(String message, char oldChar,
+			char newChar) {
+		// (?m) - Switch on multi-line matching; \h - horizontal whitespace
+		return message.replaceAll("(?m)^(\\h*)" + oldChar, "$1" + newChar); //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
 	private static String getOrdinal(int count) {
 		switch (count % 10) {
 		case 1:
@@ -886,10 +918,11 @@
 
 	private void initializeSquashFixupFile(String messageFile,
 			String fullMessage) throws IOException {
-		rebaseState
-				.createFile(
-						messageFile,
-						"# This is a combination of 1 commits.\n# The first commit's message is:\n" + fullMessage); //$NON-NLS-1$);
+		char commentChar = commitConfig.getCommentChar(fullMessage);
+		rebaseState.createFile(messageFile,
+				commentChar + " This is a combination of 1 commits.\n" //$NON-NLS-1$
+						+ commentChar + " The first commit's message is:\n" //$NON-NLS-1$
+						+ fullMessage);
 	}
 
 	private String getOurCommitName() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
index db88ad8..513f579 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
@@ -30,6 +30,7 @@
 import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitConfig;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
@@ -185,9 +186,12 @@
 								MergeStatus.CONFLICTING, strategy,
 								merger.getMergeResults(), failingPaths, null);
 					if (!merger.failed() && !unmergedPaths.isEmpty()) {
+						CommitConfig config = repo.getConfig()
+								.get(CommitConfig.KEY);
+						char commentChar = config.getCommentChar(newMessage);
 						String message = new MergeMessageFormatter()
 								.formatWithConflicts(newMessage,
-										merger.getUnmergedPaths(), '#');
+										merger.getUnmergedPaths(), commentChar);
 						repo.writeRevertHead(srcCommit.getId());
 						repo.writeMergeCommitMsg(message);
 					}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java
index 1a41df3..64ff19c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, 2020 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
@@ -91,6 +91,29 @@
 	public abstract ObjectLoader open(String path, ObjectId id)
 			throws IOException;
 
+	/**
+	 * Closes the used resources like ObjectReader, TreeWalk etc. Default
+	 * implementation does nothing.
+	 *
+	 * @since 6.2
+	 */
+	public void close() {
+		// Do nothing
+	}
+
+	/**
+	 * Checks if the source is from "working tree", so it can be accessed as a
+	 * file directly.
+	 *
+	 * @since 6.2
+	 *
+	 * @return true if working tree source and false otherwise (loader must be
+	 *         used)
+	 */
+	public boolean isWorkingTreeSource() {
+		return false;
+	}
+
 	private static class ObjectReaderSource extends ContentSource {
 		private final ObjectReader reader;
 
@@ -111,6 +134,16 @@
 		public ObjectLoader open(String path, ObjectId id) throws IOException {
 			return reader.open(id, Constants.OBJ_BLOB);
 		}
+
+		@Override
+		public void close() {
+			reader.close();
+		}
+
+		@Override
+		public boolean isWorkingTreeSource() {
+			return false;
+		}
 	}
 
 	private static class WorkingTreeSource extends ContentSource {
@@ -194,6 +227,16 @@
 					throw new FileNotFoundException(path);
 			}
 		}
+
+		@Override
+		public void close() {
+			tw.close();
+		}
+
+		@Override
+		public boolean isWorkingTreeSource() {
+			return true;
+		}
 	}
 
 	/** A pair of sources to access the old and new sides of a DiffEntry. */
@@ -261,5 +304,37 @@
 				throw new IllegalArgumentException();
 			}
 		}
+
+		/**
+		 * Closes used resources.
+		 *
+		 * @since 6.2
+		 */
+		public void close() {
+			oldSource.close();
+			newSource.close();
+		}
+
+		/**
+		 * Checks if source (side) is a "working tree".
+		 *
+		 * @since 6.2
+		 *
+		 * @param side
+		 *            which side of the entry to read (OLD or NEW).
+		 * @return is the source a "working tree"
+		 *
+		 */
+		public boolean isWorkingTreeSource(DiffEntry.Side side) {
+			switch (side) {
+			case OLD:
+				return oldSource.isWorkingTreeSource();
+			case NEW:
+				return newSource.isWorkingTreeSource();
+			default:
+				throw new IllegalArgumentException();
+			}
+		}
+
 	}
 }
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 3d50a82..f6fc393 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -26,9 +26,9 @@
 import java.text.MessageFormat;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -113,7 +113,7 @@
 
 	private Repository repo;
 
-	private HashMap<String, CheckoutMetadata> updated = new HashMap<>();
+	private Map<String, CheckoutMetadata> updated = new LinkedHashMap<>();
 
 	private ArrayList<String> conflicts = new ArrayList<>();
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java
index 535c6b9..43dbc37 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 Obeo. and others
+ * Copyright (C) 2015, 2022 Obeo 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,6 +38,8 @@
 
 	private String refs;
 
+	private boolean dryRun;
+
 	/**
 	 * Constructor for PrePushHook
 	 * <p>
@@ -145,6 +147,27 @@
 	}
 
 	/**
+	 * Sets whether the push is a dry run.
+	 *
+	 * @param dryRun
+	 *            {@code true} if the push is a dry run, {@code false} otherwise
+	 * @since 6.2
+	 */
+	public void setDryRun(boolean dryRun) {
+		this.dryRun = dryRun;
+	}
+
+	/**
+	 * Tells whether the push is a dry run.
+	 *
+	 * @return {@code true} if the push is a dry run, {@code false} otherwise
+	 * @since 6.2
+	 */
+	protected boolean isDryRun() {
+		return dryRun;
+	}
+
+	/**
 	 * Set Refs
 	 *
 	 * @param toRefs
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 2adde0a..d4e3cca 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -268,6 +268,9 @@
 	/***/ public String deletingNotSupported;
 	/***/ public String destinationIsNotAWildcard;
 	/***/ public String detachedHeadDetected;
+	/***/ public String diffToolNotGivenError;
+	/***/ public String diffToolNotSpecifiedInGitAttributesError;
+	/***/ public String diffToolNullError;
 	/***/ public String dirCacheDoesNotHaveABackingFile;
 	/***/ public String dirCacheFileIsNotLocked;
 	/***/ public String dirCacheIsNotLocked;
@@ -427,6 +430,7 @@
 	/***/ public String invalidModeFor;
 	/***/ public String invalidModeForPath;
 	/***/ public String invalidNameContainsDotDot;
+	/***/ public String invalidNegativeAndForce;
 	/***/ public String invalidObject;
 	/***/ public String invalidOldIdSent;
 	/***/ public String invalidPacketLineHeader;
@@ -490,6 +494,8 @@
 	/***/ public String mergeUsingStrategyResultedInDescription;
 	/***/ public String mergeRecursiveConflictsWhenMergingCommonAncestors;
 	/***/ public String mergeRecursiveTooManyMergeBasesFor;
+	/***/ public String mergeToolNotGivenError;
+	/***/ public String mergeToolNullError;
 	/***/ public String messageAndTaggerNotAllowedInUnannotatedTags;
 	/***/ public String minutesAgo;
 	/***/ public String mismatchOffset;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
new file mode 100644
index 0000000..668adea
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.eclipse.jgit.errors.NoWorkTreeException;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.FS_POSIX;
+import org.eclipse.jgit.util.FS_Win32;
+import org.eclipse.jgit.util.FS_Win32_Cygwin;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * Runs a command with help of FS.
+ */
+public class CommandExecutor {
+
+	private FS fs;
+
+	private boolean checkExitCode;
+
+	private File commandFile;
+
+	private boolean useMsys2;
+
+	/**
+	 * @param fs
+	 *            the file system
+	 * @param checkExitCode
+	 *            should the exit code be checked for errors ?
+	 */
+	public CommandExecutor(FS fs, boolean checkExitCode) {
+		this.fs = fs;
+		this.checkExitCode = checkExitCode;
+	}
+
+	/**
+	 * @param command
+	 *            the command string
+	 * @param workingDir
+	 *            the working directory
+	 * @param env
+	 *            the environment
+	 * @return the execution result
+	 * @throws ToolException
+	 * @throws InterruptedException
+	 * @throws IOException
+	 */
+	public ExecutionResult run(String command, File workingDir,
+			Map<String, String> env)
+			throws ToolException, IOException, InterruptedException {
+		String[] commandArray = createCommandArray(command);
+		try {
+			ProcessBuilder pb = fs.runInShell(commandArray[0],
+					Arrays.copyOfRange(commandArray, 1, commandArray.length));
+			pb.directory(workingDir);
+			Map<String, String> envp = pb.environment();
+			if (env != null) {
+				envp.putAll(env);
+			}
+			ExecutionResult result = fs.execute(pb, null);
+			int rc = result.getRc();
+			if (rc != 0) {
+				boolean execError = isCommandExecutionError(rc);
+				if (checkExitCode || execError) {
+					throw new ToolException(
+							"JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$
+									+ "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$
+									+ "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$
+									+ "stderr: \n" //$NON-NLS-1$
+									+ new String(
+											result.getStderr().toByteArray()),
+							result, execError);
+				}
+			}
+			return result;
+		} finally {
+			deleteCommandArray();
+		}
+	}
+
+	/**
+	 * @param path
+	 *            the executable path
+	 * @param workingDir
+	 *            the working directory
+	 * @param env
+	 *            the environment
+	 * @return the execution result
+	 * @throws ToolException
+	 * @throws InterruptedException
+	 * @throws IOException
+	 */
+	public boolean checkExecutable(String path, File workingDir,
+			Map<String, String> env)
+			throws ToolException, IOException, InterruptedException {
+		checkUseMsys2(path);
+		String command = null;
+		if (fs instanceof FS_Win32 && !useMsys2) {
+			Path p = Paths.get(path);
+			// Win32 (and not cygwin or MSYS2) where accepts only command / exe
+			// name as parameter
+			// so check if exists and executable in this case
+			if (p.isAbsolute() && Files.isExecutable(p)) {
+				return true;
+			}
+			// try where command for all other cases
+			command = "where " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
+		} else {
+			command = "which " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$
+		}
+		boolean available = true;
+		try {
+			ExecutionResult rc = run(command, workingDir, env);
+			if (rc.getRc() != 0) {
+				available = false;
+			}
+		} catch (IOException | InterruptedException | NoWorkTreeException
+				| ToolException e) {
+			// no op: is true to not hide possible tools from user
+		}
+		return available;
+	}
+
+	private void deleteCommandArray() {
+		deleteCommandFile();
+	}
+
+	private String[] createCommandArray(String command)
+			throws ToolException, IOException {
+		String[] commandArray = null;
+		checkUseMsys2(command);
+		createCommandFile(command);
+		if (fs instanceof FS_POSIX) {
+			commandArray = new String[1];
+			commandArray[0] = commandFile.getCanonicalPath();
+		} else if (fs instanceof FS_Win32) {
+			if (useMsys2) {
+				commandArray = new String[3];
+				commandArray[0] = "bash.exe"; //$NON-NLS-1$
+				commandArray[1] = "-c"; //$NON-NLS-1$
+				commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$
+						"/"); //$NON-NLS-1$
+			} else {
+				commandArray = new String[1];
+				commandArray[0] = commandFile.getCanonicalPath();
+			}
+		} else if (fs instanceof FS_Win32_Cygwin) {
+			commandArray = new String[1];
+			commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
+		} else {
+			throw new ToolException(
+					"JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
+		}
+		return commandArray;
+	}
+
+	private void checkUseMsys2(String command) {
+		useMsys2 = false;
+		String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$
+		if (!StringUtils.isEmptyOrNull(useMsys2Str)) {
+			if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$
+				useMsys2 = command.contains(".sh"); //$NON-NLS-1$
+			} else {
+				useMsys2 = Boolean.parseBoolean(useMsys2Str);
+			}
+		}
+	}
+
+	private void createCommandFile(String command)
+			throws ToolException, IOException {
+		String fileExtension = null;
+		if (useMsys2 || fs instanceof FS_POSIX
+				|| fs instanceof FS_Win32_Cygwin) {
+			fileExtension = ".sh"; //$NON-NLS-1$
+		} else if (fs instanceof FS_Win32) {
+			fileExtension = ".cmd"; //$NON-NLS-1$
+			command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$
+					+ System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$
+		} else {
+			throw new ToolException(
+					"JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$
+		}
+		commandFile = File.createTempFile(".__", //$NON-NLS-1$
+				"__jgit_tool" + fileExtension); //$NON-NLS-1$
+		try (OutputStream outStream = new FileOutputStream(commandFile)) {
+			byte[] strToBytes = command.getBytes();
+			outStream.write(strToBytes);
+			outStream.close();
+		}
+		commandFile.setExecutable(true);
+	}
+
+	private void deleteCommandFile() {
+		if (commandFile != null && commandFile.exists()) {
+			commandFile.delete();
+		}
+	}
+
+	private boolean isCommandExecutionError(int rc) {
+		if (useMsys2 || fs instanceof FS_POSIX
+				|| fs instanceof FS_Win32_Cygwin) {
+			// 126: permission for executing command denied
+			// 127: command not found
+			if ((rc == 126) || (rc == 127)) {
+				return true;
+			}
+		}
+		else if (fs instanceof FS_Win32) {
+			// 9009, 0x2331: Program is not recognized as an internal or
+			// external command, operable program or batch file. Indicates that
+			// command, application name or path has been misspelled when
+			// configuring the Action.
+			if (rc == 9009) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java
index 509515c..00dec32 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java
@@ -111,7 +111,7 @@
 	 * See: <a href=
 	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
 	 */
-	gvimdiff("gviewdiff", "\"$LOCAL\" \"$REMOTE\""),
+	gvimdiff("gvimdiff", "\"$LOCAL\" \"$REMOTE\""),
 	/**
 	 * See: <a href=
 	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
@@ -160,7 +160,7 @@
 	 * See: <a href=
 	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
 	 */
-	vimdiff("viewdiff", gvimdiff),
+	vimdiff("vimdiff", gvimdiff),
 	/**
 	 * See: <a href=
 	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java
new file mode 100644
index 0000000..3a22124
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+/**
+ * Pre-defined merge tools.
+ *
+ * Adds same merge tools as also pre-defined in C-Git see "git-core\mergetools\"
+ * see links to command line parameter description for the tools
+ *
+ * <pre>
+ * araxis
+ * bc
+ * bc3
+ * codecompare
+ * deltawalker
+ * diffmerge
+ * diffuse
+ * ecmerge
+ * emerge
+ * examdiff
+ * guiffy
+ * gvimdiff
+ * gvimdiff2
+ * gvimdiff3
+ * kdiff3
+ * kompare
+ * meld
+ * opendiff
+ * p4merge
+ * tkdiff
+ * tortoisemerge
+ * vimdiff
+ * vimdiff2
+ * vimdiff3
+ * winmerge
+ * xxdiff
+ * </pre>
+ *
+ */
+@SuppressWarnings("nls")
+public enum CommandLineMergeTool {
+	/**
+	 * See: <a href=
+	 * "https://www.araxis.com/merge/documentation-windows/command-line.en">https://www.araxis.com/merge/documentation-windows/command-line.en</a>
+	 */
+	araxis("compare",
+			"-wait -merge -3 -a1 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+			"-wait -2 \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a>
+	 */
+	bc("bcomp", "\"$LOCAL\" \"$REMOTE\" \"$BASE\" --mergeoutput=\"$MERGED\"",
+			"\"$LOCAL\" \"$REMOTE\" --mergeoutput=\"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a>
+	 */
+	bc3("bcompare", bc),
+	/**
+	 * See: <a href=
+	 * "https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm">https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm</a>
+	 */
+	codecompare("CodeMerge",
+			"-MF=\"$LOCAL\" -TF=\"$REMOTE\" -BF=\"$BASE\" -RF=\"$MERGED\"",
+			"-MF=\"$LOCAL\" -TF=\"$REMOTE\" -RF=\"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.deltawalker.com/integrate/command-line">https://www.deltawalker.com/integrate/command-line</a>
+	 * <p>
+	 * Hint: $(pwd) command must be defined
+	 * </p>
+	 */
+	deltawalker("DeltaWalker",
+			"\"$LOCAL\" \"$REMOTE\" \"$BASE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"",
+			"\"$LOCAL\" \"$REMOTE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html">https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html</a>
+	 */
+	diffmerge("diffmerge", //$NON-NLS-1$
+			"--merge --result=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"",
+			"--merge --result=\"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "http://diffuse.sourceforge.net/manual.html#introduction-usage">http://diffuse.sourceforge.net/manual.html#introduction-usage</a>
+	 * <p>
+	 * Hint: check the ' | cat' for the call
+	 * </p>
+	 */
+	diffuse("diffuse", "\"$LOCAL\" \"$MERGED\" \"$REMOTE\" \"$BASE\"",
+			"\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", false),
+	/**
+	 * See: <a href=
+	 * "http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp">http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp</a>
+	 */
+	ecmerge("ecmerge",
+			"--default --mode=merge3 \"$BASE\" \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"",
+			"--default --mode=merge2 \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html">https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html</a>
+	 * <p>
+	 * Hint: $(basename) command must be defined
+	 * </p>
+	 */
+	emerge("emacs",
+			"-f emerge-files-with-ancestor-command \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$(basename \"$MERGED\")\"",
+			"-f emerge-files-command \"$LOCAL\" \"$REMOTE\" \"$(basename \"$MERGED\")\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options">https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options</a>
+	 */
+	examdiff("ExamDiff",
+			"-merge \"$LOCAL\" \"$BASE\" \"$REMOTE\" -o:\"$MERGED\" -nh",
+			"-merge \"$LOCAL\" \"$REMOTE\" -o:\"$MERGED\" -nh",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html">https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html</a>
+	 */
+	guiffy("guiffy", "-s \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"",
+			"-m \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	gvimdiff("gvim",
+			"-f -d -c '4wincmd w | wincmd J' \"$LOCAL\" \"$BASE\" \"$REMOTE\" \"$MERGED\"",
+			"-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	gvimdiff2("gvim", "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"",
+			"-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", true),
+	/**
+	 * See: <a href= "http://vimdoc.sourceforge.net/htmldoc/diff.html"></a>
+	 */
+	gvimdiff3("gvim",
+			"-f -d -c 'hid | hid | hid' \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"",
+			"-f -d -c 'hid | hid' \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true),
+	/**
+	 * See: <a href=
+	 * "http://kdiff3.sourceforge.net/doc/documentation.html">http://kdiff3.sourceforge.net/doc/documentation.html</a>
+	 */
+	kdiff3("kdiff3",
+			"--auto --L1 \"$MERGED (Base)\" --L2 \"$MERGED (Local)\" --L3 \"$MERGED (Remote)\" -o \"$MERGED\" \"$BASE\" \"$LOCAL\" \"$REMOTE\"",
+			"--auto --L1 \"$MERGED (Local)\" --L2 \"$MERGED (Remote)\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "http://meldmerge.org/help/file-mode.html">http://meldmerge.org/help/file-mode.html</a>
+	 * <p>
+	 * Hint: use meld with output option only (new versions)
+	 * </p>
+	 */
+	meld("meld", "--output=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"",
+			"\"$LOCAL\" \"$MERGED\" \"$REMOTE\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "http://www.manpagez.com/man/1/opendiff/">http://www.manpagez.com/man/1/opendiff/</a>
+	 * <p>
+	 * Hint: check the ' | cat' for the call
+	 * </p>
+	 */
+	opendiff("opendiff",
+			"\"$LOCAL\" \"$REMOTE\" -ancestor \"$BASE\" -merge \"$MERGED\"",
+			"\"$LOCAL\" \"$REMOTE\" -merge \"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html">https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html</a>
+	 * <p>
+	 * Hint: check how to fix "no base present" / create_virtual_base problem
+	 * </p>
+	 */
+	p4merge("p4merge", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"",
+			"\"$REMOTE\" \"$LOCAL\" \"$MERGED\"", false),
+	/**
+	 * See: <a href=
+	 * "http://linux.math.tifr.res.in/manuals/man/tkdiff.html">http://linux.math.tifr.res.in/manuals/man/tkdiff.html</a>
+	 */
+	tkdiff("tkdiff", "-a \"$BASE\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			"-o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			true),
+	/**
+	 * See: <a href=
+	 * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a>
+	 * <p>
+	 * Hint: merge without base is not supported
+	 * </p>
+	 * <p>
+	 * Hint: cannot diff
+	 * </p>
+	 */
+	tortoisegitmerge("tortoisegitmerge",
+			"-base \"$BASE\" -mine \"$LOCAL\" -theirs \"$REMOTE\" -merged \"$MERGED\"",
+			null, false),
+	/**
+	 * See: <a href=
+	 * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a>
+	 * <p>
+	 * Hint: merge without base is not supported
+	 * </p>
+	 * <p>
+	 * Hint: cannot diff
+	 * </p>
+	 */
+	tortoisemerge("tortoisemerge",
+			"-base:\"$BASE\" -mine:\"$LOCAL\" -theirs:\"$REMOTE\" -merged:\"$MERGED\"",
+			null, false),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	vimdiff("vim", gvimdiff),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	vimdiff2("vim", gvimdiff2),
+	/**
+	 * See: <a href=
+	 * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a>
+	 */
+	vimdiff3("vim", gvimdiff3),
+	/**
+	 * See: <a href=
+	 * "http://manual.winmerge.org/Command_line.html">http://manual.winmerge.org/Command_line.html</a>
+	 * <p>
+	 * Hint: check how 'mergetool_find_win32_cmd "WinMergeU.exe" "WinMerge"'
+	 * works
+	 * </p>
+	 */
+	winmerge("WinMergeU",
+			"-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+			"-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"",
+			false),
+	/**
+	 * See: <a href=
+	 * "http://furius.ca/xxdiff/doc/xxdiff-doc.html">http://furius.ca/xxdiff/doc/xxdiff-doc.html</a>
+	 */
+	xxdiff("xxdiff",
+			"-X --show-merged-pane -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"",
+			"-X -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$REMOTE\"",
+			false);
+
+	CommandLineMergeTool(String path, String parametersWithBase,
+			String parametersWithoutBase,
+			boolean exitCodeTrustable) {
+		this.path = path;
+		this.parametersWithBase = parametersWithBase;
+		this.parametersWithoutBase = parametersWithoutBase;
+		this.exitCodeTrustable = exitCodeTrustable;
+    }
+
+	CommandLineMergeTool(CommandLineMergeTool from) {
+		this(from.getPath(), from.getParameters(true),
+				from.getParameters(false), from.isExitCodeTrustable());
+	}
+
+	CommandLineMergeTool(String path, CommandLineMergeTool from) {
+		this(path, from.getParameters(true), from.getParameters(false),
+				from.isExitCodeTrustable());
+	}
+
+	private final String path;
+
+	private final String parametersWithBase;
+
+	private final String parametersWithoutBase;
+
+	private final boolean exitCodeTrustable;
+
+	/**
+	 * @return path
+	 */
+	public String getPath() {
+		return path;
+	}
+
+	/**
+	 * @param withBase
+	 *            return parameters with base present?
+	 * @return parameters with or without base present
+	 */
+	public String getParameters(boolean withBase) {
+		if (withBase) {
+			return parametersWithBase;
+		}
+		return parametersWithoutBase;
+	}
+
+	/**
+	 * @return parameters
+	 */
+	public boolean isExitCodeTrustable() {
+		return exitCodeTrustable;
+	}
+
+	/**
+	 * @return true if command with base present is valid, false otherwise
+	 */
+	public boolean canMergeWithoutBasePresent() {
+		return parametersWithoutBase != null;
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java
index 551f634..c8b04f9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java
@@ -49,9 +49,10 @@
 		toolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_TOOL);
 		guiToolName = rc.getString(CONFIG_DIFF_SECTION, null,
 				CONFIG_KEY_GUITOOL);
-		prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, CONFIG_KEY_PROMPT,
+		prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, toolName,
+				CONFIG_KEY_PROMPT,
 				true);
-		String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, null,
+		String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, toolName,
 				CONFIG_KEY_TRUST_EXIT_CODE);
 		if (trustStr != null) {
 			trustExitCode = Boolean.parseBoolean(trustStr)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
index 39729a4..7cedd82 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java
@@ -1,5 +1,6 @@
 /*
- * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -10,24 +11,43 @@
 
 package org.eclipse.jgit.internal.diffmergetool;
 
-import java.util.TreeMap;
+import java.io.File;
+import java.io.IOException;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
 import java.util.Set;
+import java.util.TreeMap;
 
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.StringUtils;
 
 /**
  * Manages diff tools.
  */
 public class DiffTools {
 
+	private final FS fs;
+
+	private final File gitDir;
+
+	private final File workTree;
+
 	private final DiffToolConfig config;
 
-	private Map<String, ExternalDiffTool> predefinedTools;
+	private final Repository repo;
 
-	private Map<String, ExternalDiffTool> userDefinedTools;
+	private final Map<String, ExternalDiffTool> predefinedTools;
+
+	private final Map<String, ExternalDiffTool> userDefinedTools;
 
 	/**
 	 * Creates the external diff-tools manager for given repository.
@@ -36,46 +56,220 @@
 	 *            the repository
 	 */
 	public DiffTools(Repository repo) {
-		config = repo.getConfig().get(DiffToolConfig.KEY);
-		setupPredefinedTools();
-		setupUserDefinedTools();
+		this(repo, repo.getConfig());
+	}
+
+	/**
+	 * Creates the external merge-tools manager for given configuration.
+	 *
+	 * @param config
+	 *            the git configuration
+	 */
+	public DiffTools(StoredConfig config) {
+		this(null, config);
+	}
+
+	private DiffTools(Repository repo, StoredConfig config) {
+		this.repo = repo;
+		this.config = config.get(DiffToolConfig.KEY);
+		this.gitDir = repo == null ? null : repo.getDirectory();
+		this.fs = repo == null ? FS.DETECTED : repo.getFS();
+		this.workTree = repo == null ? null : repo.getWorkTree();
+		predefinedTools = setupPredefinedTools();
+		userDefinedTools = setupUserDefinedTools(predefinedTools);
 	}
 
 	/**
 	 * Compare two versions of a file.
 	 *
-	 * @param newPath
-	 *            the new file path
-	 * @param oldPath
-	 *            the old file path
-	 * @param newId
-	 *            the new object ID
-	 * @param oldId
-	 *            the old object ID
+	 * @param localFile
+	 *            The local/left version of the file.
+	 * @param remoteFile
+	 *            The remote/right version of the file.
 	 * @param toolName
-	 *            the selected tool name (can be null)
+	 *            Optionally the name of the tool to use. If not given the
+	 *            default tool will be used.
 	 * @param prompt
-	 *            the prompt option
+	 *            Optionally a flag whether to prompt the user before compare.
+	 *            If not given the default will be used.
 	 * @param gui
-	 *            the GUI option
+	 *            A flag whether to prefer a gui tool.
+	 * @param trustExitCode
+	 *            Optionally a flag whether to trust the exit code of the tool.
+	 *            If not given the default will be used.
+	 * @param promptHandler
+	 *            The handler to use when needing to prompt the user if he wants
+	 *            to continue.
+	 * @param noToolHandler
+	 *            The handler to use when needing to inform the user, that no
+	 *            tool is configured.
+	 * @return the optioanl result of executing the tool if it was executed
+	 * @throws ToolException
+	 *             when the tool fails
+	 */
+	public Optional<ExecutionResult> compare(FileElement localFile,
+			FileElement remoteFile, Optional<String> toolName,
+			BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode,
+			PromptContinueHandler promptHandler,
+			InformNoToolHandler noToolHandler) throws ToolException {
+
+		String toolNameToUse;
+
+		if (toolName == null) {
+			throw new ToolException(JGitText.get().diffToolNullError);
+		}
+
+		if (toolName.isPresent()) {
+			toolNameToUse = toolName.get();
+		} else {
+			toolNameToUse = getDefaultToolName(gui);
+		}
+
+		if (StringUtils.isEmptyOrNull(toolNameToUse)) {
+			throw new ToolException(JGitText.get().diffToolNotGivenError);
+		}
+
+		boolean doPrompt;
+		if (prompt != BooleanTriState.UNSET) {
+			doPrompt = prompt == BooleanTriState.TRUE;
+		} else {
+			doPrompt = isInteractive();
+		}
+
+		if (doPrompt) {
+			if (!promptHandler.prompt(toolNameToUse)) {
+				return Optional.empty();
+			}
+		}
+
+		boolean trust;
+		if (trustExitCode != BooleanTriState.UNSET) {
+			trust = trustExitCode == BooleanTriState.TRUE;
+		} else {
+			trust = config.isTrustExitCode();
+		}
+
+		ExternalDiffTool tool = getTool(toolNameToUse);
+		if (tool == null) {
+			throw new ToolException(
+					"External diff tool is not defined: " + toolNameToUse); //$NON-NLS-1$
+		}
+
+		return Optional.of(
+				compare(localFile, remoteFile, tool, trust));
+	}
+
+	/**
+	 * Compare two versions of a file.
+	 *
+	 * @param localFile
+	 *            the local file element
+	 * @param remoteFile
+	 *            the remote file element
+	 * @param tool
+	 *            the selected tool
 	 * @param trustExitCode
 	 *            the "trust exit code" option
-	 * @return the return code from executed tool
+	 * @return the execution result from tool
+	 * @throws ToolException
 	 */
-	public int compare(String newPath, String oldPath, String newId,
-			String oldId, String toolName, BooleanTriState prompt,
-			BooleanTriState gui, BooleanTriState trustExitCode) {
-		return 0;
+	public ExecutionResult compare(FileElement localFile,
+			FileElement remoteFile, ExternalDiffTool tool,
+			boolean trustExitCode) throws ToolException {
+		try {
+			if (tool == null) {
+				throw new ToolException(JGitText
+						.get().diffToolNotSpecifiedInGitAttributesError);
+			}
+			// prepare the command (replace the file paths)
+			String command = ExternalToolUtils.prepareCommand(tool.getCommand(),
+					localFile, remoteFile, null, null);
+			// prepare the environment
+			Map<String, String> env = ExternalToolUtils.prepareEnvironment(
+					gitDir, localFile, remoteFile, null, null);
+			// execute the tool
+			CommandExecutor cmdExec = new CommandExecutor(fs, trustExitCode);
+			return cmdExec.run(command, workTree, env);
+		} catch (IOException | InterruptedException e) {
+			throw new ToolException(e);
+		} finally {
+			localFile.cleanTemporaries();
+			remoteFile.cleanTemporaries();
+		}
 	}
 
 	/**
-	 * @return the tool names
+	 * Get user defined tool names.
+	 *
+	 * @return the user defined tool names
 	 */
-	public Set<String> getToolNames() {
-		return config.getToolNames();
+	public Set<String> getUserDefinedToolNames() {
+		return userDefinedTools.keySet();
 	}
 
 	/**
+	 * Get predefined tool names.
+	 *
+	 * @return the predefined tool names
+	 */
+	public Set<String> getPredefinedToolNames() {
+		return predefinedTools.keySet();
+	}
+
+	/**
+	 * Get all tool names.
+	 *
+	 * @return the all tool names (default or available tool name is the first
+	 *         in the set)
+	 */
+	public Set<String> getAllToolNames() {
+		String defaultName = getDefaultToolName(false);
+		if (defaultName == null) {
+			defaultName = getFirstAvailableTool();
+		}
+		return ExternalToolUtils.createSortedToolSet(defaultName,
+				getUserDefinedToolNames(), getPredefinedToolNames());
+	}
+
+	/**
+	 * Provides {@link Optional} with the name of an external diff tool if
+	 * specified in git configuration for a path.
+	 *
+	 * The formed git configuration results from global rules as well as merged
+	 * rules from info and worktree attributes.
+	 *
+	 * Triggers {@link TreeWalk} until specified path found in the tree.
+	 *
+	 * @param path
+	 *            path to the node in repository to parse git attributes for
+	 * @return name of the difftool if set
+	 * @throws ToolException
+	 */
+	public Optional<String> getExternalToolFromAttributes(final String path)
+			throws ToolException {
+		return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
+				ExternalToolUtils.KEY_DIFF_TOOL);
+	}
+
+	/**
+	 * Checks the availability of the predefined tools in the system.
+	 *
+	 * @return set of predefined available tools
+	 */
+	public Set<String> getPredefinedAvailableTools() {
+		Map<String, ExternalDiffTool> defTools = getPredefinedTools(true);
+		Set<String> availableTools = new LinkedHashSet<>();
+		for (Entry<String, ExternalDiffTool> elem : defTools.entrySet()) {
+			if (elem.getValue().isAvailable()) {
+				availableTools.add(elem.getKey());
+			}
+		}
+		return availableTools;
+	}
+
+	/**
+	 * Get user defined tools map.
+	 *
 	 * @return the user defined tools
 	 */
 	public Map<String, ExternalDiffTool> getUserDefinedTools() {
@@ -83,61 +277,106 @@
 	}
 
 	/**
-	 * @return the available predefined tools
+	 * Get predefined tools map.
+	 *
+	 * @param checkAvailability
+	 *            true: for checking if tools can be executed; ATTENTION: this
+	 *            check took some time, do not execute often (store the map for
+	 *            other actions); false: availability is NOT checked:
+	 *            isAvailable() returns default false is this case!
+	 * @return the predefined tools with optionally checked availability (long
+	 *         running operation)
 	 */
-	public Map<String, ExternalDiffTool> getAvailableTools() {
+	public Map<String, ExternalDiffTool> getPredefinedTools(
+			boolean checkAvailability) {
+		if (checkAvailability) {
+			for (ExternalDiffTool tool : predefinedTools.values()) {
+				PreDefinedDiffTool predefTool = (PreDefinedDiffTool) tool;
+				predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
+						gitDir, workTree, predefTool.getPath()));
+			}
+		}
 		return Collections.unmodifiableMap(predefinedTools);
 	}
 
 	/**
-	 * @return the NOT available predefined tools
+	 * Get first available tool name.
+	 *
+	 * @return the name of first available predefined tool or null
 	 */
-	public Map<String, ExternalDiffTool> getNotAvailableTools() {
-		return Collections.unmodifiableMap(new TreeMap<>());
+	public String getFirstAvailableTool() {
+		for (ExternalDiffTool tool : predefinedTools.values()) {
+			if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
+					tool.getPath())) {
+				return tool.getName();
+			}
+		}
+		return null;
 	}
 
 	/**
+	 * Get default (gui-)tool name.
+	 *
 	 * @param gui
 	 *            use the diff.guitool setting ?
 	 * @return the default tool name
 	 */
-	public String getDefaultToolName(BooleanTriState gui) {
-		return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$
-				: "my_default_toolname"; //$NON-NLS-1$
+	public String getDefaultToolName(boolean gui) {
+		String guiToolName;
+		if (gui) {
+			guiToolName = config.getDefaultGuiToolName();
+			if (guiToolName != null) {
+				return guiToolName;
+			}
+		}
+		return config.getDefaultToolName();
 	}
 
 	/**
+	 * Is interactive diff (prompt enabled) ?
+	 *
 	 * @return is interactive (config prompt enabled) ?
 	 */
 	public boolean isInteractive() {
-		return false;
+		return config.isPrompt();
 	}
 
-	private void setupPredefinedTools() {
-		predefinedTools = new TreeMap<>();
-		for (CommandLineDiffTool tool : CommandLineDiffTool.values()) {
-			predefinedTools.put(tool.name(), new PreDefinedDiffTool(tool));
+	private ExternalDiffTool getTool(final String name) {
+		ExternalDiffTool tool = userDefinedTools.get(name);
+		if (tool == null) {
+			tool = predefinedTools.get(name);
 		}
+		return tool;
 	}
 
-	private void setupUserDefinedTools() {
-		userDefinedTools = new TreeMap<>();
+	private static Map<String, ExternalDiffTool> setupPredefinedTools() {
+		Map<String, ExternalDiffTool> tools = new TreeMap<>();
+		for (CommandLineDiffTool tool : CommandLineDiffTool.values()) {
+			tools.put(tool.name(), new PreDefinedDiffTool(tool));
+		}
+		return tools;
+	}
+
+	private Map<String, ExternalDiffTool> setupUserDefinedTools(
+			Map<String, ExternalDiffTool> predefTools) {
+		Map<String, ExternalDiffTool> tools = new TreeMap<>();
 		Map<String, ExternalDiffTool> userTools = config.getTools();
 		for (String name : userTools.keySet()) {
 			ExternalDiffTool userTool = userTools.get(name);
 			// if difftool.<name>.cmd is defined we have user defined tool
 			if (userTool.getCommand() != null) {
-				userDefinedTools.put(name, userTool);
+				tools.put(name, userTool);
 			} else if (userTool.getPath() != null) {
 				// if difftool.<name>.path is defined we just overload the path
 				// of predefined tool
-				PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefinedTools
+				PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefTools
 						.get(name);
 				if (predefTool != null) {
 					predefTool.setPath(userTool.getPath());
 				}
 			}
 		}
+		return tools;
 	}
 
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java
index f2d7e82..e01b892 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java
@@ -30,4 +30,10 @@
 	 */
 	String getCommand();
 
+	/**
+	 * @return availability of the tool: true if tool can be executed and false
+	 *         if not
+	 */
+	boolean isAvailable();
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java
new file mode 100644
index 0000000..0c3ddf9
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+
+/**
+ * The merge tool interface.
+ */
+public interface ExternalMergeTool extends ExternalDiffTool {
+
+	/**
+	 * @return the tool "trust exit code" option
+	 */
+	BooleanTriState getTrustExitCode();
+
+	/**
+	 * @param withBase
+	 *            get command with base present (true) or without base present
+	 *            (false)
+	 * @return the tool command
+	 */
+	String getCommand(boolean withBase);
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java
new file mode 100644
index 0000000..b2dd846
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.util.TreeMap;
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.eclipse.jgit.attributes.Attributes;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
+import org.eclipse.jgit.util.FS;
+
+/**
+ * Utilities for diff- and merge-tools.
+ */
+public class ExternalToolUtils {
+
+	/**
+	 * Key for merge tool git configuration section
+	 */
+	public static final String KEY_MERGE_TOOL = "mergetool"; //$NON-NLS-1$
+
+	/**
+	 * Key for diff tool git configuration section
+	 */
+	public static final String KEY_DIFF_TOOL = "difftool"; //$NON-NLS-1$
+
+	/**
+	 * Prepare command for execution.
+	 *
+	 * @param command
+	 *            the input "command" string
+	 * @param localFile
+	 *            the local file (ours)
+	 * @param remoteFile
+	 *            the remote file (theirs)
+	 * @param mergedFile
+	 *            the merged file (worktree)
+	 * @param baseFile
+	 *            the base file (can be null)
+	 * @return the prepared (with replaced variables) command string
+	 * @throws IOException
+	 */
+	public static String prepareCommand(String command, FileElement localFile,
+			FileElement remoteFile, FileElement mergedFile,
+			FileElement baseFile) throws IOException {
+		if (localFile != null) {
+			command = localFile.replaceVariable(command);
+		}
+		if (remoteFile != null) {
+			command = remoteFile.replaceVariable(command);
+		}
+		if (mergedFile != null) {
+			command = mergedFile.replaceVariable(command);
+		}
+		if (baseFile != null) {
+			command = baseFile.replaceVariable(command);
+		}
+		return command;
+	}
+
+	/**
+	 * Prepare environment needed for execution.
+	 *
+	 * @param gitDir
+	 *            the .git directory
+	 * @param localFile
+	 *            the local file (ours)
+	 * @param remoteFile
+	 *            the remote file (theirs)
+	 * @param mergedFile
+	 *            the merged file (worktree)
+	 * @param baseFile
+	 *            the base file (can be null)
+	 * @return the environment map with variables and values (file paths)
+	 * @throws IOException
+	 */
+	public static Map<String, String> prepareEnvironment(File gitDir,
+			FileElement localFile, FileElement remoteFile,
+			FileElement mergedFile, FileElement baseFile) throws IOException {
+		Map<String, String> env = new TreeMap<>();
+		if (gitDir != null) {
+			env.put(Constants.GIT_DIR_KEY, gitDir.getAbsolutePath());
+		}
+		if (localFile != null) {
+			localFile.addToEnv(env);
+		}
+		if (remoteFile != null) {
+			remoteFile.addToEnv(env);
+		}
+		if (mergedFile != null) {
+			mergedFile.addToEnv(env);
+		}
+		if (baseFile != null) {
+			baseFile.addToEnv(env);
+		}
+		return env;
+	}
+
+	/**
+	 * @param path
+	 *            the path to be quoted
+	 * @return quoted path if it contains spaces
+	 */
+	@SuppressWarnings("nls")
+	public static String quotePath(String path) {
+		// handling of spaces in path
+		if (path.contains(" ")) {
+			// add quotes before if needed
+			if (!path.startsWith("\"")) {
+				path = "\"" + path;
+			}
+			// add quotes after if needed
+			if (!path.endsWith("\"")) {
+				path = path + "\"";
+			}
+		}
+		return path;
+	}
+
+	/**
+	 * @param fs
+	 *            the file system abstraction
+	 * @param gitDir
+	 *            the .git directory
+	 * @param directory
+	 *            the working directory
+	 * @param path
+	 *            the tool path
+	 * @return true if tool available and false otherwise
+	 */
+	public static boolean isToolAvailable(FS fs, File gitDir, File directory,
+			String path) {
+		boolean available = true;
+		try {
+			CommandExecutor cmdExec = new CommandExecutor(fs, false);
+			available = cmdExec.checkExecutable(path, directory,
+					prepareEnvironment(gitDir, null, null, null, null));
+		} catch (Exception e) {
+			available = false;
+		}
+		return available;
+	}
+
+	/**
+	 * @param defaultName
+	 *            the default tool name
+	 * @param userDefinedNames
+	 *            the user defined tool names
+	 * @param preDefinedNames
+	 *            the pre defined tool names
+	 * @return the sorted tool names set: first element is default tool name if
+	 *         valid, then user defined tool names and then pre defined tool
+	 *         names
+	 */
+	public static Set<String> createSortedToolSet(String defaultName,
+			Set<String> userDefinedNames, Set<String> preDefinedNames) {
+		Set<String> names = new LinkedHashSet<>();
+		if (defaultName != null) {
+			// remove defaultName from both sets
+			Set<String> namesPredef = new LinkedHashSet<>();
+			Set<String> namesUser = new LinkedHashSet<>();
+			namesUser.addAll(userDefinedNames);
+			namesUser.remove(defaultName);
+			namesPredef.addAll(preDefinedNames);
+			namesPredef.remove(defaultName);
+			// add defaultName as first in set
+			names.add(defaultName);
+			names.addAll(namesUser);
+			names.addAll(namesPredef);
+		} else {
+			names.addAll(userDefinedNames);
+			names.addAll(preDefinedNames);
+		}
+		return names;
+	}
+
+	/**
+	 * Provides {@link Optional} with the name of an external tool if specified
+	 * in git configuration for a path.
+	 *
+	 * The formed git configuration results from global rules as well as merged
+	 * rules from info and worktree attributes.
+	 *
+	 * Triggers {@link TreeWalk} until specified path found in the tree.
+	 *
+	 * @param repository
+	 *            target repository to traverse into
+	 * @param path
+	 *            path to the node in repository to parse git attributes for
+	 * @param toolKey
+	 *            config key name for the tool
+	 * @return attribute value for the given tool key if set
+	 * @throws ToolException
+	 */
+	public static Optional<String> getExternalToolFromAttributes(
+			final Repository repository, final String path,
+			final String toolKey) throws ToolException {
+		try {
+			WorkingTreeIterator treeIterator = new FileTreeIterator(repository);
+			try (TreeWalk walk = new TreeWalk(repository)) {
+				walk.addTree(treeIterator);
+				walk.setFilter(new NotIgnoredFilter(0));
+				while (walk.next()) {
+					String treePath = walk.getPathString();
+					if (treePath.equals(path)) {
+						Attributes attrs = walk.getAttributes();
+						if (attrs.containsKey(toolKey)) {
+							return Optional.of(attrs.getValue(toolKey));
+						}
+					}
+					if (walk.isSubtree()) {
+						walk.enterSubtree();
+					}
+				}
+				// no external tool specified
+				return Optional.empty();
+			}
+
+		} catch (RevisionSyntaxException | IOException e) {
+			throw new ToolException(e);
+		}
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
new file mode 100644
index 0000000..ba8ca54
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+
+import org.eclipse.jgit.diff.DiffEntry;
+
+/**
+ * The element used as left or right file for compare.
+ *
+ */
+public class FileElement {
+
+	/**
+	 * The file element type.
+	 *
+	 */
+	public enum Type {
+		/**
+		 * The local file element (ours).
+		 */
+		LOCAL,
+		/**
+		 * The remote file element (theirs).
+		 */
+		REMOTE,
+		/**
+		 * The merged file element (path in worktree).
+		 */
+		MERGED,
+		/**
+		 * The base file element (of ours and theirs).
+		 */
+		BASE,
+		/**
+		 * The backup file element (copy of merged / conflicted).
+		 */
+		BACKUP
+	}
+
+	private final String path;
+
+	private final Type type;
+
+	private final File workDir;
+
+	private InputStream stream;
+
+	private File tempFile;
+
+	/**
+	 * Creates file element for path.
+	 *
+	 * @param path
+	 *            the file path
+	 * @param type
+	 *            the element type
+	 */
+	public FileElement(String path, Type type) {
+		this(path, type, null);
+	}
+
+	/**
+	 * Creates file element for path.
+	 *
+	 * @param path
+	 *            the file path
+	 * @param type
+	 *            the element type
+	 * @param workDir
+	 *            the working directory of the path (can be null, then current
+	 *            working dir is used)
+	 */
+	public FileElement(String path, Type type, File workDir) {
+		this(path, type, workDir, null);
+	}
+
+	/**
+	 * @param path
+	 *            the file path
+	 * @param type
+	 *            the element type
+	 * @param workDir
+	 *            the working directory of the path (can be null, then current
+	 *            working dir is used)
+	 * @param stream
+	 *            the object stream to load and write on demand, @see getFile(),
+	 *            to tempFile once (can be null)
+	 */
+	public FileElement(String path, Type type, File workDir,
+			InputStream stream) {
+		this.path = path;
+		this.type = type;
+		this.workDir = workDir;
+		this.stream = stream;
+	}
+
+	/**
+	 * @return the file path
+	 */
+	public String getPath() {
+		return path;
+	}
+
+	/**
+	 * @return the element type
+	 */
+	public Type getType() {
+		return type;
+	}
+
+	/**
+	 * Return
+	 * <ul>
+	 * <li>a temporary file if already created and stream is not valid</li>
+	 * <li>OR a real file from work tree: if no temp file was created (@see
+	 * createTempFile()) and if no stream was set</li>
+	 * <li>OR an empty temporary file if path is "/dev/null"</li>
+	 * <li>OR a temporary file with stream content if stream is valid (not
+	 * null); stream is closed and invalidated (set to null) after write to temp
+	 * file, so stream is used only once during first call!</li>
+	 * </ul>
+	 *
+	 * @return the object stream
+	 * @throws IOException
+	 */
+	public File getFile() throws IOException {
+		// if we have already temp file and no stream
+		// then just return this temp file (it was filled from outside)
+		if ((tempFile != null) && (stream == null)) {
+			return tempFile;
+		}
+		File file = new File(workDir, path);
+		// if we have a stream or file is missing (path is "/dev/null")
+		// then optionally create temporary file and fill it with stream content
+		if ((stream != null) || isNullPath()) {
+			if (tempFile == null) {
+				tempFile = getTempFile(file, type.name(), null);
+			}
+			if (stream != null) {
+				copyFromStream(tempFile, stream);
+			}
+			// invalidate the stream, because it is used once
+			stream = null;
+			return tempFile;
+		}
+		return file;
+	}
+
+	/**
+	 * Check if path id "/dev/null"
+	 *
+	 * @return true if path is "/dev/null"
+	 */
+	public boolean isNullPath() {
+		return path.equals(DiffEntry.DEV_NULL);
+	}
+
+	/**
+	 * Create temporary file in given or system temporary directory.
+	 *
+	 * @param directory
+	 *            the directory for the file (can be null); if null system
+	 *            temporary directory is used
+	 * @return temporary file in directory or in the system temporary directory
+	 * @throws IOException
+	 */
+	public File createTempFile(File directory) throws IOException {
+		if (tempFile == null) {
+			tempFile = getTempFile(new File(path), type.name(), directory);
+		}
+		return tempFile;
+	}
+
+	/**
+	 * Delete and invalidate temporary file if necessary.
+	 */
+	public void cleanTemporaries() {
+		if (tempFile != null && tempFile.exists()) {
+			tempFile.delete();
+		}
+		tempFile = null;
+	}
+
+	/**
+	 * Replace variable in input.
+	 *
+	 * @param input
+	 *            the input string
+	 * @return the replaced input string
+	 * @throws IOException
+	 */
+	public String replaceVariable(String input) throws IOException {
+		return input.replace("$" + type.name(), getFile().getPath()); //$NON-NLS-1$
+	}
+
+	/**
+	 * Add variable to environment map.
+	 *
+	 * @param env
+	 *            the environment where this element should be added
+	 * @throws IOException
+	 */
+	public void addToEnv(Map<String, String> env) throws IOException {
+		env.put(type.name(), getFile().getPath());
+	}
+
+	private static File getTempFile(final File file, final String midName,
+			final File workingDir) throws IOException {
+		String[] fileNameAndExtension = splitBaseFileNameAndExtension(file);
+		// TODO: avoid long random file name (number generated by
+		// createTempFile)
+		return File.createTempFile(
+				fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$
+				fileNameAndExtension[1], workingDir);
+	}
+
+	private static void copyFromStream(final File file,
+			final InputStream stream)
+			throws IOException, FileNotFoundException {
+		try (OutputStream outStream = new FileOutputStream(file)) {
+			int read = 0;
+			byte[] bytes = new byte[8 * 1024];
+			while ((read = stream.read(bytes)) != -1) {
+				outStream.write(bytes, 0, read);
+			}
+		} finally {
+			// stream can only be consumed once --> close it and invalidate
+			stream.close();
+		}
+	}
+
+	private static String[] splitBaseFileNameAndExtension(File file) {
+		String[] result = new String[2];
+		result[0] = file.getName();
+		result[1] = ""; //$NON-NLS-1$
+		int idx = result[0].lastIndexOf("."); //$NON-NLS-1$
+		// if "." was found (>-1) and last-index is not first char (>0), then
+		// split (same behavior like cgit)
+		if (idx > 0) {
+			result[1] = result[0].substring(idx, result[0].length());
+			result[0] = result[0].substring(0, idx);
+		}
+		return result;
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java
new file mode 100644
index 0000000..36b290d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.util.List;
+
+/**
+ * A handler for when the diff/merge tool manager wants to inform the user that
+ * no tool has been configured and one of the default tools will be used.
+ */
+public interface InformNoToolHandler {
+	/**
+	 * Inform the user, that no tool is configured and that one of the given
+	 * tools is used.
+	 *
+	 * @param toolNames
+	 *            The tools which are tried
+	 */
+	void inform(List<String> toolNames);
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java
new file mode 100644
index 0000000..9625d5f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_BACKUP;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.SectionParser;
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+
+/**
+ * Keeps track of merge tool related configuration options.
+ */
+public class MergeToolConfig {
+
+	/** Key for {@link Config#get(SectionParser)}. */
+	public static final Config.SectionParser<MergeToolConfig> KEY = MergeToolConfig::new;
+
+	private final String toolName;
+
+	private final String guiToolName;
+
+	private final boolean prompt;
+
+	private final boolean keepBackup;
+
+	private final boolean keepTemporaries;
+
+	private final boolean writeToTemp;
+
+	private final Map<String, ExternalMergeTool> tools;
+
+	private MergeToolConfig(Config rc) {
+		toolName = rc.getString(CONFIG_MERGE_SECTION, null, CONFIG_KEY_TOOL);
+		guiToolName = rc.getString(CONFIG_MERGE_SECTION, null,
+				CONFIG_KEY_GUITOOL);
+		prompt = rc.getBoolean(CONFIG_MERGETOOL_SECTION, toolName,
+				CONFIG_KEY_PROMPT, true);
+		keepBackup = rc.getBoolean(CONFIG_MERGETOOL_SECTION,
+				CONFIG_KEY_KEEP_BACKUP, true);
+		keepTemporaries = rc.getBoolean(CONFIG_MERGETOOL_SECTION,
+				CONFIG_KEY_KEEP_TEMPORARIES, false);
+		writeToTemp = rc.getBoolean(CONFIG_MERGETOOL_SECTION,
+				CONFIG_KEY_WRITE_TO_TEMP, false);
+		tools = new HashMap<>();
+		Set<String> subsections = rc.getSubsections(CONFIG_MERGETOOL_SECTION);
+		for (String name : subsections) {
+			String cmd = rc.getString(CONFIG_MERGETOOL_SECTION, name,
+					CONFIG_KEY_CMD);
+			String path = rc.getString(CONFIG_MERGETOOL_SECTION, name,
+					CONFIG_KEY_PATH);
+			BooleanTriState trustExitCode = BooleanTriState.FALSE;
+			String trustStr = rc.getString(CONFIG_MERGETOOL_SECTION, name,
+					CONFIG_KEY_TRUST_EXIT_CODE);
+			if (trustStr != null) {
+				trustExitCode = Boolean.valueOf(trustStr).booleanValue()
+						? BooleanTriState.TRUE
+						: BooleanTriState.FALSE;
+			} else {
+				trustExitCode = BooleanTriState.UNSET;
+			}
+			if ((cmd != null) || (path != null)) {
+				tools.put(name, new UserDefinedMergeTool(name, path, cmd,
+						trustExitCode));
+			}
+		}
+	}
+
+	/**
+	 * @return the default merge tool name (merge.tool)
+	 */
+	public String getDefaultToolName() {
+		return toolName;
+	}
+
+	/**
+	 * @return the default GUI merge tool name (merge.guitool)
+	 */
+	public String getDefaultGuiToolName() {
+		return guiToolName;
+	}
+
+	/**
+	 * @return the merge tool "prompt" option (mergetool.prompt)
+	 */
+	public boolean isPrompt() {
+		return prompt;
+	}
+
+	/**
+	 * @return the tool "keep backup" option
+	 */
+	public boolean isKeepBackup() {
+		return keepBackup;
+	}
+
+	/**
+	 * @return the tool "keepTemporaries" option
+	 */
+	public boolean isKeepTemporaries() {
+		return keepTemporaries;
+	}
+
+	/**
+	 * @return the tool "write to temp" option
+	 */
+	public boolean isWriteToTemp() {
+		return writeToTemp;
+	}
+
+	/**
+	 * @return the tools map
+	 */
+	public Map<String, ExternalMergeTool> getTools() {
+		return tools;
+	}
+
+	/**
+	 * @return the tool names
+	 */
+	public Set<String> getToolNames() {
+		return tools.keySet();
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
new file mode 100644
index 0000000..b903201
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.diffmergetool;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.diffmergetool.FileElement.Type;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+
+/**
+ * Manages merge tools.
+ */
+public class MergeTools {
+
+	private final FS fs;
+
+	private final File gitDir;
+
+	private final File workTree;
+
+	private final MergeToolConfig config;
+
+	private final Repository repo;
+
+	private final Map<String, ExternalMergeTool> predefinedTools;
+
+	private final Map<String, ExternalMergeTool> userDefinedTools;
+
+	/**
+	 * Creates the external merge-tools manager for given repository.
+	 *
+	 * @param repo
+	 *            the repository
+	 */
+	public MergeTools(Repository repo) {
+		this(repo, repo.getConfig());
+	}
+
+	/**
+	 * Creates the external diff-tools manager for given configuration.
+	 *
+	 * @param config
+	 *            the git configuration
+	 */
+	public MergeTools(StoredConfig config) {
+		this(null, config);
+	}
+
+	private MergeTools(Repository repo, StoredConfig config) {
+		this.repo = repo;
+		this.config = config.get(MergeToolConfig.KEY);
+		this.gitDir = repo == null ? null : repo.getDirectory();
+		this.fs = repo == null ? FS.DETECTED : repo.getFS();
+		this.workTree = repo == null ? null : repo.getWorkTree();
+		predefinedTools = setupPredefinedTools();
+		userDefinedTools = setupUserDefinedTools(predefinedTools);
+	}
+
+	/**
+	 * Merge two versions of a file with optional base file.
+	 *
+	 * @param localFile
+	 *            The local/left version of the file.
+	 * @param remoteFile
+	 *            The remote/right version of the file.
+	 * @param mergedFile
+	 *            The file for the result.
+	 * @param baseFile
+	 *            The base version of the file. May be null.
+	 * @param tempDir
+	 *            The tmepDir used for the files. May be null.
+	 * @param toolName
+	 *            Optionally the name of the tool to use. If not given the
+	 *            default tool will be used.
+	 * @param prompt
+	 *            Optionally a flag whether to prompt the user before compare.
+	 *            If not given the default will be used.
+	 * @param gui
+	 *            A flag whether to prefer a gui tool.
+	 * @param promptHandler
+	 *            The handler to use when needing to prompt the user if he wants
+	 *            to continue.
+	 * @param noToolHandler
+	 *            The handler to use when needing to inform the user, that no
+	 *            tool is configured.
+	 * @return the optional result of executing the tool if it was executed
+	 * @throws ToolException
+	 *             when the tool fails
+	 */
+	public Optional<ExecutionResult> merge(FileElement localFile,
+			FileElement remoteFile, FileElement mergedFile,
+			FileElement baseFile, File tempDir, Optional<String> toolName,
+			BooleanTriState prompt, boolean gui,
+			PromptContinueHandler promptHandler,
+			InformNoToolHandler noToolHandler) throws ToolException {
+
+		String toolNameToUse;
+
+		if (toolName == null) {
+			throw new ToolException(JGitText.get().diffToolNullError);
+		}
+
+		if (toolName.isPresent()) {
+			toolNameToUse = toolName.get();
+		} else {
+			toolNameToUse = getDefaultToolName(gui);
+
+			if (StringUtils.isEmptyOrNull(toolNameToUse)) {
+				noToolHandler.inform(new ArrayList<>(predefinedTools.keySet()));
+				toolNameToUse = getFirstAvailableTool();
+			}
+		}
+
+		if (StringUtils.isEmptyOrNull(toolNameToUse)) {
+			throw new ToolException(JGitText.get().diffToolNotGivenError);
+		}
+
+		boolean doPrompt;
+		if (prompt != BooleanTriState.UNSET) {
+			doPrompt = prompt == BooleanTriState.TRUE;
+		} else {
+			doPrompt = isInteractive();
+		}
+
+		if (doPrompt) {
+			if (!promptHandler.prompt(toolNameToUse)) {
+				return Optional.empty();
+			}
+		}
+
+		ExternalMergeTool tool = getTool(toolNameToUse);
+		if (tool == null) {
+			throw new ToolException(
+					"External merge tool is not defined: " + toolNameToUse); //$NON-NLS-1$
+		}
+
+		return Optional.of(merge(localFile, remoteFile, mergedFile, baseFile,
+				tempDir, tool));
+	}
+
+	/**
+	 * Merge two versions of a file with optional base file.
+	 *
+	 * @param localFile
+	 *            the local file element
+	 * @param remoteFile
+	 *            the remote file element
+	 * @param mergedFile
+	 *            the merged file element
+	 * @param baseFile
+	 *            the base file element (can be null)
+	 * @param tempDir
+	 *            the temporary directory (needed for backup and auto-remove,
+	 *            can be null)
+	 * @param tool
+	 *            the selected tool
+	 * @return the execution result from tool
+	 * @throws ToolException
+	 */
+	public ExecutionResult merge(FileElement localFile, FileElement remoteFile,
+			FileElement mergedFile, FileElement baseFile, File tempDir,
+			ExternalMergeTool tool) throws ToolException {
+		FileElement backup = null;
+		ExecutionResult result = null;
+		try {
+			// create additional backup file (copy worktree file)
+			backup = createBackupFile(mergedFile,
+					tempDir != null ? tempDir : workTree);
+			// prepare the command (replace the file paths)
+			String command = ExternalToolUtils.prepareCommand(
+					tool.getCommand(baseFile != null), localFile, remoteFile,
+					mergedFile, baseFile);
+			// prepare the environment
+			Map<String, String> env = ExternalToolUtils.prepareEnvironment(
+					gitDir, localFile, remoteFile, mergedFile, baseFile);
+			boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE;
+			// execute the tool
+			CommandExecutor cmdExec = new CommandExecutor(fs, trust);
+			result = cmdExec.run(command, workTree, env);
+			// keep backup as .orig file
+			if (backup != null) {
+				keepBackupFile(mergedFile.getPath(), backup);
+			}
+			return result;
+		} catch (IOException | InterruptedException e) {
+			throw new ToolException(e);
+		} finally {
+			// always delete backup file (ignore that it was may be already
+			// moved to keep-backup file)
+			if (backup != null) {
+				backup.cleanTemporaries();
+			}
+			// if the tool returns an error and keepTemporaries is set to true,
+			// then these temporary files will be preserved
+			if (!((result == null) && config.isKeepTemporaries())) {
+				// delete the files
+				localFile.cleanTemporaries();
+				remoteFile.cleanTemporaries();
+				if (baseFile != null) {
+					baseFile.cleanTemporaries();
+				}
+				// delete temporary directory if needed
+				if (config.isWriteToTemp() && (tempDir != null)
+						&& tempDir.exists()) {
+					tempDir.delete();
+				}
+			}
+		}
+	}
+
+	private FileElement createBackupFile(FileElement from, File toParentDir)
+			throws IOException {
+		FileElement backup = null;
+		Path path = Paths.get(from.getPath());
+		if (Files.exists(path)) {
+			backup = new FileElement(from.getPath(), Type.BACKUP);
+			Files.copy(path, backup.createTempFile(toParentDir).toPath(),
+					StandardCopyOption.REPLACE_EXISTING);
+		}
+		return backup;
+	}
+
+	/**
+	 * Create temporary directory.
+	 *
+	 * @return the created temporary directory if (mergetol.writeToTemp == true)
+	 *         or null if not configured or false.
+	 * @throws IOException
+	 */
+	public File createTempDirectory() throws IOException {
+		return config.isWriteToTemp()
+				? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$
+				: null;
+	}
+
+	/**
+	 * Get user defined tool names.
+	 *
+	 * @return the user defined tool names
+	 */
+	public Set<String> getUserDefinedToolNames() {
+		return userDefinedTools.keySet();
+	}
+
+	/**
+	 * @return the predefined tool names
+	 */
+	public Set<String> getPredefinedToolNames() {
+		return predefinedTools.keySet();
+	}
+
+	/**
+	 * Get all tool names.
+	 *
+	 * @return the all tool names (default or available tool name is the first
+	 *         in the set)
+	 */
+	public Set<String> getAllToolNames() {
+		String defaultName = getDefaultToolName(false);
+		if (defaultName == null) {
+			defaultName = getFirstAvailableTool();
+		}
+		return ExternalToolUtils.createSortedToolSet(defaultName,
+				getUserDefinedToolNames(), getPredefinedToolNames());
+	}
+
+	/**
+	 * Provides {@link Optional} with the name of an external merge tool if
+	 * specified in git configuration for a path.
+	 *
+	 * The formed git configuration results from global rules as well as merged
+	 * rules from info and worktree attributes.
+	 *
+	 * Triggers {@link TreeWalk} until specified path found in the tree.
+	 *
+	 * @param path
+	 *            path to the node in repository to parse git attributes for
+	 * @return name of the difftool if set
+	 * @throws ToolException
+	 */
+	public Optional<String> getExternalToolFromAttributes(final String path)
+			throws ToolException {
+		return ExternalToolUtils.getExternalToolFromAttributes(repo, path,
+				ExternalToolUtils.KEY_MERGE_TOOL);
+	}
+
+	/**
+	 * Checks the availability of the predefined tools in the system.
+	 *
+	 * @return set of predefined available tools
+	 */
+	public Set<String> getPredefinedAvailableTools() {
+		Map<String, ExternalMergeTool> defTools = getPredefinedTools(true);
+		Set<String> availableTools = new LinkedHashSet<>();
+		for (Entry<String, ExternalMergeTool> elem : defTools.entrySet()) {
+			if (elem.getValue().isAvailable()) {
+				availableTools.add(elem.getKey());
+			}
+		}
+		return availableTools;
+	}
+
+	/**
+	 * @return the user defined tools
+	 */
+	public Map<String, ExternalMergeTool> getUserDefinedTools() {
+		return Collections.unmodifiableMap(userDefinedTools);
+	}
+
+	/**
+	 * Get predefined tools map.
+	 *
+	 * @param checkAvailability
+	 *            true: for checking if tools can be executed; ATTENTION: this
+	 *            check took some time, do not execute often (store the map for
+	 *            other actions); false: availability is NOT checked:
+	 *            isAvailable() returns default false is this case!
+	 * @return the predefined tools with optionally checked availability (long
+	 *         running operation)
+	 */
+	public Map<String, ExternalMergeTool> getPredefinedTools(
+			boolean checkAvailability) {
+		if (checkAvailability) {
+			for (ExternalMergeTool tool : predefinedTools.values()) {
+				PreDefinedMergeTool predefTool = (PreDefinedMergeTool) tool;
+				predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs,
+						gitDir, workTree, predefTool.getPath()));
+			}
+		}
+		return Collections.unmodifiableMap(predefinedTools);
+	}
+
+	/**
+	 * Get first available tool name.
+	 *
+	 * @return the name of first available predefined tool or null
+	 */
+	public String getFirstAvailableTool() {
+		String name = null;
+		for (ExternalMergeTool tool : predefinedTools.values()) {
+			if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree,
+					tool.getPath())) {
+				name = tool.getName();
+				break;
+			}
+		}
+		return name;
+	}
+
+	/**
+	 * Is interactive merge (prompt enabled) ?
+	 *
+	 * @return is interactive (config prompt enabled) ?
+	 */
+	public boolean isInteractive() {
+		return config.isPrompt();
+	}
+
+	/**
+	 * Get the default (gui-)tool name.
+	 *
+	 * @param gui
+	 *            use the diff.guitool setting ?
+	 * @return the default tool name
+	 */
+	public String getDefaultToolName(boolean gui) {
+		return gui ? config.getDefaultGuiToolName()
+				: config.getDefaultToolName();
+	}
+
+	private ExternalMergeTool getTool(final String name) {
+		ExternalMergeTool tool = userDefinedTools.get(name);
+		if (tool == null) {
+			tool = predefinedTools.get(name);
+		}
+		return tool;
+	}
+
+	private void keepBackupFile(String mergedFilePath, FileElement backup)
+			throws IOException {
+		if (config.isKeepBackup()) {
+			Path backupPath = backup.getFile().toPath();
+			Files.move(backupPath,
+					backupPath.resolveSibling(
+							Paths.get(mergedFilePath).getFileName() + ".orig"), //$NON-NLS-1$
+					StandardCopyOption.REPLACE_EXISTING);
+		}
+	}
+
+	private Map<String, ExternalMergeTool> setupPredefinedTools() {
+		Map<String, ExternalMergeTool> tools = new TreeMap<>();
+		for (CommandLineMergeTool tool : CommandLineMergeTool.values()) {
+			tools.put(tool.name(), new PreDefinedMergeTool(tool));
+		}
+		return tools;
+	}
+
+	private Map<String, ExternalMergeTool> setupUserDefinedTools(
+			Map<String, ExternalMergeTool> predefTools) {
+		Map<String, ExternalMergeTool> tools = new TreeMap<>();
+		Map<String, ExternalMergeTool> userTools = config.getTools();
+		for (String name : userTools.keySet()) {
+			ExternalMergeTool userTool = userTools.get(name);
+			// if mergetool.<name>.cmd is defined we have user defined tool
+			if (userTool.getCommand() != null) {
+				tools.put(name, userTool);
+			} else if (userTool.getPath() != null) {
+				// if mergetool.<name>.path is defined we just overload the path
+				// of predefined tool
+				PreDefinedMergeTool predefTool = (PreDefinedMergeTool) predefTools
+						.get(name);
+				if (predefTool != null) {
+					predefTool.setPath(userTool.getPath());
+					if (userTool.getTrustExitCode() != BooleanTriState.UNSET) {
+						predefTool
+								.setTrustExitCode(userTool.getTrustExitCode());
+					}
+				}
+			}
+		}
+		return tools;
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java
index 1c69fb4..e1169a2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java
@@ -46,17 +46,6 @@
 	 */
 	@Override
 	public void setPath(String path) {
-		// handling of spaces in path
-		if (path.contains(" ")) { //$NON-NLS-1$
-			// add quotes before if needed
-			if (!path.startsWith("\"")) { //$NON-NLS-1$
-				path = "\"" + path; //$NON-NLS-1$
-			}
-			// add quotes after if needed
-			if (!path.endsWith("\"")) { //$NON-NLS-1$
-				path = path + "\""; //$NON-NLS-1$
-			}
-		}
 		super.setPath(path);
 	}
 
@@ -67,7 +56,7 @@
 	 */
 	@Override
 	public String getCommand() {
-		return getPath() + " " + super.getCommand(); //$NON-NLS-1$
+		return ExternalToolUtils.quotePath(getPath()) + " " + super.getCommand(); //$NON-NLS-1$
 	}
 
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java
new file mode 100644
index 0000000..7b28d328
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+
+/**
+ * The pre-defined merge tool.
+ */
+public class PreDefinedMergeTool extends UserDefinedMergeTool {
+
+	/**
+	 * the tool parameters without base
+	 */
+	private final String parametersWithoutBase;
+
+	/**
+	 * Creates the pre-defined merge tool
+	 *
+	 * @param name
+	 *            the name
+	 * @param path
+	 *            the path
+	 * @param parametersWithBase
+	 *            the tool parameters that are used together with path as
+	 *            command and "base is present" ($BASE)
+	 * @param parametersWithoutBase
+	 *            the tool parameters that are used together with path as
+	 *            command and "base is present" ($BASE)
+	 * @param trustExitCode
+	 *            the "trust exit code" option
+	 */
+	public PreDefinedMergeTool(String name, String path,
+			String parametersWithBase, String parametersWithoutBase,
+			BooleanTriState trustExitCode) {
+		super(name, path, parametersWithBase, trustExitCode);
+		this.parametersWithoutBase = parametersWithoutBase;
+	}
+
+	/**
+	 * Creates the pre-defined merge tool
+	 *
+	 * @param tool
+	 *            the command line merge tool
+	 *
+	 */
+	public PreDefinedMergeTool(CommandLineMergeTool tool) {
+		this(tool.name(), tool.getPath(), tool.getParameters(true),
+				tool.getParameters(false),
+				tool.isExitCodeTrustable() ? BooleanTriState.TRUE
+						: BooleanTriState.FALSE);
+	}
+
+	/**
+	 * @param trustExitCode
+	 *            the "trust exit code" option
+	 */
+	@Override
+	public void setTrustExitCode(BooleanTriState trustExitCode) {
+		super.setTrustExitCode(trustExitCode);
+	}
+
+	/**
+	 * @return the tool command (with base present)
+	 */
+	@Override
+	public String getCommand() {
+		return getCommand(true);
+	}
+
+	/**
+	 * @param withBase
+	 *            get command with base present (true) or without base present
+	 *            (false)
+	 * @return the tool command
+	 */
+	@Override
+	public String getCommand(boolean withBase) {
+		return ExternalToolUtils.quotePath(getPath()) + " " //$NON-NLS-1$
+				+ (withBase ? super.getCommand() : parametersWithoutBase);
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java
new file mode 100644
index 0000000..6ad33df
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+/**
+ * A handler for when the diff/merge tool manager wants to prompt the user
+ * whether to continue
+ */
+public interface PromptContinueHandler {
+	/**
+	 * Prompt the user whether to continue with the next file by opening a given
+	 * tool.
+	 *
+	 * @param toolName
+	 *            The name of the tool to open
+	 * @return Whether the user wants to continue
+	 */
+	boolean prompt(String toolName);
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
new file mode 100644
index 0000000..7cc5bb5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import org.eclipse.jgit.util.FS.ExecutionResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tool exception for differentiation.
+ *
+ */
+public class ToolException extends Exception {
+
+	private final static Logger LOG = LoggerFactory
+			.getLogger(ToolException.class);
+
+	private final ExecutionResult result;
+
+	private final boolean commandExecutionError;
+
+	/**
+	 * the serial version UID
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 *
+	 */
+	public ToolException() {
+		this(null, null, false);
+	}
+
+	/**
+	 * @param message
+	 *            the exception message
+	 */
+	public ToolException(String message) {
+		this(message, null, false);
+	}
+
+	/**
+	 * @param message
+	 *            the exception message
+	 * @param result
+	 *            the execution result
+	 * @param commandExecutionError
+	 *            is command execution error happened ?
+	 */
+	public ToolException(String message, ExecutionResult result,
+			boolean commandExecutionError) {
+		super(message);
+		this.result = result;
+		this.commandExecutionError = commandExecutionError;
+	}
+
+	/**
+	 * @param message
+	 *            the exception message
+	 * @param cause
+	 *            the cause for throw
+	 */
+	public ToolException(String message, Throwable cause) {
+		super(message, cause);
+		result = null;
+		commandExecutionError = false;
+	}
+
+	/**
+	 * @param cause
+	 *            the cause for throw
+	 */
+	public ToolException(Throwable cause) {
+		super(cause);
+		result = null;
+		commandExecutionError = false;
+	}
+
+	/**
+	 * @return true if result is valid, false else
+	 */
+	public boolean isResult() {
+		return result != null;
+	}
+
+	/**
+	 * @return the execution result
+	 */
+	public ExecutionResult getResult() {
+		return result;
+	}
+
+	/**
+	 * @return true if command execution error appears, false otherwise
+	 */
+	public boolean isCommandExecutionError() {
+		return commandExecutionError;
+	}
+
+	/**
+	 * @return the result Stderr
+	 */
+	public String getResultStderr() {
+		if (result == null) {
+			return ""; //$NON-NLS-1$
+		}
+		try {
+			return new String(result.getStderr().toByteArray());
+		} catch (Exception e) {
+			LOG.warn("Failed to retrieve standard error output", e); //$NON-NLS-1$
+		}
+		return ""; //$NON-NLS-1$
+	}
+
+	/**
+	 * @return the result Stdout
+	 */
+	public String getResultStdout() {
+		if (result == null) {
+			return ""; //$NON-NLS-1$
+		}
+		try {
+			return new String(result.getStdout().toByteArray());
+		} catch (Exception e) {
+			LOG.warn("Failed to retrieve standard output", e); //$NON-NLS-1$
+		}
+		return ""; //$NON-NLS-1$
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java
index 012296e..eb72d01 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java
@@ -15,6 +15,8 @@
  */
 public class UserDefinedDiffTool implements ExternalDiffTool {
 
+	private boolean available;
+
 	/**
 	 * the diff tool name
 	 */
@@ -99,6 +101,23 @@
 	}
 
 	/**
+	 * @return availability of the tool: true if tool can be executed and false
+	 *         if not
+	 */
+	@Override
+	public boolean isAvailable() {
+		return available;
+	}
+
+	/**
+	 * @param available
+	 *            true if tool can be found and false if not
+	 */
+	public void setAvailable(boolean available) {
+		this.available = available;
+	}
+
+	/**
 	 * Overrides the path for the given tool. Equivalent to setting
 	 * {@code difftool.<tool>.path}.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java
new file mode 100644
index 0000000..1dd2f0d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com>
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.diffmergetool;
+
+import org.eclipse.jgit.lib.internal.BooleanTriState;
+
+/**
+ * The user-defined merge tool.
+ */
+public class UserDefinedMergeTool extends UserDefinedDiffTool
+		implements ExternalMergeTool {
+
+	/**
+	 * the merge tool "trust exit code" option
+	 */
+	private BooleanTriState trustExitCode;
+
+	/**
+	 * Creates the merge tool
+	 *
+	 * @param name
+	 *            the name
+	 * @param path
+	 *            the path
+	 * @param cmd
+	 *            the command
+	 * @param trustExitCode
+	 *            the "trust exit code" option
+	 */
+	public UserDefinedMergeTool(String name, String path, String cmd,
+			BooleanTriState trustExitCode) {
+		super(name, path, cmd);
+		this.trustExitCode = trustExitCode;
+	}
+	/**
+	 * @return the "trust exit code" flag
+	 */
+	@Override
+	public BooleanTriState getTrustExitCode() {
+		return trustExitCode;
+	}
+
+	/**
+	 * @param trustExitCode
+	 *            the new "trust exit code" flag
+	 */
+	protected void setTrustExitCode(BooleanTriState trustExitCode) {
+		this.trustExitCode = trustExitCode;
+	}
+
+	/**
+	 * @param withBase
+	 *            not used, because user-defined merge tool can only define one
+	 *            cmd -> it must handle with and without base present (empty)
+	 * @return the tool command
+	 */
+	@Override
+	public String getCommand(boolean withBase) {
+		return getCommand();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java
index 17bd863..a784af8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java
@@ -15,6 +15,7 @@
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.Equality;
 
 class PackFileSnapshot extends FileSnapshot {
 
@@ -61,7 +62,8 @@
 	}
 
 	boolean isChecksumChanged(File packFile) {
-		return wasChecksumChanged = checksum != MISSING_CHECKSUM
+		return wasChecksumChanged = !Equality.isSameInstance(checksum,
+				MISSING_CHECKSUM)
 				&& !checksum.equals(readChecksum(packFile));
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
index 1db4f74..d72f935 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
@@ -938,38 +938,27 @@
 	}
 
 	private PackedRefList readPackedRefs() throws IOException {
-		int maxStaleRetries = 5;
-		int retries = 0;
-		while (true) {
-			final FileSnapshot snapshot = FileSnapshot.save(packedRefsFile);
-			final MessageDigest digest = Constants.newMessageDigest();
-			try (BufferedReader br = new BufferedReader(new InputStreamReader(
-					new DigestInputStream(new FileInputStream(packedRefsFile),
-							digest),
-					UTF_8))) {
-				try {
-					return new PackedRefList(parsePackedRefs(br), snapshot,
-							ObjectId.fromRaw(digest.digest()));
-				} catch (IOException e) {
-					if (FileUtils.isStaleFileHandleInCausalChain(e)
-							&& retries < maxStaleRetries) {
-						if (LOG.isDebugEnabled()) {
-							LOG.debug(MessageFormat.format(
-									JGitText.get().packedRefsHandleIsStale,
-									Integer.valueOf(retries)), e);
+		try {
+			PackedRefList result = FileUtils.readWithRetries(packedRefsFile,
+					f -> {
+						FileSnapshot snapshot = FileSnapshot.save(f);
+						MessageDigest digest = Constants.newMessageDigest();
+						try (BufferedReader br = new BufferedReader(
+								new InputStreamReader(
+										new DigestInputStream(
+												new FileInputStream(f), digest),
+										UTF_8))) {
+							return new PackedRefList(parsePackedRefs(br),
+									snapshot,
+									ObjectId.fromRaw(digest.digest()));
 						}
-						retries++;
-						continue;
-					}
-					throw e;
-				}
-			} catch (FileNotFoundException noPackedRefs) {
-				if (packedRefsFile.exists()) {
-					throw noPackedRefs;
-				}
-				// Ignore it and leave the new list empty.
-				return NO_PACKED_REFS;
-			}
+					});
+			return result != null ? result : NO_PACKED_REFS;
+		} catch (IOException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new IOException(MessageFormat
+					.format(JGitText.get().cannotReadFile, packedRefsFile), e);
 		}
 	}
 
@@ -1121,40 +1110,55 @@
 		}
 
 		final int limit = 4096;
-		final byte[] buf;
-		FileSnapshot otherSnapshot = FileSnapshot.save(path);
-		try {
-			buf = IO.readSome(path, limit);
-		} catch (FileNotFoundException noFile) {
-			if (path.isFile()) {
-				throw noFile;
-			}
-			return null; // doesn't exist or no file; not a reference.
-		}
 
-		int n = buf.length;
+		class LooseItems {
+			final FileSnapshot snapshot;
+
+			final byte[] buf;
+
+			LooseItems(FileSnapshot snapshot, byte[] buf) {
+				this.snapshot = snapshot;
+				this.buf = buf;
+			}
+		}
+		LooseItems loose = null;
+		try {
+			loose = FileUtils.readWithRetries(path,
+					f -> new LooseItems(FileSnapshot.save(f),
+							IO.readSome(f, limit)));
+		} catch (IOException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new IOException(
+					MessageFormat.format(JGitText.get().cannotReadFile, path),
+					e);
+		}
+		if (loose == null) {
+			return null;
+		}
+		int n = loose.buf.length;
 		if (n == 0)
 			return null; // empty file; not a reference.
 
-		if (isSymRef(buf, n)) {
+		if (isSymRef(loose.buf, n)) {
 			if (n == limit)
 				return null; // possibly truncated ref
 
 			// trim trailing whitespace
-			while (0 < n && Character.isWhitespace(buf[n - 1]))
+			while (0 < n && Character.isWhitespace(loose.buf[n - 1]))
 				n--;
 			if (n < 6) {
-				String content = RawParseUtils.decode(buf, 0, n);
+				String content = RawParseUtils.decode(loose.buf, 0, n);
 				throw new IOException(MessageFormat.format(JGitText.get().notARef, name, content));
 			}
-			final String target = RawParseUtils.decode(buf, 5, n);
+			final String target = RawParseUtils.decode(loose.buf, 5, n);
 			if (ref != null && ref.isSymbolic()
 					&& ref.getTarget().getName().equals(target)) {
 				assert(currentSnapshot != null);
-				currentSnapshot.setClean(otherSnapshot);
+				currentSnapshot.setClean(loose.snapshot);
 				return ref;
 			}
-			return newSymbolicRef(otherSnapshot, name, target);
+			return newSymbolicRef(loose.snapshot, name, target);
 		}
 
 		if (n < OBJECT_ID_STRING_LENGTH)
@@ -1162,23 +1166,23 @@
 
 		final ObjectId id;
 		try {
-			id = ObjectId.fromString(buf, 0);
+			id = ObjectId.fromString(loose.buf, 0);
 			if (ref != null && !ref.isSymbolic()
 					&& id.equals(ref.getTarget().getObjectId())) {
 				assert(currentSnapshot != null);
-				currentSnapshot.setClean(otherSnapshot);
+				currentSnapshot.setClean(loose.snapshot);
 				return ref;
 			}
 
 		} catch (IllegalArgumentException notRef) {
-			while (0 < n && Character.isWhitespace(buf[n - 1]))
+			while (0 < n && Character.isWhitespace(loose.buf[n - 1]))
 				n--;
-			String content = RawParseUtils.decode(buf, 0, n);
+			String content = RawParseUtils.decode(loose.buf, 0, n);
 
 			throw new IOException(MessageFormat.format(JGitText.get().notARef,
 					name, content), notRef);
 		}
-		return new LooseUnpeeled(otherSnapshot, name, id);
+		return new LooseUnpeeled(loose.snapshot, name, id);
 	}
 
 	private static boolean isSymRef(byte[] buf, int n) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java
index 1c24aff..cda456c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java
@@ -142,6 +142,7 @@
 		return ptr;
 	}
 
+	@SuppressWarnings("ReferenceEquality")
 	private void add(AnyObjectId id, int objectType, int pathHash) {
 		ObjectToPack obj = new ObjectToPack(id, objectType);
 		obj.setEdge();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
index 55cc026..6a9b45b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020 Julian Ruppel <julian.ruppel@sap.com>
+ * Copyright (c) 2020, 2022 Julian Ruppel <julian.ruppel@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
@@ -29,6 +29,7 @@
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
 
 /**
  * The standard "commit" configuration parameters.
@@ -44,6 +45,9 @@
 
 	private static final String CUT = " ------------------------ >8 ------------------------\n"; //$NON-NLS-1$
 
+	private static final char[] COMMENT_CHARS = { '#', ';', '@', '!', '$', '%',
+			'^', '&', '|', ':' };
+
 	/**
 	 * How to clean up commit messages when committing.
 	 *
@@ -99,6 +103,10 @@
 
 	private CleanupMode cleanupMode;
 
+	private char commentCharacter = '#';
+
+	private boolean autoCommentChar = false;
+
 	private CommitConfig(Config rc) {
 		commitTemplatePath = rc.getString(ConfigConstants.CONFIG_COMMIT_SECTION,
 				null, ConfigConstants.CONFIG_KEY_COMMIT_TEMPLATE);
@@ -106,6 +114,18 @@
 				null, ConfigConstants.CONFIG_KEY_COMMIT_ENCODING);
 		cleanupMode = rc.getEnum(ConfigConstants.CONFIG_COMMIT_SECTION, null,
 				ConfigConstants.CONFIG_KEY_CLEANUP, CleanupMode.DEFAULT);
+		String comment = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_COMMENT_CHAR);
+		if (!StringUtils.isEmptyOrNull(comment)) {
+			if ("auto".equalsIgnoreCase(comment)) { //$NON-NLS-1$
+				autoCommentChar = true;
+			} else {
+				char first = comment.charAt(0);
+				if (first > ' ' && first < 127) {
+					commentCharacter = first;
+				}
+			}
+		}
 	}
 
 	/**
@@ -131,6 +151,51 @@
 	}
 
 	/**
+	 * Retrieves the comment character set by git config
+	 * {@code core.commentChar}.
+	 *
+	 * @return the character to use for comments in commit messages
+	 * @since 6.2
+	 */
+	public char getCommentChar() {
+		return commentCharacter;
+	}
+
+	/**
+	 * Determines the comment character to use for a particular text. If
+	 * {@code core.commentChar} is "auto", tries to determine an unused
+	 * character; if none is found, falls back to '#'. Otherwise returns the
+	 * character given by {@code core.commentChar}.
+	 *
+	 * @param text
+	 *            existing text
+	 *
+	 * @return the character to use
+	 * @since 6.2
+	 */
+	public char getCommentChar(String text) {
+		if (isAutoCommentChar()) {
+			char toUse = determineCommentChar(text);
+			if (toUse > 0) {
+				return toUse;
+			}
+			return '#';
+		}
+		return getCommentChar();
+	}
+
+	/**
+	 * Tells whether the comment character should be determined by choosing a
+	 * character not occurring in a commit message.
+	 *
+	 * @return {@code true} if git config {@code core.commentChar} is "auto"
+	 * @since 6.2
+	 */
+	public boolean isAutoCommentChar() {
+		return autoCommentChar;
+	}
+
+	/**
 	 * Retrieves the {@link CleanupMode} as given by git config
 	 * {@code commit.cleanup}.
 	 *
@@ -315,4 +380,41 @@
 		}
 		return false;
 	}
+
+	/**
+	 * Determines a comment character by choosing one from a limited set of
+	 * 7-bit ASCII characters that do not occur in the given text at the
+	 * beginning of any line. If none can be determined, {@code (char) 0} is
+	 * returned.
+	 *
+	 * @param text
+	 *            to get a comment character for
+	 * @return the comment character, or {@code (char) 0} if none could be
+	 *         determined
+	 * @since 6.2
+	 */
+	public static char determineCommentChar(String text) {
+		if (StringUtils.isEmptyOrNull(text)) {
+			return '#';
+		}
+		final boolean[] inUse = new boolean[127];
+		for (String line : text.split("\n")) { //$NON-NLS-1$
+			int len = line.length();
+			for (int i = 0; i < len; i++) {
+				char ch = line.charAt(i);
+				if (!Character.isWhitespace(ch)) {
+					if (ch >= 0 && ch < inUse.length) {
+						inUse[ch] = true;
+					}
+					break;
+				}
+			}
+		}
+		for (char candidate : COMMENT_CHARS) {
+			if (!inUse[candidate]) {
+				return candidate;
+			}
+		}
+		return (char) 0;
+	}
 }
\ No newline at end of file
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 2d83549..2b49f95 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -2,7 +2,7 @@
  * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
  * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com>
  * Copyright (C) 2012-2013, Robin Rosenberg
- * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> and others
+ * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -10,6 +10,7 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
+
 package org.eclipse.jgit.lib;
 
 /**
@@ -31,14 +32,14 @@
 	public static final String CONFIG_DIFF_SECTION = "diff";
 
 	/**
-	 * The "tool" key within "diff" section
+	 * The "tool" key within "diff" or "merge" section
 	 *
 	 * @since 6.1
 	 */
 	public static final String CONFIG_KEY_TOOL = "tool";
 
 	/**
-	 * The "guitool" key within "diff" section
+	 * The "guitool" key within "diff" or "merge" section
 	 *
 	 * @since 6.1
 	 */
@@ -52,21 +53,21 @@
 	public static final String CONFIG_DIFFTOOL_SECTION = "difftool";
 
 	/**
-	 * The "prompt" key within "difftool" section
+	 * The "prompt" key within "difftool" or "mergetool" section
 	 *
 	 * @since 6.1
 	 */
 	public static final String CONFIG_KEY_PROMPT = "prompt";
 
 	/**
-	 * The "trustExitCode" key within "difftool" section
+	 * The "trustExitCode" key within "difftool" or "mergetool.<name>." section
 	 *
 	 * @since 6.1
 	 */
 	public static final String CONFIG_KEY_TRUST_EXIT_CODE = "trustExitCode";
 
 	/**
-	 * The "cmd" key within "difftool.*." section
+	 * The "cmd" key within "difftool.*." or "mergetool.*." section
 	 *
 	 * @since 6.1
 	 */
@@ -130,6 +131,34 @@
 	public static final String CONFIG_MERGE_SECTION = "merge";
 
 	/**
+	 * The "mergetool" section
+	 *
+	 * @since 6.2
+	 */
+	public static final String CONFIG_MERGETOOL_SECTION = "mergetool";
+
+	/**
+	 * The "keepBackup" key within "mergetool" section
+	 *
+	 * @since 6.2
+	 */
+	public static final String CONFIG_KEY_KEEP_BACKUP = "keepBackup";
+
+	/**
+	 * The "keepTemporaries" key within "mergetool" section
+	 *
+	 * @since 6.2
+	 */
+	public static final String CONFIG_KEY_KEEP_TEMPORARIES = "keepTemporaries";
+
+	/**
+	 * The "writeToTemp" key within "mergetool" section
+	 *
+	 * @since 6.2
+	 */
+	public static final String CONFIG_KEY_WRITE_TO_TEMP = "writeToTemp";
+
+	/**
 	 * The "filter" section
 	 * @since 4.6
 	 */
@@ -209,6 +238,13 @@
 	public static final String CONFIG_KEY_FORCE_SIGN_ANNOTATED = "forceSignAnnotated";
 
 	/**
+	 * The "commentChar" key.
+	 *
+	 * @since 6.2
+	 */
+	public static final String CONFIG_KEY_COMMENT_CHAR = "commentChar";
+
+	/**
 	 * The "hooksPath" key.
 	 *
 	 * @since 5.6
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
index 4b1dbed..59775c4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2021, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -26,20 +26,41 @@
 	private static final Logger LOG = LoggerFactory
 			.getLogger(GpgSignatureVerifierFactory.class);
 
-	private static volatile GpgSignatureVerifierFactory defaultFactory = loadDefault();
+	private static class DefaultFactory {
 
-	private static GpgSignatureVerifierFactory loadDefault() {
-		try {
-			ServiceLoader<GpgSignatureVerifierFactory> loader = ServiceLoader
-					.load(GpgSignatureVerifierFactory.class);
-			Iterator<GpgSignatureVerifierFactory> iter = loader.iterator();
-			if (iter.hasNext()) {
-				return iter.next();
+		private static volatile GpgSignatureVerifierFactory defaultFactory = loadDefault();
+
+		private static GpgSignatureVerifierFactory loadDefault() {
+			try {
+				ServiceLoader<GpgSignatureVerifierFactory> loader = ServiceLoader
+						.load(GpgSignatureVerifierFactory.class);
+				Iterator<GpgSignatureVerifierFactory> iter = loader.iterator();
+				if (iter.hasNext()) {
+					return iter.next();
+				}
+			} catch (ServiceConfigurationError e) {
+				LOG.error(e.getMessage(), e);
 			}
-		} catch (ServiceConfigurationError e) {
-			LOG.error(e.getMessage(), e);
+			return null;
 		}
-		return null;
+
+		private DefaultFactory() {
+			// No instantiation
+		}
+
+		public static GpgSignatureVerifierFactory getDefault() {
+			return defaultFactory;
+		}
+
+		/**
+		 * Sets the default factory.
+		 *
+		 * @param factory
+		 *            the new default factory
+		 */
+		public static void setDefault(GpgSignatureVerifierFactory factory) {
+			defaultFactory = factory;
+		}
 	}
 
 	/**
@@ -48,7 +69,7 @@
 	 * @return the default factory or {@code null} if none set
 	 */
 	public static GpgSignatureVerifierFactory getDefault() {
-		return defaultFactory;
+		return DefaultFactory.getDefault();
 	}
 
 	/**
@@ -58,7 +79,7 @@
 	 *            the new default factory
 	 */
 	public static void setDefault(GpgSignatureVerifierFactory factory) {
-		defaultFactory = factory;
+		DefaultFactory.setDefault(factory);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
index 5b32cf0..b25a61b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Salesforce. and others
+ * Copyright (C) 2018, 2022 Salesforce 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
@@ -26,22 +26,38 @@
  * @since 5.3
  */
 public abstract class GpgSigner {
+
 	private static final Logger LOG = LoggerFactory.getLogger(GpgSigner.class);
 
-	private static GpgSigner defaultSigner = loadGpgSigner();
+	private static class DefaultSigner {
 
-	private static GpgSigner loadGpgSigner() {
-		try {
-			ServiceLoader<GpgSigner> loader = ServiceLoader
-					.load(GpgSigner.class);
-			Iterator<GpgSigner> iter = loader.iterator();
-			if (iter.hasNext()) {
-				return iter.next();
+		private static volatile GpgSigner defaultSigner = loadGpgSigner();
+
+		private static GpgSigner loadGpgSigner() {
+			try {
+				ServiceLoader<GpgSigner> loader = ServiceLoader
+						.load(GpgSigner.class);
+				Iterator<GpgSigner> iter = loader.iterator();
+				if (iter.hasNext()) {
+					return iter.next();
+				}
+			} catch (ServiceConfigurationError e) {
+				LOG.error(e.getMessage(), e);
 			}
-		} catch (ServiceConfigurationError e) {
-			LOG.error(e.getMessage(), e);
+			return null;
 		}
-		return null;
+
+		private DefaultSigner() {
+			// No instantiation
+		}
+
+		public static GpgSigner getDefault() {
+			return defaultSigner;
+		}
+
+		public static void setDefault(GpgSigner signer) {
+			defaultSigner = signer;
+		}
 	}
 
 	/**
@@ -50,7 +66,7 @@
 	 * @return the default signer, or <code>null</code>.
 	 */
 	public static GpgSigner getDefault() {
-		return defaultSigner;
+		return DefaultSigner.getDefault();
 	}
 
 	/**
@@ -61,7 +77,7 @@
 	 *            default.
 	 */
 	public static void setDefault(GpgSigner signer) {
-		GpgSigner.defaultSigner = signer;
+		DefaultSigner.setDefault(signer);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java
index 94e7c53..c11fca1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java
@@ -138,6 +138,7 @@
 	 *            the commit to test.
 	 * @return true if the given commit built on top of this commit.
 	 */
+	@SuppressWarnings("ReferenceEquality")
 	public final boolean isChild(PlotCommit c) {
 		for (PlotCommit a : children)
 			if (a == c)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java
index 18ea756..458f240 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java
@@ -92,6 +92,7 @@
 	}
 
 	/** {@inheritDoc} */
+	@SuppressWarnings("ReferenceEquality")
 	@Override
 	protected void enter(int index, PlotCommit<L> currCommit) {
 		setupChildren(currCommit);
@@ -188,6 +189,7 @@
 	 *            may be null if <code>currCommit</code> is the first commit on
 	 *            the lane
 	 */
+	@SuppressWarnings("ReferenceEquality")
 	private void handleBlockedLanes(final int index, final PlotCommit currCommit,
 			final PlotCommit childOnLane) {
 		for (PlotCommit child : currCommit.children) {
@@ -214,6 +216,7 @@
 	}
 
 	// Handles the case where currCommit is a non-first parent of the child
+	@SuppressWarnings("ReferenceEquality")
 	private PlotLane handleMerge(final int index, final PlotCommit currCommit,
 			final PlotCommit childOnLane, PlotCommit child, PlotLane laneToUse) {
 
@@ -287,6 +290,7 @@
 	 * @param child
 	 * @param laneToContinue
 	 */
+	@SuppressWarnings("ReferenceEquality")
 	private void drawLaneToChild(final int commitIndex, PlotCommit child,
 			PlotLane laneToContinue) {
 		for (int r = commitIndex - 1; r >= 0; r--) {
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 a50eaf1..a25948e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
@@ -143,8 +143,19 @@
 	 */
 	static final int TOPO_QUEUED = 1 << 6;
 
+	/**
+	 * Set on a RevCommit when a {@link TreeRevFilter} has been applied.
+	 * <p>
+	 * This flag is processed by the {@link RewriteGenerator} to check if a
+	 * {@link TreeRevFilter} has been applied.
+	 *
+	 * @see TreeRevFilter
+	 * @see RewriteGenerator
+	 */
+	static final int TREE_REV_FILTER_APPLIED = 1 << 7;
+
 	/** Number of flag bits we keep internal for our own use. See above flags. */
-	static final int RESERVED_FLAGS = 7;
+	static final int RESERVED_FLAGS = 8;
 
 	private static final int APP_FLAGS = -1 & ~((1 << RESERVED_FLAGS) - 1);
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java
index a928c2e..1adef07 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java
@@ -24,14 +24,7 @@
  * commit that matched the revision walker's filters.
  * <p>
  * This generator is the second phase of a path limited revision walk and
- * assumes it is receiving RevCommits from {@link TreeRevFilter},
- * after they have been fully buffered by {@link AbstractRevQueue}. The full
- * buffering is necessary to allow the simple loop used within our own
- * {@link #rewrite(RevCommit)} to pull completely through a strand of
- * {@link RevWalk#REWRITE} colored commits and come up with a simplification
- * that makes the DAG dense. Not fully buffering the commits first would cause
- * this loop to abort early, due to commits not being parsed and colored
- * correctly.
+ * assumes it is receiving RevCommits from {@link TreeRevFilter}.
  *
  * @see TreeRevFilter
  */
@@ -43,9 +36,12 @@
 
 	private final Generator source;
 
+	private final FIFORevQueue pending;
+
 	RewriteGenerator(Generator s) {
 		super(s.firstParent);
 		source = s;
+		pending = new FIFORevQueue(s.firstParent);
 	}
 
 	@Override
@@ -58,13 +54,23 @@
 		return source.outputType() & ~NEEDS_REWRITE;
 	}
 
+	@SuppressWarnings("ReferenceEquality")
 	@Override
 	RevCommit next() throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
-		final RevCommit c = source.next();
+		RevCommit c = pending.next();
+
 		if (c == null) {
-			return null;
+			c = source.next();
+			if (c == null) {
+				// We are done: Both the source generator and our internal list
+				// are completely exhausted.
+				return null;
+			}
 		}
+
+		applyFilterToParents(c);
+
 		boolean rewrote = false;
 		final RevCommit[] pList = c.parents;
 		final int nParents = pList.length;
@@ -90,10 +96,41 @@
 		return c;
 	}
 
-	private RevCommit rewrite(RevCommit p) {
+	/**
+	 * Makes sure that the {@link TreeRevFilter} has been applied to all parents
+	 * of this commit by the previous {@link PendingGenerator}.
+	 *
+	 * @param c
+	 * @throws MissingObjectException
+	 * @throws IncorrectObjectTypeException
+	 * @throws IOException
+	 */
+	private void applyFilterToParents(RevCommit c)
+			throws MissingObjectException, IncorrectObjectTypeException,
+			IOException {
+		for (RevCommit parent : c.parents) {
+			while ((parent.flags & RevWalk.TREE_REV_FILTER_APPLIED) == 0) {
+
+				RevCommit n = source.next();
+
+				if (n != null) {
+					pending.add(n);
+				} else {
+					// Source generator is exhausted; filter has been applied to
+					// all commits
+					return;
+				}
+
+			}
+
+		}
+	}
+
+	private RevCommit rewrite(RevCommit p) throws MissingObjectException,
+			IncorrectObjectTypeException, IOException {
 		for (;;) {
-			final RevCommit[] pList = p.parents;
-			if (pList.length > 1) {
+
+			if (p.parents.length > 1) {
 				// This parent is a merge, so keep it.
 				//
 				return p;
@@ -113,14 +150,16 @@
 				return p;
 			}
 
-			if (pList.length == 0) {
+			if (p.parents.length == 0) {
 				// We can't go back any further, other than to
 				// just delete the parent entirely.
 				//
 				return null;
 			}
 
-			p = pList[0];
+			applyFilterToParents(p.parents[0]);
+			p = p.parents[0];
+
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java
index bfcea6e..a79901c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java
@@ -125,12 +125,6 @@
 		}
 
 		if ((g.outputType() & NEEDS_REWRITE) != 0) {
-			// Correction for an upstream NEEDS_REWRITE is to buffer
-			// fully and then apply a rewrite generator that can
-			// pull through the rewrite chain and produce a dense
-			// output graph.
-			//
-			g = new FIFORevQueue(g);
 			g = new RewriteGenerator(g);
 		}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java
index 822fc53..92d7226 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java
@@ -41,6 +41,8 @@
 
 	private static final int UNINTERESTING = RevWalk.UNINTERESTING;
 
+	private static final int FILTER_APPLIED = RevWalk.TREE_REV_FILTER_APPLIED;
+
 	private final int rewriteFlag;
 	private final TreeWalk pathFilter;
 
@@ -101,6 +103,7 @@
 	public boolean include(RevWalk walker, RevCommit c)
 			throws StopWalkException, MissingObjectException,
 			IncorrectObjectTypeException, IOException {
+		c.flags |= FILTER_APPLIED;
 		// Reset the tree filter to scan this commit and parents.
 		//
 		RevCommit[] pList = c.parents;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
index 2443c4e..cba5e16 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
@@ -20,7 +20,6 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.text.MessageFormat;
 
@@ -37,15 +36,11 @@
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * The configuration file that is stored in the file of the file system.
  */
 public class FileBasedConfig extends StoredConfig {
-	private static final Logger LOG = LoggerFactory
-			.getLogger(FileBasedConfig.class);
 
 	private final File configFile;
 
@@ -115,16 +110,15 @@
 	 */
 	@Override
 	public void load() throws IOException, ConfigInvalidException {
-		final int maxRetries = 5;
-		int retryDelayMillis = 20;
-		int retries = 0;
-		while (true) {
-			final FileSnapshot oldSnapshot = snapshot;
-			final FileSnapshot newSnapshot;
-			// don't use config in this snapshot to avoid endless recursion
-			newSnapshot = FileSnapshot.saveNoConfig(getFile());
-			try {
-				final byte[] in = IO.readFully(getFile());
+		try {
+			FileSnapshot[] lastSnapshot = { null };
+			Boolean wasRead = FileUtils.readWithRetries(getFile(), f -> {
+				final FileSnapshot oldSnapshot = snapshot;
+				final FileSnapshot newSnapshot;
+				// don't use config in this snapshot to avoid endless recursion
+				newSnapshot = FileSnapshot.saveNoConfig(f);
+				lastSnapshot[0] = newSnapshot;
+				final byte[] in = IO.readFully(f);
 				final ObjectId newHash = hash(in);
 				if (hash.equals(newHash)) {
 					if (oldSnapshot.equals(newSnapshot)) {
@@ -145,47 +139,17 @@
 					snapshot = newSnapshot;
 					hash = newHash;
 				}
-				return;
-			} catch (FileNotFoundException noFile) {
-				// might be locked by another process (see exception Javadoc)
-				if (retries < maxRetries && configFile.exists()) {
-					if (LOG.isDebugEnabled()) {
-						LOG.debug(MessageFormat.format(
-								JGitText.get().configHandleMayBeLocked,
-								Integer.valueOf(retries)), noFile);
-					}
-					try {
-						Thread.sleep(retryDelayMillis);
-					} catch (InterruptedException e) {
-						Thread.currentThread().interrupt();
-					}
-					retries++;
-					retryDelayMillis *= 2; // max wait 1260 ms
-					continue;
-				}
-				if (configFile.exists()) {
-					throw noFile;
-				}
+				return Boolean.TRUE;
+			});
+			if (wasRead == null) {
 				clear();
-				snapshot = newSnapshot;
-				return;
-			} catch (IOException e) {
-				if (FileUtils.isStaleFileHandle(e)
-						&& retries < maxRetries) {
-					if (LOG.isDebugEnabled()) {
-						LOG.debug(MessageFormat.format(
-								JGitText.get().configHandleIsStale,
-								Integer.valueOf(retries)), e);
-					}
-					retries++;
-					continue;
-				}
-				throw new IOException(MessageFormat
-						.format(JGitText.get().cannotReadFile, getFile()), e);
-			} catch (ConfigInvalidException e) {
-				throw new ConfigInvalidException(MessageFormat
-						.format(JGitText.get().cannotReadFile, getFile()), e);
+				snapshot = lastSnapshot[0];
 			}
+		} catch (IOException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new ConfigInvalidException(MessageFormat
+					.format(JGitText.get().cannotReadFile, getFile()), e);
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
index f48e1e6..3f167cc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -1004,9 +1004,12 @@
 			OutputStream outputStream) throws IOException {
 		onReceivePack();
 		InputStream input = in;
-		if (sideband)
-			input = new SideBandInputStream(input, monitor, getMessageWriter(),
-					outputStream);
+		SideBandInputStream sidebandIn = null;
+		if (sideband) {
+			sidebandIn = new SideBandInputStream(input, monitor,
+					getMessageWriter(), outputStream);
+			input = sidebandIn;
+		}
 
 		try (ObjectInserter ins = local.newObjectInserter()) {
 			PackParser parser = ins.newPackParser(input);
@@ -1015,6 +1018,10 @@
 			parser.setLockMessage(lockMessage);
 			packLock = parser.parse(monitor);
 			ins.flush();
+		} finally {
+			if (sidebandIn != null) {
+				sidebandIn.drainMessages();
+			}
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
index b87a85d..b7be59d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -194,10 +194,11 @@
 					// the other data channels.
 					//
 					int b = in.read();
-					if (0 <= b)
+					if (0 <= b) {
 						throw new TransportException(uri, MessageFormat.format(
 								JGitText.get().expectedEOFReceived,
 								Character.valueOf((char) b)));
+					}
 				}
 			}
 		} catch (TransportException e) {
@@ -205,6 +206,9 @@
 		} catch (Exception e) {
 			throw new TransportException(uri, e.getMessage(), e);
 		} finally {
+			if (in instanceof SideBandInputStream) {
+				((SideBandInputStream) in).drainMessages();
+			}
 			close();
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
index 1c1aa7b..bb58a7e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
@@ -31,6 +31,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.NotSupportedException;
@@ -57,6 +58,12 @@
 	/** List of things we want to fetch from the remote repository. */
 	private final Collection<RefSpec> toFetch;
 
+	/**
+	 * List of things we don't want to fetch from the remote repository or to
+	 * the local repository.
+	 */
+	private final Collection<RefSpec> negativeRefSpecs;
+
 	/** Set of refs we will actually wind up asking to obtain. */
 	private final HashMap<ObjectId, Ref> askFor = new HashMap<>();
 
@@ -75,9 +82,12 @@
 
 	private Map<String, Ref> localRefs;
 
-	FetchProcess(Transport t, Collection<RefSpec> f) {
+	FetchProcess(Transport t, Collection<RefSpec> refSpecs) {
 		transport = t;
-		toFetch = f;
+		toFetch = refSpecs.stream().filter(refSpec -> !refSpec.isNegative())
+				.collect(Collectors.toList());
+		negativeRefSpecs = refSpecs.stream().filter(RefSpec::isNegative)
+				.collect(Collectors.toList());
 	}
 
 	void execute(ProgressMonitor monitor, FetchResult result,
@@ -403,8 +413,13 @@
 	private void expandWildcard(RefSpec spec, Set<Ref> matched)
 			throws TransportException {
 		for (Ref src : conn.getRefs()) {
-			if (spec.matchSource(src) && matched.add(src))
-				want(src, spec.expandFromSource(src));
+			if (spec.matchSource(src)) {
+				RefSpec expandedRefSpec = spec.expandFromSource(src);
+				if (!matchNegativeRefSpec(expandedRefSpec)
+						&& matched.add(src)) {
+					want(src, expandedRefSpec);
+				}
+			}
 		}
 	}
 
@@ -420,11 +435,27 @@
 		if (src == null) {
 			throw new TransportException(MessageFormat.format(JGitText.get().remoteDoesNotHaveSpec, want));
 		}
-		if (matched.add(src)) {
+		if (!matchNegativeRefSpec(spec) && matched.add(src)) {
 			want(src, spec);
 		}
 	}
 
+	private boolean matchNegativeRefSpec(RefSpec spec) {
+		for (RefSpec negativeRefSpec : negativeRefSpecs) {
+			if (negativeRefSpec.getSource() != null && spec.getSource() != null
+					&& negativeRefSpec.matchSource(spec.getSource())) {
+				return true;
+			}
+
+			if (negativeRefSpec.getDestination() != null
+					&& spec.getDestination() != null && negativeRefSpec
+							.matchDestination(spec.getDestination())) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	private boolean localHasObject(ObjectId id) throws TransportException {
 		try {
 			return transport.local.getObjectDatabase().has(id);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
index 942dad4..b59ae0c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> and others
+ * Copyright (C) 2008, 2022 Marek Zawirski <marek.zawirski@gmail.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -166,6 +166,7 @@
 					if (prePush != null) {
 						try {
 							prePush.setRefs(willBeAttempted);
+							prePush.setDryRun(transport.isDryRun());
 							prePush.call();
 						} catch (AbortedByHookException | IOException e) {
 							throw new TransportException(e.getMessage(), e);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
index 56d0036..e9134a1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
@@ -53,6 +53,9 @@
 	/** Is this the special ":" RefSpec? */
 	private boolean matching;
 
+	/** Is this a negative refspec. */
+	private boolean negative;
+
 	/**
 	 * How strict to be about wildcards.
 	 *
@@ -96,12 +99,23 @@
 		wildcard = false;
 		srcName = Constants.HEAD;
 		dstName = null;
+		negative =false;
 		allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH;
 	}
 
 	/**
 	 * Parse a ref specification for use during transport operations.
 	 * <p>
+	 * {@link RefSpec}s can be regular or negative, regular RefSpecs indicate
+	 * what to include in transport operations while negative RefSpecs indicate
+	 * what to exclude in fetch.
+	 * <p>
+	 * Negative {@link RefSpec}s can't be force, must have only source or
+	 * destination. Wildcard patterns are also supported in negative RefSpecs
+	 * but they can not go with {@code WildcardMode.REQUIRE_MATCH} because they
+	 * are natually one to many mappings.
+	 *
+	 * <p>
 	 * Specifications are typically one of the following forms:
 	 * <ul>
 	 * <li><code>refs/heads/master</code></li>
@@ -121,6 +135,12 @@
 	 * <li><code>refs/heads/*:refs/heads/master</code></li>
 	 * </ul>
 	 *
+	 * Negative specifications are usually like:
+	 * <ul>
+	 * <li><code>^:refs/heads/master</code></li>
+	 * <li><code>^refs/heads/*</code></li>
+	 * </ul>
+	 *
 	 * @param spec
 	 *            string describing the specification.
 	 * @param mode
@@ -133,11 +153,22 @@
 	public RefSpec(String spec, WildcardMode mode) {
 		this.allowMismatchedWildcards = mode;
 		String s = spec;
+
+		if (s.startsWith("^+") || s.startsWith("+^")) { //$NON-NLS-1$ //$NON-NLS-2$
+			throw new IllegalArgumentException(
+					JGitText.get().invalidNegativeAndForce);
+		}
+
 		if (s.startsWith("+")) { //$NON-NLS-1$
 			force = true;
 			s = s.substring(1);
 		}
 
+		if (s.startsWith("^")) { //$NON-NLS-1$
+			negative = true;
+			s = s.substring(1);
+		}
+
 		boolean matchPushSpec = false;
 		final int c = s.lastIndexOf(':');
 		if (c == 0) {
@@ -181,6 +212,21 @@
 			}
 			srcName = checkValid(s);
 		}
+
+		// Negative refspecs must only have dstName or srcName.
+		if (isNegative()) {
+			if (isNullOrEmpty(srcName) && isNullOrEmpty(dstName)) {
+				throw new IllegalArgumentException(MessageFormat
+						.format(JGitText.get().invalidRefSpec, spec));
+			}
+			if (!isNullOrEmpty(srcName) && !isNullOrEmpty(dstName)) {
+				throw new IllegalArgumentException(MessageFormat
+						.format(JGitText.get().invalidRefSpec, spec));
+			}
+			if(wildcard && mode == WildcardMode.REQUIRE_MATCH) {
+				throw new IllegalArgumentException(MessageFormat
+						.format(JGitText.get().invalidRefSpec, spec));}
+		}
 		matching = matchPushSpec;
 	}
 
@@ -205,13 +251,15 @@
 	 *             the specification is invalid.
 	 */
 	public RefSpec(String spec) {
-		this(spec, WildcardMode.REQUIRE_MATCH);
+		this(spec, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH //$NON-NLS-1$
+				: WildcardMode.REQUIRE_MATCH);
 	}
 
 	private RefSpec(RefSpec p) {
 		matching = false;
 		force = p.isForceUpdate();
 		wildcard = p.isWildcard();
+		negative = p.isNegative();
 		srcName = p.getSource();
 		dstName = p.getDestination();
 		allowMismatchedWildcards = p.allowMismatchedWildcards;
@@ -246,6 +294,10 @@
 	 */
 	public RefSpec setForceUpdate(boolean forceUpdate) {
 		final RefSpec r = new RefSpec(this);
+		if (forceUpdate && isNegative()) {
+			throw new IllegalArgumentException(
+					JGitText.get().invalidNegativeAndForce);
+		}
 		r.matching = matching;
 		r.force = forceUpdate;
 		return r;
@@ -265,6 +317,16 @@
 	}
 
 	/**
+	 * Check if this specification is a negative one.
+	 *
+	 * @return true if this specification is negative.
+	 * @since 6.2
+	 */
+	public boolean isNegative() {
+		return negative;
+	}
+
+	/**
 	 * Get the source ref description.
 	 * <p>
 	 * During a fetch this is the name of the ref on the remote repository we
@@ -435,6 +497,10 @@
 		return this;
 	}
 
+	private static boolean isNullOrEmpty(String refName) {
+		return refName == null || refName.isEmpty();
+	}
+
 	/**
 	 * Expand this specification to exactly match a ref.
 	 * <p>
@@ -570,6 +636,9 @@
 		if (isForceUpdate() != b.isForceUpdate()) {
 			return false;
 		}
+		if(isNegative() != b.isNegative()) {
+			return false;
+		}
 		if (isMatching()) {
 			return b.isMatching();
 		} else if (b.isMatching()) {
@@ -587,6 +656,9 @@
 		if (isForceUpdate()) {
 			r.append('+');
 		}
+		if(isNegative()) {
+			r.append('^');
+		}
 		if (isMatching()) {
 			r.append(':');
 		} else {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java
index 2f3160b..c4e105e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java
@@ -16,10 +16,7 @@
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
 
 import org.eclipse.jgit.lib.Config;
 
@@ -54,10 +51,6 @@
 
 	private static final String KEY_TIMEOUT = "timeout"; //$NON-NLS-1$
 
-	private static final String KEY_INSTEADOF = "insteadof"; //$NON-NLS-1$
-
-	private static final String KEY_PUSHINSTEADOF = "pushinsteadof"; //$NON-NLS-1$
-
 	private static final boolean DEFAULT_MIRROR = false;
 
 	/** Default value for {@link #getUploadPack()} if not specified. */
@@ -135,10 +128,10 @@
 		String val;
 
 		vlst = rc.getStringList(SECTION, name, KEY_URL);
-		Map<String, String> insteadOf = getReplacements(rc, KEY_INSTEADOF);
+		UrlConfig urls = new UrlConfig(rc);
 		uris = new ArrayList<>(vlst.length);
 		for (String s : vlst) {
-			uris.add(new URIish(replaceUri(s, insteadOf)));
+			uris.add(new URIish(urls.replace(s)));
 		}
 		String[] plst = rc.getStringList(SECTION, name, KEY_PUSHURL);
 		pushURIs = new ArrayList<>(plst.length);
@@ -148,11 +141,9 @@
 		if (pushURIs.isEmpty()) {
 			// Would default to the uris. If we have pushinsteadof, we must
 			// supply rewritten push uris.
-			Map<String, String> pushInsteadOf = getReplacements(rc,
-					KEY_PUSHINSTEADOF);
-			if (!pushInsteadOf.isEmpty()) {
+			if (urls.hasPushReplacements()) {
 				for (String s : vlst) {
-					String replaced = replaceUri(s, pushInsteadOf);
+					String replaced = urls.replacePush(s);
 					if (!s.equals(replaced)) {
 						pushURIs.add(new URIish(replaced));
 					}
@@ -248,39 +239,6 @@
 		rc.unset(SECTION, getName(), key);
 	}
 
-	private Map<String, String> getReplacements(final Config config,
-			final String keyName) {
-		final Map<String, String> replacements = new HashMap<>();
-		for (String url : config.getSubsections(KEY_URL))
-			for (String insteadOf : config.getStringList(KEY_URL, url, keyName))
-				replacements.put(insteadOf, url);
-		return replacements;
-	}
-
-	private String replaceUri(final String uri,
-			final Map<String, String> replacements) {
-		if (replacements.isEmpty()) {
-			return uri;
-		}
-		Entry<String, String> match = null;
-		for (Entry<String, String> replacement : replacements.entrySet()) {
-			// Ignore current entry if not longer than previous match
-			if (match != null
-					&& match.getKey().length() > replacement.getKey()
-							.length()) {
-				continue;
-			}
-			if (!uri.startsWith(replacement.getKey())) {
-				continue;
-			}
-			match = replacement;
-		}
-		if (match != null) {
-			return match.getValue() + uri.substring(match.getKey().length());
-		}
-		return uri;
-	}
-
 	/**
 	 * Get the local name this remote configuration is recognized as.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
index 8a8d977..96c7be5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -28,6 +28,8 @@
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Unmultiplexes the data portion of a side-band channel.
@@ -46,6 +48,10 @@
  * @since 4.11
  */
 public class SideBandInputStream extends InputStream {
+
+	private static final Logger LOG = LoggerFactory
+			.getLogger(SideBandInputStream.class);
+
 	static final int CH_DATA = 1;
 	static final int CH_PROGRESS = 2;
 	static final int CH_ERROR = 3;
@@ -210,6 +216,21 @@
 		monitor.beginTask(remote(currentTask), totalWorkUnits);
 	}
 
+	/**
+	 * Forces any buffered progress messages to be written.
+	 */
+	void drainMessages() {
+		if (!progressBuffer.isEmpty()) {
+			try {
+				progress("\n"); //$NON-NLS-1$
+			} catch (IOException e) {
+				// Just log; otherwise this IOException might hide a real
+				// TransportException
+				LOG.error(e.getMessage(), e);
+			}
+		}
+	}
+
 	private static String remote(String msg) {
 		String prefix = JGitText.get().prefixRemote;
 		StringBuilder r = new StringBuilder(prefix.length() + msg.length() + 1);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
index 1e98a56..a0194ea 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -36,15 +36,35 @@
  */
 public abstract class SshSessionFactory {
 
-	private static volatile SshSessionFactory INSTANCE = loadSshSessionFactory();
+	private static class DefaultFactory {
 
-	private static SshSessionFactory loadSshSessionFactory() {
-		ServiceLoader<SshSessionFactory> loader = ServiceLoader.load(SshSessionFactory.class);
-		Iterator<SshSessionFactory> iter = loader.iterator();
-		if(iter.hasNext()) {
-			return iter.next();
+		private static volatile SshSessionFactory INSTANCE = loadSshSessionFactory();
+
+		private static SshSessionFactory loadSshSessionFactory() {
+			ServiceLoader<SshSessionFactory> loader = ServiceLoader
+					.load(SshSessionFactory.class);
+			Iterator<SshSessionFactory> iter = loader.iterator();
+			if (iter.hasNext()) {
+				return iter.next();
+			}
+			return null;
 		}
-		return null;
+
+		private DefaultFactory() {
+			// No instantiation
+		}
+
+		public static SshSessionFactory getInstance() {
+			return INSTANCE;
+		}
+
+		public static void setInstance(SshSessionFactory newFactory) {
+			if (newFactory != null) {
+				INSTANCE = newFactory;
+			} else {
+				INSTANCE = loadSshSessionFactory();
+			}
+		}
 	}
 
 	/**
@@ -57,7 +77,7 @@
 	 * @return factory the current factory for this JVM.
 	 */
 	public static SshSessionFactory getInstance() {
-		return INSTANCE;
+		return DefaultFactory.getInstance();
 	}
 
 	/**
@@ -68,11 +88,7 @@
 	 *            {@code null} the default factory will be restored.
 	 */
 	public static void setInstance(SshSessionFactory newFactory) {
-		if (newFactory != null) {
-			INSTANCE = newFactory;
-		} else {
-			INSTANCE = loadSshSessionFactory();
-		}
+		DefaultFactory.setInstance(newFactory);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
index 0eab443..3222d63 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
@@ -1230,7 +1230,9 @@
 	 * @param toFetch
 	 *            specification of refs to fetch locally. May be null or the
 	 *            empty collection to use the specifications from the
-	 *            RemoteConfig. Source for each RefSpec can't be null.
+	 *            RemoteConfig. May contains regular and negative 
+	 *            {@link RefSpec}s. Source for each regular RefSpec can't
+	 *            be null.
 	 * @return information describing the tracking refs updated.
 	 * @throws org.eclipse.jgit.errors.NotSupportedException
 	 *             this transport implementation does not support fetching
@@ -1264,7 +1266,9 @@
 	 * @param toFetch
 	 *            specification of refs to fetch locally. May be null or the
 	 *            empty collection to use the specifications from the
-	 *            RemoteConfig. Source for each RefSpec can't be null.
+	 *            RemoteConfig. May contains regular and negative 
+	 *            {@link RefSpec}s. Source for each regular RefSpec can't
+	 *            be null.
 	 * @param branch
 	 *            the initial branch to check out when cloning the repository.
 	 *            Can be specified as ref name (<code>refs/heads/master</code>),
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java
new file mode 100644
index 0000000..574fcf8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Support for URL translations via git configs {@code url.<base>.insteadOf} and
+ * {@code url.<base>.pushInsteadOf}.
+ *
+ * @since 6.2
+ */
+public class UrlConfig {
+
+	private static final String KEY_INSTEADOF = "insteadof"; //$NON-NLS-1$
+
+	private static final String KEY_PUSHINSTEADOF = "pushinsteadof"; //$NON-NLS-1$
+
+	private static final String SECTION_URL = "url"; //$NON-NLS-1$
+
+	private final Config config;
+
+	private Map<String, String> insteadOf;
+
+	private Map<String, String> pushInsteadOf;
+
+	/**
+	 * Creates a new {@link UrlConfig} instance.
+	 *
+	 * @param config
+	 *            {@link Config} to read values from
+	 */
+	public UrlConfig(Config config) {
+		this.config = config;
+	}
+
+	/**
+	 * Performs replacements as defined by git config
+	 * {@code url.<base>.insteadOf}. If there is no match, the input is returned
+	 * unchanged.
+	 *
+	 * @param url
+	 *            to substitute
+	 * @return the {@code url} with substitution applied
+	 */
+	public String replace(String url) {
+		if (insteadOf == null) {
+			insteadOf = load(KEY_INSTEADOF);
+		}
+		return replace(url, insteadOf);
+	}
+
+	/**
+	 * Tells whether there are push replacements.
+	 *
+	 * @return {@code true} if there are push replacements, {@code false}
+	 *         otherwise
+	 */
+	public boolean hasPushReplacements() {
+		if (pushInsteadOf == null) {
+			pushInsteadOf = load(KEY_PUSHINSTEADOF);
+		}
+		return !pushInsteadOf.isEmpty();
+	}
+
+	/**
+	 * Performs replacements as defined by git config
+	 * {@code url.<base>.pushInsteadOf}. If there is no match, the input is
+	 * returned unchanged.
+	 *
+	 * @param url
+	 *            to substitute
+	 * @return the {@code url} with substitution applied
+	 */
+	public String replacePush(String url) {
+		if (pushInsteadOf == null) {
+			pushInsteadOf = load(KEY_PUSHINSTEADOF);
+		}
+		return replace(url, pushInsteadOf);
+	}
+
+	private Map<String, String> load(String key) {
+		Map<String, String> replacements = new HashMap<>();
+		for (String url : config.getSubsections(SECTION_URL)) {
+			for (String prefix : config.getStringList(SECTION_URL, url, key)) {
+				replacements.put(prefix, url);
+			}
+		}
+		return replacements;
+	}
+
+	private String replace(String uri, Map<String, String> replacements) {
+		Entry<String, String> match = null;
+		for (Entry<String, String> replacement : replacements.entrySet()) {
+			// Ignore current entry if not longer than previous match
+			if (match != null && match.getKey().length() > replacement.getKey()
+					.length()) {
+				continue;
+			}
+			if (uri.startsWith(replacement.getKey())) {
+				match = replacement;
+			}
+		}
+		if (match != null) {
+			return match.getValue() + uri.substring(match.getKey().length());
+		}
+		return uri;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java
new file mode 100644
index 0000000..da16846
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022, Fabio Ponciroli <ponch78@gmail.com> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.util;
+
+/**
+ * Equality utilities.
+ *
+ * @since: 6.2
+ */
+public class Equality {
+
+    /**
+     * Compare by reference
+     *
+     * @param a
+     *            First object to compare
+     * @param b
+     *            Second object to compare
+     * @return {@code true} if the objects are identical, {@code false}
+     *         otherwise
+     *
+     * @since 6.2
+     */
+    @SuppressWarnings("ReferenceEquality")
+    public static <T> boolean isSameInstance(T a, T b) {
+        return a == b;
+    }
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
index b9dd9ba..f013e7e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
@@ -17,6 +17,7 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InterruptedIOException;
 import java.nio.channels.FileChannel;
 import java.nio.file.AtomicMoveNotSupportedException;
 import java.nio.file.CopyOption;
@@ -655,6 +656,99 @@
 	}
 
 	/**
+	 * Like a {@link java.util.function.Function} but throwing an
+	 * {@link Exception}.
+	 *
+	 * @param <A>
+	 *            input type
+	 * @param <B>
+	 *            output type
+	 * @since 6.2
+	 */
+	@FunctionalInterface
+	public interface IOFunction<A, B> {
+
+		/**
+		 * Performs the function.
+		 *
+		 * @param t
+		 *            input to operate on
+		 * @return the output
+		 * @throws Exception
+		 *             if a problem occurs
+		 */
+		B apply(A t) throws Exception;
+	}
+
+	private static void backOff(long delay, IOException cause)
+			throws IOException {
+		try {
+			Thread.sleep(delay);
+		} catch (InterruptedException e) {
+			IOException interruption = new InterruptedIOException();
+			interruption.initCause(e);
+			interruption.addSuppressed(cause);
+			Thread.currentThread().interrupt(); // Re-set flag
+			throw interruption;
+		}
+	}
+
+	/**
+	 * Invokes the given {@link IOFunction}, performing a limited number of
+	 * re-tries if exceptions occur that indicate either a stale NFS file handle
+	 * or that indicate that the file may be written concurrently.
+	 *
+	 * @param <T>
+	 *            result type
+	 * @param file
+	 *            to read
+	 * @param reader
+	 *            for reading the file and creating an instance of {@code T}
+	 * @return the result of the {@code reader}, or {@code null} if the file
+	 *         does not exist
+	 * @throws Exception
+	 *             if a problem occurs
+	 * @since 6.2
+	 */
+	public static <T> T readWithRetries(File file,
+			IOFunction<File, ? extends T> reader)
+			throws Exception {
+		int maxStaleRetries = 5;
+		int retries = 0;
+		long backoff = 50;
+		while (true) {
+			try {
+				try {
+					return reader.apply(file);
+				} catch (IOException e) {
+					if (FileUtils.isStaleFileHandleInCausalChain(e)
+							&& retries < maxStaleRetries) {
+						if (LOG.isDebugEnabled()) {
+							LOG.debug(MessageFormat.format(
+									JGitText.get().packedRefsHandleIsStale,
+									Integer.valueOf(retries)), e);
+						}
+						retries++;
+						continue;
+					}
+					throw e;
+				}
+			} catch (FileNotFoundException noFile) {
+				if (!file.isFile()) {
+					return null;
+				}
+				// Probably Windows and some other thread is writing the file
+				// concurrently.
+				if (backoff > 1000) {
+					throw noFile;
+				}
+				backOff(backoff, noFile);
+				backoff *= 2; // 50, 100, 200, 400, 800 ms
+			}
+		}
+	}
+
+	/**
 	 * @param file
 	 * @return {@code true} if the passed file is a symbolic link
 	 */
diff --git a/pom.xml b/pom.xml
index 81093c0..787f001 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
   <groupId>org.eclipse.jgit</groupId>
   <artifactId>org.eclipse.jgit-parent</artifactId>
   <packaging>pom</packaging>
-  <version>6.1.1-SNAPSHOT</version>
+  <version>6.2.1-SNAPSHOT</version>
 
   <name>JGit - Parent</name>
   <url>${jgit-url}</url>
@@ -150,11 +150,11 @@
     <java.version>11</java.version>
     <bundle-manifest>${project.build.directory}/META-INF/MANIFEST.MF</bundle-manifest>
 
-    <jgit-last-release-version>6.0.0.202111291000-r</jgit-last-release-version>
+    <jgit-last-release-version>6.1.0.202203080745-r</jgit-last-release-version>
     <ant-version>1.10.12</ant-version>
     <apache-sshd-version>2.8.0</apache-sshd-version>
     <jsch-version>0.1.55</jsch-version>
-    <jzlib-version>1.1.1</jzlib-version>
+    <jzlib-version>1.1.3</jzlib-version>
     <javaewah-version>1.1.13</javaewah-version>
     <junit-version>4.13.2</junit-version>
     <test-fork-count>1C</test-fork-count>
@@ -168,7 +168,7 @@
     <httpcore-version>4.4.14</httpcore-version>
     <slf4j-version>1.7.30</slf4j-version>
     <maven-javadoc-plugin-version>3.3.1</maven-javadoc-plugin-version>
-    <tycho-extras-version>2.5.0</tycho-extras-version>
+    <tycho-extras-version>2.6.0</tycho-extras-version>
     <gson-version>2.8.9</gson-version>
     <bouncycastle-version>1.70</bouncycastle-version>
     <spotbugs-maven-plugin-version>4.3.0</spotbugs-maven-plugin-version>