Merge branch 'stable-5.10' into stable-5.11

* stable-5.10:
  Retry loose object read upon "Stale file handle" exception
  Ignore missing javadoc in test bundles

Change-Id: Ia385fa6b5d2fee64476793e06860a279bf2f6e36
diff --git a/.bazelversion b/.bazelversion
index 8faff82..fcdb2e1 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-4.0.0rc2
+4.0.0
diff --git a/BUILD b/BUILD
index be6dd76..184ab27 100644
--- a/BUILD
+++ b/BUILD
@@ -13,6 +13,8 @@
         "//org.eclipse.jgit.lfs:jgit-lfs",
         "//org.eclipse.jgit.lfs.server:jgit-lfs-server",
         "//org.eclipse.jgit.junit:junit",
+        "//org.eclipse.jgit.ssh.apache:ssh-apache",
+        "//org.eclipse.jgit.ssh.jsch:ssh-jsch",
     ],
     outs = ["all.zip"],
     cmd = " && ".join([
diff --git a/DEPENDENCIES b/DEPENDENCIES
new file mode 100644
index 0000000..bffe3d9
--- /dev/null
+++ b/DEPENDENCIES
@@ -0,0 +1,66 @@
+maven/mavencentral/args4j/args4j/2.33, MIT, approved, CQ11068
+maven/mavencentral/com.google.code.gson/gson/2.8.6, Apache-2.0, approved, CQ23102
+maven/mavencentral/com.googlecode.javaewah/JavaEWAH/1.1.7, 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/commons-codec/commons-codec/1.11, Apache-2.0, approved, CQ15971
+maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ10162
+maven/mavencentral/javax.servlet/javax.servlet-api/3.1.0, Apache-2.0 AND (CDDL-1.1 OR GPL-2.0 WITH Classpath-exception-2.0), approved, emo_ip_team
+maven/mavencentral/junit/junit/4.13, , approved, CQ22796
+maven/mavencentral/log4j/log4j/1.2.15, Apache-2.0, approved, CQ7837
+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
+maven/mavencentral/net.i2p.crypto/eddsa/0.3.0, CC0, approved, CQ17804
+maven/mavencentral/net.sf.jopt-simple/jopt-simple/4.6, MIT, approved, clearlydefined
+maven/mavencentral/org.apache.ant/ant-launcher/1.10.8, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560
+maven/mavencentral/org.apache.ant/ant/1.10.8, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560
+maven/mavencentral/org.apache.commons/commons-compress/1.19, Apache-2.0, approved, clearlydefined
+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, approved, CQ22761
+maven/mavencentral/org.apache.httpcomponents/httpcore/4.4.14, Apache-2.0, approved, CQ18704
+maven/mavencentral/org.apache.sshd/sshd-common/2.6.0, Apache-2.0 AND ISC, approved, CQ22992
+maven/mavencentral/org.apache.sshd/sshd-core/2.6.0, Apache-2.0 AND ISC, approved, CQ22992
+maven/mavencentral/org.apache.sshd/sshd-osgi/2.6.0, Apache-2.0 AND ISC, approved, CQ22992
+maven/mavencentral/org.apache.sshd/sshd-sftp/2.6.0, Apache-2.0 AND ISC, approved, CQ22993
+maven/mavencentral/org.assertj/assertj-core/3.14.0, Apache-2.0, approved, clearlydefined
+maven/mavencentral/org.bouncycastle/bcpg-jdk15on/1.65, Apache-2.0, approved, CQ21975
+maven/mavencentral/org.bouncycastle/bcpkix-jdk15on/1.65, MIT AND LicenseRef-Public-Domain, approved, CQ21976
+maven/mavencentral/org.bouncycastle/bcprov-jdk15on/1.65.01, MIT AND LicenseRef-Public-Domain, approved, CQ21977
+maven/mavencentral/org.eclipse.jetty/jetty-http/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-io/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-security/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-server/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-servlet/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-util-ajax/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-util/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ7063
+maven/mavencentral/org.mockito/mockito-core/2.23.0, MIT, approved, CQ17976
+maven/mavencentral/org.objenesis/objenesis/2.6, Apache-2.0, approved, CQ15478
+maven/mavencentral/org.openjdk.jmh/jmh-core/1.21, GPL-2.0, approved, CQ20517
+maven/mavencentral/org.openjdk.jmh/jmh-generator-annprocess/1.21, GPL-2.0, approved, CQ20518
+maven/mavencentral/org.osgi/org.osgi.core/4.3.1, Apache-2.0, approved, CQ10111
+maven/mavencentral/org.slf4j/slf4j-api/1.7.30, MIT, approved, CQ13368
+maven/mavencentral/org.slf4j/slf4j-log4j12/1.7.30, MIT, approved, CQ7665
+maven/mavencentral/org.tukaani/xz/1.8, LicenseRef-Public-Domain, approved, CQ15386
diff --git a/WORKSPACE b/WORKSPACE
index ad04d14..224968a 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -111,26 +111,26 @@
 
 maven_jar(
     name = "httpclient",
-    artifact = "org.apache.httpcomponents:httpclient:4.5.10",
-    sha1 = "7ca2e4276f4ef95e4db725a8cd4a1d1e7585b9e5",
+    artifact = "org.apache.httpcomponents:httpclient:4.5.13",
+    sha1 = "e5f6cae5ca7ecaac1ec2827a9e2d65ae2869cada",
 )
 
 maven_jar(
     name = "httpcore",
-    artifact = "org.apache.httpcomponents:httpcore:4.4.12",
-    sha1 = "21ebaf6d532bc350ba95bd81938fa5f0e511c132",
+    artifact = "org.apache.httpcomponents:httpcore:4.4.14",
+    sha1 = "9dd1a631c082d92ecd4bd8fd4cf55026c720a8c1",
 )
 
 maven_jar(
     name = "sshd-osgi",
-    artifact = "org.apache.sshd:sshd-osgi:2.4.0",
-    sha1 = "fc4551c1eeda35e4671b263297d37d2bca81c4d4",
+    artifact = "org.apache.sshd:sshd-osgi:2.6.0",
+    sha1 = "40e365bb799e1bff3d31dc858b1e59a93c123f29",
 )
 
 maven_jar(
     name = "sshd-sftp",
-    artifact = "org.apache.sshd:sshd-sftp:2.4.0",
-    sha1 = "92e1b7d1e19c715efb4a8871d34145da8f87cdb2",
+    artifact = "org.apache.sshd:sshd-sftp:2.6.0",
+    sha1 = "6eddfe8fdf59a3d9a49151e4177f8c1bebeb30c9",
 )
 
 maven_jar(
@@ -233,52 +233,59 @@
 
 maven_jar(
     name = "gson",
-    artifact = "com.google.code.gson:gson:2.8.2",
-    sha1 = "3edcfe49d2c6053a70a2a47e4e1c2f94998a49cf",
+    artifact = "com.google.code.gson:gson:2.8.6",
+    sha1 = "9180733b7df8542621dc12e21e87557e8c99b8cb",
 )
 
-JETTY_VER = "9.4.30.v20200611"
+JETTY_VER = "9.4.36.v20210114"
 
 maven_jar(
     name = "jetty-servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VER,
-    sha1 = "ca3dea2cd34ee88cec017001603af0c9e74781d6",
-    src_sha1 = "6908f24428060bd542bddfa3e89e03d0dbbc2a6d",
+    sha1 = "b189e52a5ee55ae172e4e99e29c5c314f5daf4b9",
+    src_sha1 = "3a0fa449772ab0d76625f6afb81f60c32a490613",
 )
 
 maven_jar(
     name = "jetty-security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VER,
-    sha1 = "1a5261f6ad4081ad9e9bb01416d639931d391273",
-    src_sha1 = "6ca41b34aa4f84c267603edd4b069122bd5f17d3",
+    sha1 = "42030d6ed7dfc0f75818cde0adcf738efc477574",
+    src_sha1 = "612220a97d45fad3983ccc56b0cb9a271f3fd003",
 )
 
 maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VER,
-    sha1 = "e5ede3724d062717d0c04e4c77f74fe8115c2a6f",
-    src_sha1 = "c8b02a47a35c1f083b310cbd202738cf08bc1d55",
+    sha1 = "88a7d342974aadca658e7386e8d0fcc5c0788f41",
+    src_sha1 = "4552c0c6db2948e8557db477b6b48d291006e481",
 )
 
 maven_jar(
     name = "jetty-http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VER,
-    sha1 = "cd6223382e4f82b9ea807d8cdb04a23e5d629f1c",
-    src_sha1 = "00520c04b10609b981159b5ca284b5a158c077a9",
+    sha1 = "1eee89a55e04ff94df0f85d95200fc48acb43d86",
+    src_sha1 = "552a784ec789c7ba581c5341ae6d8b6353ed5ace",
 )
 
 maven_jar(
     name = "jetty-io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VER,
-    sha1 = "9c360d08e903b2dbd5d1f8e889a32046948628ce",
-    src_sha1 = "dac8f8a3f84afdd3686d36f58b5ccb276961b8ce",
+    sha1 = "84a8faf9031eb45a5a2ddb7681e22c483d81ab3a",
+    src_sha1 = "72d5fc6d909e28f8720394b25babda80805a46b9",
 )
 
 maven_jar(
     name = "jetty-util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VER,
-    sha1 = "39ec6aa4745952077f5407cb1394d8ba2db88b13",
-    src_sha1 = "f41f9391f91884a79350f3ad9b09b8e46c9be0ec",
+    sha1 = "925257fbcca6b501a25252c7447dbedb021f7404",
+    src_sha1 = "532e8b66044f4e58ca5da3aec19f02a2f3c16ddd",
+)
+
+maven_jar(
+    name = "jetty-util-ajax",
+    artifact = "org.eclipse.jetty:jetty-util-ajax:" + JETTY_VER,
+    sha1 = "2f478130c21787073facb64d7242e06f94980c60",
+    src_sha1 = "7153d7ca38878d971fd90992c303bb7719ba7a21",
 )
 
 BOUNCYCASTLE_VER = "1.65"
diff --git a/lib/BUILD b/lib/BUILD
index 7720696..8ad49d4 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -132,7 +132,10 @@
     name = "jetty-servlet",
     # TODO: This should be testonly but org.eclipse.jgit.pgm depends on it.
     visibility = ["//visibility:public"],
-    exports = ["@jetty-servlet//jar"],
+    exports = [
+        "@jetty-servlet//jar",
+        "@jetty-util-ajax//jar",
+    ],
 )
 
 java_library(
@@ -159,6 +162,7 @@
         "//org.eclipse.jgit:__pkg__",
         "//org.eclipse.jgit.gpg.bc:__pkg__",
         "//org.eclipse.jgit.test:__pkg__",
+        "//org.eclipse.jgit.gpg.bc.test:__pkg__",
     ],
     exports = ["@bcpg//jar"],
 )
@@ -169,6 +173,7 @@
         "//org.eclipse.jgit:__pkg__",
         "//org.eclipse.jgit.gpg.bc:__pkg__",
         "//org.eclipse.jgit.test:__pkg__",
+        "//org.eclipse.jgit.gpg.bc.test:__pkg__",
     ],
     exports = ["@bcprov//jar"],
 )
diff --git a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
index e1c5192..e0d802e 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: org.apache.tools.ant,
- org.eclipse.jgit.ant.tasks;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.ant.tasks;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
  org.hamcrest.core;version="[1.1.0,2.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 002c7fe..155e763 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 ea2bd15..f3ecc34 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: org.apache.tools.ant,
-  org.eclipse.jgit.storage.file;version="[5.10.1,5.11.0)"
+  org.eclipse.jgit.storage.file;version="[5.11.2,5.12.0)"
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.ant;version="5.10.1",
- org.eclipse.jgit.ant.tasks;version="5.10.1";
+Export-Package: org.eclipse.jgit.ant;version="5.11.2",
+ org.eclipse.jgit.ant.tasks;version="5.11.2";
   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 aacb3c4..4ed9bc0 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ant;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ant;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.ant/pom.xml b/org.eclipse.jgit.ant/pom.xml
index 927fe4c..0ef0f28 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ant</artifactId>
diff --git a/org.eclipse.jgit.archive/.settings/.api_filters b/org.eclipse.jgit.archive/.settings/.api_filters
new file mode 100644
index 0000000..f4a934a
--- /dev/null
+++ b/org.eclipse.jgit.archive/.settings/.api_filters
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<component id="org.eclipse.jgit.archive" version="2">
+    <resource path="src/org/eclipse/jgit/archive/BaseFormat.java" type="org.eclipse.jgit.archive.BaseFormat">
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.archive.BaseFormat"/>
+                <message_argument value="COMPRESSION_LEVEL"/>
+            </message_arguments>
+        </filter>
+    </resource>
+</component>
diff --git a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
index 0f98722..a772db1 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
@@ -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="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.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="5.10.1";
+Export-Package: org.eclipse.jgit.archive;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.api,
    org.apache.commons.compress.archivers,
    org.osgi.framework",
- org.eclipse.jgit.archive.internal;version="5.10.1";x-internal:=true
+ org.eclipse.jgit.archive.internal;version="5.11.2";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 7c8f179..749a21e 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.archive;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.archive;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.archive/pom.xml b/org.eclipse.jgit.archive/pom.xml
index a11e015..6cb8675 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.archive</artifactId>
diff --git a/org.eclipse.jgit.archive/resources/org/eclipse/jgit/archive/internal/ArchiveText.properties b/org.eclipse.jgit.archive/resources/org/eclipse/jgit/archive/internal/ArchiveText.properties
index 3b50bb4..e6e1227 100644
--- a/org.eclipse.jgit.archive/resources/org/eclipse/jgit/archive/internal/ArchiveText.properties
+++ b/org.eclipse.jgit.archive/resources/org/eclipse/jgit/archive/internal/ArchiveText.properties
@@ -1,3 +1,4 @@
 cannotSetOption=Cannot set option: {0}
+invalidCompressionLevel=Invalid compression level: {0}
 pathDoesNotMatchMode=Path {0} does not match mode {1}
 unsupportedMode=Unsupported mode {0}
diff --git a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/BaseFormat.java b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/BaseFormat.java
index 27f001e..0ebac77 100644
--- a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/BaseFormat.java
+++ b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/BaseFormat.java
@@ -25,6 +25,11 @@
  * @since 4.0
  */
 public class BaseFormat {
+	/**
+	 * Compression-level for the archive file. Only values in [0-9] are allowed.
+	 * @since 5.11
+	 */
+	protected static final String COMPRESSION_LEVEL = "compression-level"; //$NON-NLS-1$
 
 	/**
 	 * Apply options to archive output stream
@@ -40,6 +45,9 @@
 			Map<String, Object> o) throws IOException {
 		for (Map.Entry<String, Object> p : o.entrySet()) {
 			try {
+				if (p.getKey().equals(COMPRESSION_LEVEL)) {
+					continue;
+				}
 				new Statement(s, "set" + StringUtils.capitalize(p.getKey()), //$NON-NLS-1$
 						new Object[] { p.getValue() }).execute();
 			} catch (Exception e) {
@@ -49,4 +57,32 @@
 		}
 		return s;
 	}
+
+	/**
+	 * Removes and returns the {@link #COMPRESSION_LEVEL} key from the input map
+	 * parameter if it exists, or -1 if this key does not exist.
+	 *
+	 * @param o
+	 *            options map
+	 * @return The compression level if it exists in the map, or -1 instead.
+	 * @throws IllegalArgumentException
+	 *             if the {@link #COMPRESSION_LEVEL} option does not parse to an
+	 *             Integer.
+	 * @since 5.11
+	 */
+	protected int getCompressionLevel(Map<String, Object> o) {
+		if (!o.containsKey(COMPRESSION_LEVEL)) {
+			return -1;
+		}
+		Object option = o.get(COMPRESSION_LEVEL);
+		try {
+			Integer compressionLevel = (Integer) option;
+			return compressionLevel.intValue();
+		} catch (ClassCastException e) {
+			throw new IllegalArgumentException(
+					MessageFormat.format(
+							ArchiveText.get().invalidCompressionLevel, option),
+					e);
+		}
+	}
 }
diff --git a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/Tbz2Format.java b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/Tbz2Format.java
index e880f5e..940dafd 100644
--- a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/Tbz2Format.java
+++ b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/Tbz2Format.java
@@ -45,7 +45,13 @@
 	@Override
 	public ArchiveOutputStream createArchiveOutputStream(OutputStream s,
 			Map<String, Object> o) throws IOException {
-		BZip2CompressorOutputStream out = new BZip2CompressorOutputStream(s);
+		BZip2CompressorOutputStream out;
+		int compressionLevel = getCompressionLevel(o);
+		if (compressionLevel != -1) {
+			out = new BZip2CompressorOutputStream(s, compressionLevel);
+		} else {
+			out = new BZip2CompressorOutputStream(s);
+		}
 		return tarFormat.createArchiveOutputStream(out, o);
 	}
 
diff --git a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/TgzFormat.java b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/TgzFormat.java
index 859a59d..72e2439 100644
--- a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/TgzFormat.java
+++ b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/TgzFormat.java
@@ -18,6 +18,7 @@
 
 import org.apache.commons.compress.archivers.ArchiveOutputStream;
 import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
+import org.apache.commons.compress.compressors.gzip.GzipParameters;
 import org.eclipse.jgit.api.ArchiveCommand;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
@@ -45,7 +46,15 @@
 	@Override
 	public ArchiveOutputStream createArchiveOutputStream(OutputStream s,
 			Map<String, Object> o) throws IOException {
-		GzipCompressorOutputStream out = new GzipCompressorOutputStream(s);
+		GzipCompressorOutputStream out;
+		int compressionLevel = getCompressionLevel(o);
+		if (compressionLevel != -1) {
+			GzipParameters parameters = new GzipParameters();
+			parameters.setCompressionLevel(compressionLevel);
+			out = new GzipCompressorOutputStream(s, parameters);
+		} else {
+			out = new GzipCompressorOutputStream(s);
+		}
 		return tarFormat.createArchiveOutputStream(out, o);
 	}
 
diff --git a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/TxzFormat.java b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/TxzFormat.java
index 484ab57..b16fb6d 100644
--- a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/TxzFormat.java
+++ b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/TxzFormat.java
@@ -45,7 +45,13 @@
 	@Override
 	public ArchiveOutputStream createArchiveOutputStream(OutputStream s,
 			Map<String, Object> o) throws IOException {
-		XZCompressorOutputStream out = new XZCompressorOutputStream(s);
+		XZCompressorOutputStream out;
+		int compressionLevel = getCompressionLevel(o);
+		if (compressionLevel != -1) {
+			out = new XZCompressorOutputStream(s, compressionLevel);
+		} else {
+			out = new XZCompressorOutputStream(s);
+		}
 		return tarFormat.createArchiveOutputStream(out, o);
 	}
 
diff --git a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/ZipFormat.java b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/ZipFormat.java
index 59a9765..97a24c7 100644
--- a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/ZipFormat.java
+++ b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/ZipFormat.java
@@ -47,7 +47,12 @@
 	@Override
 	public ArchiveOutputStream createArchiveOutputStream(OutputStream s,
 			Map<String, Object> o) throws IOException {
-		return applyFormatOptions(new ZipArchiveOutputStream(s), o);
+		ZipArchiveOutputStream out = new ZipArchiveOutputStream(s);
+		int compressionLevel = getCompressionLevel(o);
+		if (compressionLevel != -1) {
+			out.setLevel(compressionLevel);
+		}
+		return applyFormatOptions(out, o);
 	}
 
 	/** {@inheritDoc} */
diff --git a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/internal/ArchiveText.java b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/internal/ArchiveText.java
index 45f96fa..551646b 100644
--- a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/internal/ArchiveText.java
+++ b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/internal/ArchiveText.java
@@ -28,6 +28,7 @@
 
 	// @formatter:off
 	/***/ public String cannotSetOption;
+	/***/ public String invalidCompressionLevel;
 	/***/ public String pathDoesNotMatchMode;
 	/***/ public String unsupportedMode;
 }
diff --git a/org.eclipse.jgit.benchmarks/.settings/edu.umd.cs.findbugs.core.prefs b/org.eclipse.jgit.benchmarks/.settings/edu.umd.cs.findbugs.core.prefs
new file mode 100644
index 0000000..1c0a344
--- /dev/null
+++ b/org.eclipse.jgit.benchmarks/.settings/edu.umd.cs.findbugs.core.prefs
@@ -0,0 +1,145 @@
+#SpotBugs User Preferences
+#Fri Dec 04 10:39:51 CET 2020
+detectorExplicitSerialization=ExplicitSerialization|true
+detectorMultithreadedInstanceAccess=MultithreadedInstanceAccess|true
+detectorConfusionBetweenInheritedAndOuterMethod=ConfusionBetweenInheritedAndOuterMethod|true
+detectorWrongMapIterator=WrongMapIterator|true
+detectorUnnecessaryMath=UnnecessaryMath|true
+detectorUselessSubclassMethod=UselessSubclassMethod|false
+filter_settings=Medium|BAD_PRACTICE,CORRECTNESS,EXPERIMENTAL,I18N,MALICIOUS_CODE,MT_CORRECTNESS,PERFORMANCE,SECURITY,STYLE|false|15
+detectorURLProblems=URLProblems|true
+detectorIteratorIdioms=IteratorIdioms|true
+detectorMutableEnum=MutableEnum|true
+detectorFindNonShortCircuit=FindNonShortCircuit|true
+detectorSynchronizeAndNullCheckField=SynchronizeAndNullCheckField|true
+detectorVolatileUsage=VolatileUsage|true
+detectorFindNakedNotify=FindNakedNotify|true
+detectorFindUninitializedGet=FindUninitializedGet|true
+detectorFindUseOfNonSerializableValue=FindUseOfNonSerializableValue|true
+detectorFindJSR166LockMonitorenter=FindJSR166LockMonitorenter|true
+detectorQuestionableBooleanAssignment=QuestionableBooleanAssignment|true
+detectorSwitchFallthrough=SwitchFallthrough|true
+detectorFindLocalSelfAssignment2=FindLocalSelfAssignment2|true
+detectorConfusedInheritance=ConfusedInheritance|true
+detectorSynchronizationOnSharedBuiltinConstant=SynchronizationOnSharedBuiltinConstant|true
+detectorMutableStaticFields=MutableStaticFields|true
+detectorInvalidJUnitTest=InvalidJUnitTest|true
+detectorInfiniteLoop=InfiniteLoop|true
+detectorFindRunInvocations=FindRunInvocations|true
+detectorBadSyntaxForRegularExpression=BadSyntaxForRegularExpression|true
+detectorXMLFactoryBypass=XMLFactoryBypass|true
+detectorFindOpenStream=FindOpenStream|true
+detectorCheckExpectedWarnings=CheckExpectedWarnings|false
+detectorHugeSharedStringConstants=HugeSharedStringConstants|true
+detectorLostLoggerDueToWeakReference=LostLoggerDueToWeakReference|true
+detectorStringConcatenation=StringConcatenation|true
+detectorLoadOfKnownNullValue=LoadOfKnownNullValue|true
+detectorFinalizerNullsFields=FinalizerNullsFields|true
+detectorFindFieldSelfAssignment=FindFieldSelfAssignment|true
+detectorInefficientToArray=InefficientToArray|false
+detectorDontCatchIllegalMonitorStateException=DontCatchIllegalMonitorStateException|true
+detectorInconsistentAnnotations=InconsistentAnnotations|true
+detectorBadlyOverriddenAdapter=BadlyOverriddenAdapter|true
+detectorInstantiateStaticClass=InstantiateStaticClass|true
+detectorCheckRelaxingNullnessAnnotation=CheckRelaxingNullnessAnnotation|true
+detectorMethodReturnCheck=MethodReturnCheck|true
+detectorEqualsOperandShouldHaveClassCompatibleWithThis=EqualsOperandShouldHaveClassCompatibleWithThis|true
+detectorFindDoubleCheck=FindDoubleCheck|true
+detectorFindBadForLoop=FindBadForLoop|true
+detectorDefaultEncodingDetector=DefaultEncodingDetector|true
+detectorFindInconsistentSync2=FindInconsistentSync2|true
+detectorFindSpinLoop=FindSpinLoop|true
+detectorFindMaskedFields=FindMaskedFields|true
+detectorBooleanReturnNull=BooleanReturnNull|true
+detectorFindUnsyncGet=FindUnsyncGet|true
+detectorCrossSiteScripting=CrossSiteScripting|true
+detectorDroppedException=DroppedException|true
+detectorFindDeadLocalStores=FindDeadLocalStores|true
+detectorCheckImmutableAnnotation=CheckImmutableAnnotation|true
+detectorInfiniteRecursiveLoop=InfiniteRecursiveLoop|true
+detectorFindRefComparison=FindRefComparison|true
+detectorFindRoughConstants=FindRoughConstants|true
+detectorMutableLock=MutableLock|true
+detectorFindNullDeref=FindNullDeref|true
+detectorFindReturnRef=FindReturnRef|true
+detectorSynchronizeOnClassLiteralNotGetClass=SynchronizeOnClassLiteralNotGetClass|true
+detectorFindUselessControlFlow=FindUselessControlFlow|true
+detectorOverridingEqualsNotSymmetrical=OverridingEqualsNotSymmetrical|true
+detectorIDivResultCastToDouble=IDivResultCastToDouble|true
+detectorReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass=ReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass|true
+detectorFindSelfComparison=FindSelfComparison|true
+detectorFindFloatEquality=FindFloatEquality|true
+detectorFindComparatorProblems=FindComparatorProblems|true
+detectorRepeatedConditionals=RepeatedConditionals|true
+filter_settings_neg=NOISE|
+detectorInefficientMemberAccess=InefficientMemberAccess|false
+detectorFindUncalledPrivateMethods=FindUncalledPrivateMethods|true
+detectorNumberConstructor=NumberConstructor|true
+detectorDontAssertInstanceofInTests=DontAssertInstanceofInTests|true
+detectorFindFinalizeInvocations=FindFinalizeInvocations|true
+detectorFindNullDerefsInvolvingNonShortCircuitEvaluation=FindNullDerefsInvolvingNonShortCircuitEvaluation|true
+detectorDontIgnoreResultOfPutIfAbsent=DontIgnoreResultOfPutIfAbsent|true
+detectorFindUnconditionalWait=FindUnconditionalWait|true
+detectorFindTwoLockWait=FindTwoLockWait|true
+detectorFindSleepWithLockHeld=FindSleepWithLockHeld|true
+detectorFindUnreleasedLock=FindUnreleasedLock|true
+detectorInefficientIndexOf=InefficientIndexOf|false
+detectorDoInsideDoPrivileged=DoInsideDoPrivileged|true
+detectorFindEmptySynchronizedBlock=FindEmptySynchronizedBlock|true
+detectorOverridingMethodsMustInvokeSuperDetector=OverridingMethodsMustInvokeSuperDetector|true
+detectorWaitInLoop=WaitInLoop|true
+detectorIntCast2LongAsInstant=IntCast2LongAsInstant|true
+detectorBadUseOfReturnValue=BadUseOfReturnValue|true
+detectorFindSqlInjection=FindSqlInjection|true
+detectorUnreadFields=UnreadFields|true
+detectorSynchronizingOnContentsOfFieldToProtectField=SynchronizingOnContentsOfFieldToProtectField|true
+detectorFindUselessObjects=FindUselessObjects|true
+detectorBadAppletConstructor=BadAppletConstructor|false
+detectorInheritanceUnsafeGetResource=InheritanceUnsafeGetResource|true
+detectorSerializableIdiom=SerializableIdiom|true
+detectorNaming=Naming|true
+detectorNoteUnconditionalParamDerefs=NoteUnconditionalParamDerefs|true
+detectorFormatStringChecker=FormatStringChecker|true
+detectorSuspiciousThreadInterrupted=SuspiciousThreadInterrupted|true
+detectorEmptyZipFileEntry=EmptyZipFileEntry|false
+detectorFindCircularDependencies=FindCircularDependencies|false
+detectorPreferZeroLengthArrays=PreferZeroLengthArrays|true
+detectorAtomicityProblem=AtomicityProblem|true
+detectorRuntimeExceptionCapture=RuntimeExceptionCapture|true
+detectorInitializationChain=InitializationChain|true
+detectorInitializeNonnullFieldsInConstructor=InitializeNonnullFieldsInConstructor|true
+detectorOptionalReturnNull=OptionalReturnNull|true
+detectorStartInConstructor=StartInConstructor|true
+detectorFindUnsatisfiedObligation=FindUnsatisfiedObligation|true
+detectorRedundantConditions=RedundantConditions|true
+effort=default
+detectorRedundantInterfaces=RedundantInterfaces|true
+detectorDuplicateBranches=DuplicateBranches|true
+detectorCheckTypeQualifiers=CheckTypeQualifiers|true
+detectorComparatorIdiom=ComparatorIdiom|true
+detectorFindBadCast2=FindBadCast2|true
+detectorFindMismatchedWaitOrNotify=FindMismatchedWaitOrNotify|true
+excludefilter0=findBugs/FindBugsExcludeFilter.xml|true
+detectorBadResultSetAccess=BadResultSetAccess|true
+detectorIncompatMask=IncompatMask|true
+detectorCovariantArrayAssignment=CovariantArrayAssignment|false
+detectorDumbMethodInvocations=DumbMethodInvocations|true
+run_at_full_build=false
+detectorStaticCalendarDetector=StaticCalendarDetector|true
+detectorUncallableMethodOfAnonymousClass=UncallableMethodOfAnonymousClass|true
+detectorVarArgsProblems=VarArgsProblems|true
+detectorInefficientInitializationInsideLoop=InefficientInitializationInsideLoop|false
+detectorCloneIdiom=CloneIdiom|true
+detectorFindHEmismatch=FindHEmismatch|true
+detectorAppendingToAnObjectOutputStream=AppendingToAnObjectOutputStream|true
+detectorFindSelfComparison2=FindSelfComparison2|true
+detectorLazyInit=LazyInit|true
+detectorFindUnrelatedTypesInGenericContainer=FindUnrelatedTypesInGenericContainer|true
+detectorDontUseEnum=DontUseEnum|true
+detectorFindPuzzlers=FindPuzzlers|true
+detectorCallToUnsupportedMethod=CallToUnsupportedMethod|false
+detectorSuperfluousInstanceOf=SuperfluousInstanceOf|true
+detectorReadReturnShouldBeChecked=ReadReturnShouldBeChecked|true
+detector_threshold=2
+detectorPublicSemaphores=PublicSemaphores|false
+detectorDumbMethods=DumbMethods|true
diff --git a/org.eclipse.jgit.benchmarks/findBugs/FindBugsExcludeFilter.xml b/org.eclipse.jgit.benchmarks/findBugs/FindBugsExcludeFilter.xml
new file mode 100644
index 0000000..ad63e8f
--- /dev/null
+++ b/org.eclipse.jgit.benchmarks/findBugs/FindBugsExcludeFilter.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<FindBugsFilter>
+     <!-- Silence warnings in classes generated by apt -->
+     <Match>
+       <Package name="org.eclipse.jgit.benchmarks.generated" />
+       <Bug pattern="DLS_DEAD_LOCAL_STORE" />
+     </Match>
+</FindBugsFilter>
diff --git a/org.eclipse.jgit.benchmarks/pom.xml b/org.eclipse.jgit.benchmarks/pom.xml
index 4033fdd..e26ac30 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>5.10.1-SNAPSHOT</version>
+  <version>5.11.2-SNAPSHOT</version>
   <artifactId>org.eclipse.jgit.benchmarks</artifactId>
   <packaging>jar</packaging>
 
@@ -51,51 +51,37 @@
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-compiler-plugin</artifactId>
-        <version>3.8.1</version>
-        <configuration>
-          <compilerVersion>${javac.target}</compilerVersion>
-          <source>${javac.target}</source>
-          <target>${javac.target}</target>
-          <generatedSourcesDirectory>.apt_generated</generatedSourcesDirectory>
-        </configuration>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <version>3.0.0-M3</version>
         <executions>
           <execution>
-            <id>compile-with-errorprone</id>
-            <phase>compile</phase>
+            <id>enforce-maven</id>
             <goals>
-              <goal>compile</goal>
+              <goal>enforce</goal>
             </goals>
             <configuration>
-              <compilerId>javac-with-errorprone</compilerId>
-              <forceJavacCompilerUse>true</forceJavacCompilerUse>
+              <rules>
+                <requireMavenVersion>
+                  <version>3.6.3</version>
+                </requireMavenVersion>
+              </rules>
             </configuration>
           </execution>
         </executions>
-        <dependencies>
-          <dependency>
-            <groupId>org.codehaus.plexus</groupId>
-            <artifactId>plexus-compiler-javac</artifactId>
-            <version>2.8.5</version>
-          </dependency>
-          <dependency>
-            <groupId>org.codehaus.plexus</groupId>
-            <artifactId>plexus-compiler-javac-errorprone</artifactId>
-            <version>2.8.5</version>
-          </dependency>
-          <!-- override plexus-compiler-javac-errorprone's dependency on
-               Error Prone with the latest version -->
-          <dependency>
-            <groupId>com.google.errorprone</groupId>
-            <artifactId>error_prone_core</artifactId>
-            <version>2.3.4</version>
-          </dependency>
-        </dependencies>
+      </plugin>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <encoding>UTF-8</encoding>
+          <source>1.8</source>
+          <target>1.8</target>
+          <generatedSourcesDirectory>.apt_generated</generatedSourcesDirectory>
+        </configuration>
       </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-shade-plugin</artifactId>
-        <version>3.2.1</version>
+        <version>3.2.4</version>
         <executions>
           <execution>
             <phase>package</phase>
@@ -157,19 +143,19 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-site-plugin</artifactId>
-          <version>3.8.2</version>
+          <version>3.9.1</version>
           <dependencies>
             <dependency><!-- add support for ssh/scp -->
               <groupId>org.apache.maven.wagon</groupId>
               <artifactId>wagon-ssh</artifactId>
-              <version>3.3.4</version>
+              <version>3.4.2</version>
             </dependency>
           </dependencies>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-surefire-report-plugin</artifactId>
-          <version>3.0.0-M3</version>
+          <version>3.0.0-M5</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
diff --git a/org.eclipse.jgit.coverage/pom.xml b/org.eclipse.jgit.coverage/pom.xml
index 94cce0a..6b02158 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
@@ -27,88 +27,88 @@
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ant</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.archive</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.apache</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.server</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.server</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.pgm</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ui</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
 
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.test</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ant.test</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.test</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.pgm.test</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.test</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.server.test</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ssh.apache.test</artifactId>
-      <version>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-SNAPSHOT</version>
     </dependency>
   </dependencies>
 
diff --git a/org.eclipse.jgit.gpg.bc.test/.classpath b/org.eclipse.jgit.gpg.bc.test/.classpath
index f08af0a..0acccba 100644
--- a/org.eclipse.jgit.gpg.bc.test/.classpath
+++ b/org.eclipse.jgit.gpg.bc.test/.classpath
@@ -2,10 +2,15 @@
 <classpath>
 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
 	<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
-	<classpathentry kind="src" path="tst">
+	<classpathentry kind="src" output="bin-tst" path="tst">
 		<attributes>
 			<attribute name="test" value="true"/>
 		</attributes>
 	</classpathentry>
-	<classpathentry kind="output" path="bin"/>
+	<classpathentry kind="src" output="bin-tst" path="tst-rsrc">
+		<attributes>
+			<attribute name="test" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="output" path="bin-tst"/>
 </classpath>
diff --git a/org.eclipse.jgit.gpg.bc.test/.gitignore b/org.eclipse.jgit.gpg.bc.test/.gitignore
index 934e0e0..8b6760c 100644
--- a/org.eclipse.jgit.gpg.bc.test/.gitignore
+++ b/org.eclipse.jgit.gpg.bc.test/.gitignore
@@ -1,2 +1,3 @@
 /bin
+/bin-tst
 /target
diff --git a/org.eclipse.jgit.gpg.bc.test/BUILD b/org.eclipse.jgit.gpg.bc.test/BUILD
index 1e3677d..925536e 100644
--- a/org.eclipse.jgit.gpg.bc.test/BUILD
+++ b/org.eclipse.jgit.gpg.bc.test/BUILD
@@ -1,4 +1,9 @@
 load(
+    "@com_googlesource_gerrit_bazlets//tools:genrule2.bzl",
+    "genrule2",
+)
+load("@rules_java//java:defs.bzl", "java_import")
+load(
     "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
     "junit_tests",
 )
@@ -8,7 +13,23 @@
     srcs = glob(["tst/**/*.java"]),
     tags = ["bc"],
     deps = [
+        "//lib:bcpg",
+        "//lib:bcprov",
         "//lib:junit",
+        "//org.eclipse.jgit:jgit",
         "//org.eclipse.jgit.gpg.bc:gpg-bc",
+        "//org.eclipse.jgit.gpg.bc.test:tst_rsrc",
     ],
 )
+
+java_import(
+    name = "tst_rsrc",
+    jars = [":tst_rsrc_jar"],
+)
+
+genrule2(
+    name = "tst_rsrc_jar",
+    srcs = glob(["tst-rsrc/**"]),
+    outs = ["tst_rsrc.jar"],
+    cmd = "o=$$PWD/$@ && tar cf - $(SRCS) | tar -C $$TMP --strip-components=2 -xf - && cd  $$TMP && zip -qr $$o .",
+)
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 2658c4c..0a121a7 100644
--- a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
@@ -3,12 +3,22 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.gpg.bc.test
 Bundle-SymbolicName: org.eclipse.jgit.gpg.bc.test
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.gpg.bc.internal;version="[5.10.1,5.11.0)",
- org.junit;version="[4.13,5.0.0)"
-Export-Package: org.eclipse.jgit.gpg.bc.internal;x-internal:=true
+Import-Package: org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
+ org.bouncycastle.openpgp;version="[1.65.0,2.0.0)",
+ 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="[5.11.2,5.12.0)",
+ org.eclipse.jgit.gpg.bc.internal.keys;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util.sha1;version="[5.11.2,5.12.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.gpg.bc.internal;x-internal:=true,
+ org.eclipse.jgit.gpg.bc.internal.keys;x-internal:=true
 Require-Bundle: org.hamcrest.core;bundle-version="[1.1.0,2.0.0)",
  org.hamcrest.library;bundle-version="[1.1.0,2.0.0)"
diff --git a/org.eclipse.jgit.gpg.bc.test/build.properties b/org.eclipse.jgit.gpg.bc.test/build.properties
index 9ffa0ca..e36d666 100644
--- a/org.eclipse.jgit.gpg.bc.test/build.properties
+++ b/org.eclipse.jgit.gpg.bc.test/build.properties
@@ -1,5 +1,5 @@
 source.. = tst/
-output.. = bin/
+output.. = bin-tst/
 bin.includes = META-INF/,\
                .,\
                plugin.properties
diff --git a/org.eclipse.jgit.gpg.bc.test/pom.xml b/org.eclipse.jgit.gpg.bc.test/pom.xml
index 6a119a8..08966f8 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.gpg.bc.test</artifactId>
@@ -85,6 +85,12 @@
     <sourceDirectory>src/</sourceDirectory>
     <testSourceDirectory>tst/</testSourceDirectory>
 
+    <testResources>
+      <testResource>
+        <directory>tst-rsrc/</directory>
+      </testResource>
+    </testResources>
+
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.asc
new file mode 100644
index 0000000..355462c
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.asc
@@ -0,0 +1,41 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGNBGAHBLQBDACsS1vFqE3qgKD2R5X9n90Gz8bucwwvJWIqaHDsVoAtF6IcKIDo
+1hQC9YksTQYl/L7BsMDdmjyEbWRfzW4ory5596d342Hl6g7ZB5jJR5kJJdhy2MCJ
+BUiMy/724Fr/Dz8PNPcEoULz9ZH7HEaPRKqWWEQDUCq5ak0MfLKXtWVUBgsY5Mry
+29d/GLJvnxZ5v16PK+P4oqZ7vh7FWJPlqPK2TCZ6s1rYfWlu9XbHOzwXwozVg7IX
+tfFq4Rij4c0sg0S0GY8hGAlnOpRc/6J2S41Y8p3WqND6r1LPDQUFnNCKXVoHUGeK
+X9U5iAP7pxZSuonsFCqr3CDGxr+kKUpbfZeLrqTA4lBUK7T6w6Wq0qHosCYUU7YC
+GZjlEeCZBRWNfeq45LKlhdNUxHWWgaBsgWaaDmpFWaivblmQGOvmSv1nJMNmedRs
+DSF51nsJnkQceprsvThSa6qJwEYi7pj6L9HO2UGgJLCb3dL5VTQih2gdhghckUSB
+okUkvqBvvdiP2nEAEQEAAbQdVGVzdGVyMiA8dGVzdGVyMkBleGFtcGxlLm9yZz6J
+Ac4EEwEKADgWIQRPCAvglun3I2Bs1iITM2XBzCpwbgUCYAcEtAIbAwULCQgHAwUV
+CgkICwUWAgMBAAIeAQIXgAAKCRATM2XBzCpwboiBC/493+ruANV2eiro8MY8wZ3Y
+gdjp3pHBSg9RK74SIh95J+MW5qzPwkU+vHd8l0+aj9e1sDQb5BFcFk/Z1ioI3TDW
+B4vYWoMkdN932fJ/LcIlhOGjWwSNFZphbYmJzrAwUTA499yx3jt9Dg+vSU88S+8S
+FzYe6CBNt+PqDCbk6Gm+ZcVpR+elq/QJeyhdDzCCrrfNXwPwsVGAM61Z8SvdvNKE
+DA5gHXRsOKf8fu8lqW2Ay0MCvgsZLMIGOMDPCyBUd1bhlU0p18V6D6wdatfzu9gR
+X/k36HJyqB2cHh89/F2KdBSonRVRJOvHc/88zEeRFkiV5pUyrXv40l099+5dvA+2
+h4ODftY7ZbR22k4iX5rqj2BRow3H+N5lTIWgiADPUl+H8z4ZY5G+LWk9Xms3o1G9
+DmEepM3ma1pg4sZbxf0iStikch7aPvL/HgGRPJnDxA/W4KJxqmSw9TTMH/6XHq3D
+ah5Z1lbcylChgrFLFVJi+shnLTZSYttTeKOIqTPi0765AY0EYAcEtAEMANS23tqF
+Dr69wz0AaT7tjoccT/WlSO/gxd80ShMr4vbr21PZp8qGklFmlcrSrMDRwfXY04x2
+qxHR/Kf+hCD5gNvg8kh/yH2lQRcvekzQ4/rLmSXBfGOFg+LioQQ3CZJ1MZyIHzu5
+YVZ2pqALfJwJSw9P5Z340y8sq8AOPaJ+cpIC0rYBp9BUAmz9IeLVT7fUc6CjaWBo
+++E8H+9FyZC71RIPNcCvY+24Qky8ms7nw4hA47Dlht1pqL8dzOggCnohuSYMCXs2
+YPLvDGdZMg7GgQ3AyZawDmjTxFWt51VU5hunGfGiC5Aock8rVHSYsQzUFjVBSR+Y
+Zy+c4noxZD1eRfb8KdFnrewyVqGKFtc/JwA61qhhyYFe5AWMAFtudjGYG0WiTP82
+CmFFc1Qsvyls9G2yMkLuay5wsdIJMnRW9XwBzwxm0mdZI6D3nSbWjPUUfRcGBY8C
+Hqpc736G+UzMevZtorwy/5Q6D8v+Obrk02DIDKa6CJ7g7dTwK0I/fleJlwARAQAB
+iQG2BBgBCgAgFiEETwgL4Jbp9yNgbNYiEzNlwcwqcG4FAmAHBLQCGwwACgkQEzNl
+wcwqcG6mYQv8CFIVGj7/Qnr+wmviMzm8+B4WwQIUHGryqv9hnfp9hLOXMFmNuEDl
+QYkHVChWO7ehrR3fpvpebhcieV19skf/WO8xm0pGSXyjV2/0/bVhXq01xesXHH9r
+4aFxsCu0E8M9fZVAHP7NBr4A67knQ4EHRF6Rwml2ba6Zt2oP15IHvsAq/2B3f8ar
+5sUau4zM1cItG3tg49rbYr6V71HdgkWA22+EkbXL/Qq3haY/er2dIGc73lu8t7oQ
+msGK4LSAGc2661wMvJ6w6feCagkXAtrqyxodhSLoWgF3i0QVQnMbgmYWKEK2B6YA
+g669CZCCXJF+9Ebq+PP/d3Cy/k9iUmWDh72C7iL136kYZt+71b+yOmlDRT9l6DvU
+FP3bhRZWomOt3F3aP5mAdbwrP1NbvlxTYUAf++nUPdpr0Jrvgi67/VHVjaUtVh/K
+gVQ2C+4Cp/fllxXXKQMPcC8dD1x/AL6ytDzPu099ETMULntgbt7A5Lsd/fFScnF3
+ZNx6wjRReIvT
+=8E/K
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.key b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.key
new file mode 100644
index 0000000..afa459c
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A.key
@@ -0,0 +1,42 @@
+Key: (private-key (rsa (n #00AC4B5BC5A84DEA80A0F64795FD9FDD06CFC6EE730C
+ 2F25622A6870EC56802D17A21C2880E8D61402F5892C4D0625FCBEC1B0C0DD9A3C846D
+ 645FCD6E28AF2E79F7A777E361E5EA0ED90798C947990925D872D8C08905488CCBFEF6
+ E05AFF0F3F0F34F704A142F3F591FB1C468F44AA96584403502AB96A4D0C7CB297B565
+ 54060B18E4CAF2DBD77F18B26F9F1679BF5E8F2BE3F8A2A67BBE1EC55893E5A8F2B64C
+ 267AB35AD87D696EF576C73B3C17C28CD583B217B5F16AE118A3E1CD2C8344B4198F21
+ 1809673A945CFFA2764B8D58F29DD6A8D0FAAF52CF0D05059CD08A5D5A0750678A5FD5
+ 398803FBA71652BA89EC142AABDC20C6C6BFA4294A5B7D978BAEA4C0E250542BB4FAC3
+ A5AAD2A1E8B0261453B6021998E511E09905158D7DEAB8E4B2A585D354C4759681A06C
+ 81669A0E6A4559A8AF6E599018EBE64AFD6724C36679D46C0D2179D67B099E441C7A9A
+ ECBD38526BAA89C04622EE98FA2FD1CED941A024B09BDDD2F955342287681D86085C91
+ 4481A24524BEA06FBDD88FDA71#)(e #010001#)(d
+  #208024A31FF0F6B3E5E91F2ED58572F1A67F1D9AD9290991BF732D1DFFE134E058E5
+ 9BE459478CC5D42058997CF7EC79E55A9CBF10A9AAC761E04A85A5AA0A07DAE61DD0E8
+ 36311534EE606D5392B42D8DEB7824B594280FDB2950D39886B58F0D24CE15F2FF88BA
+ 819B8F4566202B57A9F5C6743862FA80E7429C83CEA57B189ABE4AE657B28DAF7D6EA7
+ 6CA89635B9B6232EE14779452D636B919E707B92B13DA3229133A953DAF021E0928B83
+ 75EDEE98163C2189E22CE9A236C3D0EABD2608DAEF09211B2C77FFE9158A95F8EF2750
+ 221C5ADEDAED0446DC0E4CD8D38AD897D44FA3915934B6CF03F489DFAA6D939AB9F8EF
+ 1C2A0CDCFC3F2207D63A9EB55B09A0A45323D5F59AE4A9D48E819E98E53D04F022905A
+ 9C4D137F32CB33A974F202B0D3AD4AC64CFBA2A4C18650B671AB753D1D3BD7C4FCC8D2
+ 0F85D1876D89A3D94C77423778C08BDF8FBA23A8501D766FC1B4D51F2D4BB4C27B8491
+ CC2595FF54034F4F192D668C1934D292752A4E44C95135D29449B75928BAF1A2389ED9
+ #)(p #00CCD74AC0DC1CC46052F784DB19545A09FF904846247BAD1AFA5E00CE84A4DA
+ BFCD3BCA175508C068553226DBA49EDAFBCC33CF2A914F9006326FCB62C0473B1E93F6
+ DCF89A24006B090834E5686464A8C216B70AD797732E671ED78CD3E922161069E46BA7
+ 489F2A79CE46BDC4E6F5FCE97C3C9DC59399212235C53246609F8B7FDBF2AD191B3FB4
+ 4CC59760BA6D2029541811D1E9B72DC2ADC98513589A9715C82EE88ADF9000A41512C9
+ 6D491D2A30269FBFCD9CF5D2F275A1DBFFEEB72BE5#)(q
+  #00D7532ABA5F442A994ED8290AA71EAAB6F8137FE3F01488298637084157972D31EA
+ E9F495B4360A6E92ABA7C2418A5960DF29B8C8146CC7D1DF1201C17509D7315B6ECF86
+ F0A73C9F5B48D96F2108DD323FAE6BF897A8CB773EDCF5C82E203813945405F414E3F2
+ 99EEDE43EE6259FDED1C01B47C20F67AC488209585FE6FB7D958AF5EF567737E1ACCB4
+ E293724BE8AB6159CD5A6603FFEFC2DBC30FB6EAF647DBE7D9664ED0BBA1C4A2268AE3
+ DE0850572E145BA811EB2087E1E26490F9439D#)(u
+  #00A8026DB6685170EC05DA3574451678718501489843227BCEB772FDB86B20AB2F2A
+ 00B790A2373FD5DF7AD2CAA2E3896C4C9CBA08581F3644DF67743FA4BE1153421F8FB2
+ 47F0EFB64C566CB7E361BAB21CCAF029B43B7172232D11D14912BC035E17FB8BC663CA
+ 6766E6D2531F87AF378896A2AC7D98824AA8F51C5D6C11B7DC266209BCD3B23597F02B
+ A317CCAACC979402F84E47F3D143187F9739FE9A6C71829CC94E4003D70CFFA33AC0FA
+ A12776EFAFFB8261ABE664C6A6FAE623D3678F#)))
+Created: 20210119T161132
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.asc
new file mode 100644
index 0000000..362e210
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.asc
@@ -0,0 +1,42 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGNBGAHBAUBDADAzIW1FhCcQmP/NDhzXoeRQ+DNACqTed7eEhqm3rowkW4wKi56
+v1UxFR0ZoA3LoT1oQQjiL3IS2l4/qpBR3JQhMFH3pl7yBsCIrN7JvZfAvxq2Ud4e
+YbdonY8mv/yCLq+nkTWlHkWGCppKMm6DupEUw5CFUCiptPXIxikU0uQYB7VRtXhh
+Q1RGdsv6mcOwMIh7hj9flTrX025x0vRypjqgDR05RuM3hMMpJDGMAuf51w/lbRl9
+dAsJzFzg2cf+8qv92Gx3RuP3a3yl6pEuKnkpddC47lj2pvuhWZBf2sXZMyPvMIvA
+Dve4GIVj6k+wXE3DMp0xMy0Fvaxw5OORPxUAKNBR/BgjoRbkbjm+rJZzviu/XVPR
+42+78isvsa+lnEAmTeoTqiz9nlTTPN+6JjwJXYn2LuiFM8XzNPJwNmd/86lW1qbP
+DxZHhD9jjeXZNHUBUCKTIj2Rs2uFa0xrALdhhhGEao9JlVcUqz/Tw85qC+DlzSa3
+3re5C6wGe2pnW3sAEQEAAbRGVGVzdGVyICjDhG5kZXJ1bmdlbiBpbiBHUEcga8O2
+bm5lbiBGb2xnZW4gaGFiZW4hKSA8dGVzdGVyQGV4YW1wbGUub3JnPokBzgQTAQoA
+OBYhBPidC43dWzUvOqsRbzx6+72u3pDoBQJgBwQFAhsDBQsJCAcDBRUKCQgLBRYC
+AwEAAh4BAheAAAoJEDx6+72u3pDogeAL/iNn1aKxA7pKmucyuzcbzvUjtcbbqFL8
+lOLdRxkrQNCDMb+wHgGY1UqJ6wsDruhV+TdPLzUXHpCl731MeLxZyIENr4wnjTBf
+Cr4SU8eFUkusVf3aWK3rlk54W50EkfBjMvDVavRKNkVbCWAAwXZ7mTRf3UlWxg+F
+9Sq4j2P/hEZIznKV3y7zXLDYg0OpMZLgbo3si0CD19/1T/8Z9C680qSwyAiPtjRo
+vfJYqZFQc+ZH7j1Hmvg68d2Qwrkg/WMfOGoTLZq/6PQcM5leQBAodcS1t7C8o1JQ
+6D+f2gLHpMfFdUKh9TkmvnKYI20TWUVm9XQLqyAHsOn2vRMUhydcZ8OP103TKmP4
+mbpgiyp4i4S/7XofHSeFBrbdqt73ebubESuZVXNjTuuSUjH8Jq5nHq22ZrmotSd0
+FNwc9qQmwPG/gmOGq3rUdT/KzUVntc66QN/+hMhFDYMRJsjJhhyszvGuuBp6vBzI
+lMZqx5jqOaI9ON0m0o8CKC50sKdJ25G3wLkBjQRgBwQFAQwAqMXwgfSaIM+eSQWs
+xb4Sf2aCr/RZi5wzZz89lSomMcblqtCpuHv9/C1PSd+N/D1yJKzPChbDjHh6B6gc
+4OUvuDKHmxK+oAiURpvR+yJEdbSEYwiBhfAUD6u2q3IfY5PpyyZT3NjZ9EY8FpOX
+wpgdSOdSiZVZfZt0xUPsGbW/xP1yVR1NHYLuZX5P5oTCvyNJyP8zQQmToamJsvzR
+v9r+2sa5di9roe53kZwq24VjIvTDOOE4xoYEXk5UD83u1LA++9Nfdisxxts1bxgj
+w1ThO/IRTyY5y5bKSQPskYFD3eVz+azjVurqhbj6ep69mR2X2gjLCetz6G1l4PU2
+R6wcqVG6gR6XpGFPsN+M2JRtbgKrtMElA8egJIMMpH4hdXYqIqmpe/31zXhClHkO
+99EbBpk6OawZC+MnreQFN4NIK5uO2aLi+3KL1FFnNlFCXkh/8afbt4+6rHcWC6En
+q5W0ZkLZnpjdFOF5NPTinAdei+14gnf2QhIlFLeMHvqiVEXvABEBAAGJAbYEGAEK
+ACAWIQT4nQuN3Vs1LzqrEW88evu9rt6Q6AUCYAcEBQIbDAAKCRA8evu9rt6Q6DcX
+C/4orMX1YBZNJK5hLLdjfk45EsQDfCnhf8H991xd0Rq4VPJP6bvzikSdOn9bTUEz
+AAhA4JnFu9AMTh8ioOA7ZtViIccplFBivsxi3rAVrQvmCfoP2AdHfG/jB6D9uWGs
+MV5/o1p93Hr0ReO73HK6G4Q3FbJOG3fg6wTcMYyyEQrD5g4IQhKiIhudUlSkKUkA
+9hWKlXSLw3Yx/S2Nq5Ye+Pqr4CU7UFOTCsBIH4Ky+6gLTmP6esPx0k8vXLcOjaCk
+ENcLi0OaL/AgfATH/InN3wzrx2AFfU6eQdEG7HS+402eHl0fmWwMGV+SCsLl+2hx
+AguLFwjetuVrroc/d+XeZdTcpr/2vojsr4UgbkH8Pa2KrGIpK7V85nSOeVbpDUru
+tuimIRSxIQ6GDF2c7Ih5yBy+JPV47gppSV/GgHPgrOlebeqy4sytshRiEYw/nJzZ
+LKBaG6gykN+6MeV6+A7c1BlCYpyi2vcyvouU+l3/Z9gR7vY+Oj1eAaxrqeTFf++3
+qnA=
+=03Wd
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.key b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.key
new file mode 100644
index 0000000..cef72f6
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9.key
@@ -0,0 +1,45 @@
+Key: (protected-private-key (rsa (n #00C0CC85B516109C4263FF3438735E8791
+ 43E0CD002A9379DEDE121AA6DEBA30916E302A2E7ABF5531151D19A00DCBA13D684108
+ E22F7212DA5E3FAA9051DC94213051F7A65EF206C088ACDEC9BD97C0BF1AB651DE1E61
+ B7689D8F26BFFC822EAFA79135A51E45860A9A4A326E83BA9114C390855028A9B4F5C8
+ C62914D2E41807B551B5786143544676CBFA99C3B030887B863F5F953AD7D36E71D2F4
+ 72A63AA00D1D3946E33784C32924318C02E7F9D70FE56D197D740B09CC5CE0D9C7FEF2
+ ABFDD86C7746E3F76B7CA5EA912E2A792975D0B8EE58F6A6FBA159905FDAC5D93323EF
+ 308BC00EF7B8188563EA4FB05C4DC3329D31332D05BDAC70E4E3913F150028D051FC18
+ 23A116E46E39BEAC9673BE2BBF5D53D1E36FBBF22B2FB1AFA59C40264DEA13AA2CFD9E
+ 54D33CDFBA263C095D89F62EE88533C5F334F27036677FF3A956D6A6CF0F1647843F63
+ 8DE5D9347501502293223D91B36B856B4C6B00B7618611846A8F49955714AB3FD3C3CE
+ 6A0BE0E5CD26B7DEB7B90BAC067B6A675B7B#)(e #010001#)(protected
+  openpgp-s2k3-ocb-aes ((sha1 #84A4A8051974D94E#
+  "26420224")#914E6847983627126D1CDF93#)#DEB66FA3201F91591F688F5D2B1B79
+ 39FD75F58A962C227BC739C6F2F814ADE5115BD85B2E55427153CEC82612F0C5BBE8B9
+ 71A0E5A6B796111B6B1A03C4C926825F03B871CBFE0F64BD0F0CC65EA34E718BA823BD
+ 136D78C9E88CA1733DFC8D6A38830274322A589BC522A2A824FCEAF453523CDD9BC391
+ EEF1355470C110E9A92681DA0C61563465D5238CECCA2D6CFA78FFDFBDDA17A308D6E1
+ 3B1858890EF25A7655F22FE6305AA0129DE5A353B657065E608A616A23C6AF561C4472
+ 5AA705E55343E9C728641BB63C64F804F76A4C5008CE5FFBC09F03B632B42180425D28
+ 9DC1201D91B1989627EE5930E6EF2F6606108B2F048934A9D79DB4834DD950C4A2013C
+ A40B50EE54FA9E3CCB20C210244BFACA795494A1FCFF35856ACF63214A0498ED894BAB
+ FE80CC24D8A478AD08D0BE8CDC8F357FB7F28A30B87540B9B4970D6EA0AEEF46A2549F
+ BA43A98FAD75B4228108DB50D1C3654422E24B4C7754673A66281BB283CD6A1EA8E64B
+ 97DC9083C62034BF7FBCD193830F8FEC3589673B864E50EF7AF4DEE046BD26041E2925
+ 170EB7B6DC6060E78309CC8A136AE9CE44D3B4EBDEE4479482464D0D23C13529184021
+ 795557323D353A70CC710EC2A79C66E860095C082E40724867A9ABBFD3407B2F92CB2A
+ D0D95CC8DAC2FB2C0187B3BB09AEDF869AF1969BA641027D4D5DAA31B1DA5822D40A5A
+ 7FAD1C054C02CF8F8F692B1C45C879299C0E9D5E5A165F6C22DEEBB8C16FFB91F381C4
+ 8FCB209657A7BF9268BD34808D0A9D3D6F50F7026BC297FD3A08790B8EB5CC0291246B
+ 16E4B50A7E9630B33F59B5EA24EEA396F07AEFD0C262BE9915CB32D5F03673CBD3D20A
+ 831FDF55B5BA3D03440A8E1A331147A8AE0760EC593EDC881F5F0A04F4FCBC80C1531A
+ 4DE71D014E3612C2C679BEF3AED59F358ABA5731FF80FA15EAD2CB95AC548F6AB0FD7B
+ BBBB2CB63DFFE9E672605B7F54EEA4B4C046C4CC8036F2F76260CF068D232A40F492FF
+ 9648CC7459F0F46FEABA3D62B9F421B0F8A1BF914E41702540213848345498AA13F989
+ 49EFC2888D3720DC34D20634472FC3A194F1403C1609C38A020F7E47F3205CA5C0CB50
+ 26270083ED153BF97375407514BA15D92808A8C10F8160880F6981BE53294292E4EEE8
+ D215E7854FC79016B64984BBBAD2E99EED8D66B25575183C279E4DAFCB63F1067FA2B0
+ 0888A9C226D4846376520720FC1E947A93A1D32444F78B2F4EB836A6F8C685C1D82958
+ E31560C3FC861D2B68B889E1B5EE0476B914DFE316411F750D252F24076E53557AD5F0
+ 4050E5E839B33E5B8AF16FDD9FE033B39796A52DE8AF65375966D4DB137C85C800B5B9
+ 0E29434E4B215DE35E60C85391DFDFA572C6F9747A0EA0964236FBB3B04394D9DF0694
+ 6E4CC9CBCE352382908148D265293C6EADE7C2AED6F5AE#)(protected-at
+  "20210119T160858")))
+Created: 20210119T160837
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11.asc
new file mode 100644
index 0000000..f412019
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11.asc
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBF7EL8wBCADO46xh7nXn7vZ5ow2Zdrp7WTh9BlT2wtaHNKpnKvSoYHjJbbGz
+yF8Jf/qVPuXNbjx2df1lT7zT7x3evcjQoNy80deftCw8ApZB9RMOo3uUIqS2VpO+
+cS9rjTgBRFL6xDv3g4++CE9s+5dKE9gKkwleZ5/tVqUIoHPAIUEjpcPHngi5m2bi
+tSmQUYWLGcliR1E79sJMSzPt1neksqHFMJ1KTEJLAABZ0t3PiBzmycIQWThX3uU/
+lcgnZmmhWCJIqV0yRZqxl61ejUfq+zK0T7MzhAAugqe7D6BM1FRwZRNCHwDQXIvt
+/t3fczTe+x9oTy4qX4MfaP8lHM0223MwGR13ABEBAAG0H0EgVSBUaG9yIDxhLnUu
+dGhvckBleGFtcGxlLm9yZz6JAU4EEwEKADgWIQQILQAv4wNQfEJ6I/NEWemKCmiQ
++wUCXsQvzAIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRBEWemKCmiQ+xev
+CACZSWh4xjTgafwGMP9RnReOhubVmfHS+XGlidDQzJDtshDQddPZ3oQwyTe3OgkW
+ZgOrzjrHGsZp3WZmGUZejrKt2Brqp+h+VRujFVcKk4N9A52BkM6OeT9lzBabOpuA
+UaDsNMSFsMcGTTYpB16+sDcyui8LW1jGi1y+8aQa+u1lIk/vVycq8o4htn2Af8xZ
+rAT8peapsjoNjETEs8OQ0al3Q0UX9amW6Rq1zZZ0XtoXDCPTI01EfczDMN+AZoFk
+UYHwSREDFLSh+c+q1HhYp4TqP+2a5Rayna//n7zci1PmSX7zD3iWzV1jEQ3Jm8U3
+DY+P/WLezQdSJIBVCFpCualquQENBF7EL8wBCAC+ef+vNvfu1jl9BXpu6K9PG0I5
+DQfrNtcdPq90O32ipvsYvqGOJX9MHoTyxBPLew+e5UsYb3ex62JyJqdAaqSwYXEN
+MBESZx7yBqBMUvildfh8dowbJeblxCf5KsE4C9uNfg4ApWGD7PjVsUCh47V8VcfG
+ymCxxq80r+4GfFtt/HC+l9fPUnDLuXpAWEM2GPUzcauUoEXxZK6nhstYCRlKlQcK
+Tn+LtCC7SGpYlqvwWBzAnOYP9+eZfSJ897g0AiTEhK0JsBlDAb3UAWHYHkAkVa1+
+oU/UedhPC4j2Q7RzPQFMun6aGkaDrntCxvT7IFiMplPG7iy0JDd6ubrWSzivABEB
+AAGJATYEGAEKACAWIQQILQAv4wNQfEJ6I/NEWemKCmiQ+wUCXsQvzAIbDAAKCRBE
+WemKCmiQ+xoBB/9BAmlHQUmVl/bkwszAcyXkR5HsyA4htMJt+6GKlqftuhLP0SGK
+Il+7GeK6NqNdQXxXG5Wj6dn7ZqWalQRA0evEa6VLH+74zrn0llWfzTPIcP1bHW7l
+uYaOzZ1z/q4FoEGNJxp/jdToZ4970OXLzqY/G/QlMJIlXWCC0EXNYbKCEpOE9uvW
+h4kWe5xeGOmhZylYbzurTDzqEtKy+LZ9f2xNYn6ElcWtwxsxwSY7L9B3eNcCYE46
+Np6uqzPffB9s7PHW46yEL1lQs6ME+9hBGyjeVop+Wg9qkh3YCrp+KY5Vkmdndwkn
+Th4FnTpcCiS06fCVHHC5kelh+H6TgRA+XQ/V
+=WGUq
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11.key b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11.key
new file mode 100644
index 0000000..b8765aa
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11.key
Binary files differ
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.asc
new file mode 100644
index 0000000..c6e0408
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.asc
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBGAHViwBCADaE67a5U8oqNjqg//SmlNLd+MrFIccHsg+pkSDhF6ipjZEXdFu
+oRQ116tH/qY1SzsHw/TMLujYeTBW0KwQ5E2+TWagckL8pJpDt7ZC08CTK6u0Xvjy
+BSqy/t29NPTuQxxxqRx5gq91mtuo8v00fQmqkbFUgkVfEOKOv4qe2tlaR5pTvpmV
+VboXOls87RPgP/X665kamHjsywrsDpZ5FbvPS8E2kKdYXqeHaiEU4i0Bizjx3diK
+ilPEIxxl8zgDsROXyKagCy0KOOajBqhFhStQH1soIzvk8aG/9eItKmTa2v1BD2mV
+UlZNQ9ZfsnXx3QIBLmA3jugH69rkcekHRCWPABEBAAG0HVRlc3RlcjQgPHRlc3Rl
+cjRAZXhhbXBsZS5vcmc+iQFOBBMBCgA4FiEEJJx/+EvV3v35cJ+Jx5r/T8UxegwF
+AmAHViwCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQx5r/T8Uxegx+hQgA
+pIy+E58aWh9FIHW/POqLB/y3v/GbYdIbzk9ro0FAXQ2tQsoGQbsuLckJVIlyja7n
+oQX23OmWTOc2tj/Kpy9lZ2ButW43FaMSiLh1G/VtM5pqJ9yHFdb1z7Q+LHMhhB7w
+RKOoNSEgkwtxv4LAkKz5t/BrDU0hvDPxWPqCSRvSAEE6qIY0fa3mLf9ijy3gLlCd
+hBayUC53W0tL7gLHgprTM/fX7b1pDab8beorroPHs5XzcYUBleaCGmEYbtV2eXPQ
+5irOXOC/D/E8vOfOZwhOhFOZk9b4UnhFK4OCfpKIzIooc6tlboVPhdx7jb5DkXQo
+rozfavEvAPx8INi9KNmdBrkBDQRgB1YsAQgA6q/O6mAPB0kj3pnpuIdJC8EumgKm
+7W8rv+ZfRGePg+IEEm5DeFKtfWl70YH33nGmwnWB95wO5412JCNC174z5LKDBbz5
+PWT/yTNnmjooxj6G4p/YVwXYJkvfaDP+kQnYgJAybpeqTa30tES0eqvI0J9aQo1h
+GSMRCkE6QMV45IMj6gH9rptQv9e8U6gbnwBPxWPG2FH5rsGIGQGzIEmGxmKRyxXm
+YDU4f7oWPHSg1ikQqCzAxxCBxeCOaid3acLK8//TOwF/Do8GPJbcupEDqsgbFNGM
+BDWtmkmxjrLntlU+dvIPcsBxdBUrrADiJ/k7EfFv3kHfLfdAonSdKZL85wARAQAB
+iQE2BBgBCgAgFiEEJJx/+EvV3v35cJ+Jx5r/T8UxegwFAmAHViwCGwwACgkQx5r/
+T8Uxegy4+AgA1bzFKpsqkwrjZKDCCT759xeuUbxnYE9kBJgFSVuhn7fUbB4MoHx4
+shBptx7iBOdxxT7yC0oaDPhbiIkttb/c5W0f6JuLr08JpjkFfkrWF+dMcVrtXwPx
+i/30ccV98qWJDCBunyeCwBNie1Ck+qXMxm3FYy4qIbftMQ7mG6KKN6eFlbxu8B6M
+p93DFUvycGH9CWz0yJcho7KT0NSSyoLZhJz2uxRe1BwGMV20O9AG9yicsU0/uJxY
+a2Hble8NkH54XDuZkrsBaAb/o8UsWP7AJdYYsb904UZDIZNRfjWapOmODnlnK8Ta
+Q8pyYRGS5of1SapatMfpQZF6hdsamnTH6A==
+=guSE
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.key b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.key
new file mode 100644
index 0000000..63617c0
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/F727FAB884DA3BD402B6E0F5472E108D21033124.key
@@ -0,0 +1,32 @@
+Key: (protected-private-key (rsa (n #00DA13AEDAE54F28A8D8EA83FFD29A534B
+ 77E32B14871C1EC83EA64483845EA2A636445DD16EA11435D7AB47FEA6354B3B07C3F4
+ CC2EE8D8793056D0AC10E44DBE4D66A07242FCA49A43B7B642D3C0932BABB45EF8F205
+ 2AB2FEDDBD34F4EE431C71A91C7982AF759ADBA8F2FD347D09AA91B15482455F10E28E
+ BF8A9EDAD95A479A53BE999555BA173A5B3CED13E03FF5FAEB991A9878ECCB0AEC0E96
+ 7915BBCF4BC13690A7585EA7876A2114E22D018B38F1DDD88A8A53C4231C65F33803B1
+ 1397C8A6A00B2D0A38E6A306A845852B501F5B28233BE4F1A1BFF5E22D2A64DADAFD41
+ 0F699552564D43D65FB275F1DD02012E60378EE807EBDAE471E90744258F#)(e
+  #010001#)(protected openpgp-s2k3-ocb-aes ((sha1 #3E87FF0806B054D6#
+  "26420224")#7E8282B83522F91C53D76805#)#70B1997D514FF5800155A90FD785E7
+ 7DC2D782C48FC9BE44D0192C0AD56804468C910A202191EAD077B5542C95FE72BCC450
+ 0C2A8E8313C0CBD6C881236AC13E0BE893663946B5AABBDA57FFE4BA49973D547FA5DD
+ 1236DEF9FA5A9CE52F7AF1947F42A6C3502A47E8EF7E8CEFDDD44D0BFE090EA3220C2B
+ 52E11776DE36DD6C72D3B39A56F5D7295D26A69DB8CDAD1ABDBE1B21C1B754C9184E65
+ 2CAE169E2F492FA0EE5908AC5CB3BE5F4C7F6CD9F41314D1BD9B1DE713A4E6C7DFB11D
+ 2E64000ECFBBC89326B1322A8A227ECE7B919408C9187B5C9D53FC3985833E76D72164
+ 40B7386569E4DE270C3616ABD2A91A657AC58AA872704CB3DD4DF08C77D03D8E3CEDB5
+ 0D83BC3837FFE45D64B457AFE9A6ABF680637C51F80CB54691233BC4DE640026ACDAAC
+ 3FC0749FA8353F6EAD5D362A63C1CF25ACA73A9CF3290B54C18DB3214AF078D918682E
+ 513C434EFA06D9045571B1734CAE42990A1BE962D6E2A45846169EAAF2CDBD520813C2
+ D4DE97FFFABE582A02CA893F91EAF0EBCDCEB70B35850FDDF56EEA60C845A7E5C052C4
+ 33344776E7A4C787690CC0E13F32373EE425CA10520C251D045C0AE73EE7A0CA83C858
+ E2E528CDBD117BC022ED3F5DDE40CED0128B761E29B11F422C8E7C4281BF94F6F75D07
+ 0EE58426137548ABA38019A34DF1A66F700C29EF5545AC88BED75B5036801F0D8D4DB9
+ C6CCEA83D9DE3D626A04A80F218EFE9C74C173412A8A86786AB4A85403E8F8292CFED5
+ B8BA72FB5CE1BDD094AD9D633FD482F8FDBFA540DD2224149786ADA8DB6310A7C0C6E5
+ 9167815B2CEF34E7C458C41B5C56A79414BA57073E9B06D28CA08C56ED5E685EEA2BA5
+ DD112F87B253A0D02AC7CF93EDE93F48A80B2DB57B254937EA80E9AC1CEBD36FD297EB
+ C8A3B42CBC3D2CA732891B49457F3F15AA3F9BF93553968A07CB1A834B392F27B2D152
+ 47D93E46A6338694EA53CA0F5968109B4FAC9A#)(protected-at
+  "20210119T215925")))
+Created: 20210119T215908
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool256.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool256.asc
new file mode 100644
index 0000000..8427cfc
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool256.asc
@@ -0,0 +1,14 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mFMEWsODvRMJKyQDAwIIAQEHAgMEQLOxiiHZ/V6v3kvrhbnRtTp+oOPVpuvDKOiy
+gJOCZ7EWMVAwTr4syaSh8W8hdRgZ85Evv/1PYNFovYb6vzgVr7QJZWNjLWJwMjU2
+iJQEExMIADwCGwMFCwkIBwIDIgIBBhUKCQgLAgQWAgMBAh4DAheAFiEEBjPF9yoZ
+j1HmUOSr0Mij2vngY0oFAlxVsKIACgkQ0Mij2vngY0pARAD/RozGDidH/0aFlxeU
+VWNJKjPiax6vdHqur5dqBS/RhhIA/1sPUnyAIvAXXID1uhK6oIBRKi7WJ5rI7vSy
+rBR5MlNJuFcEWsODvRIJKyQDAwIIAQEHAgMEE9Vd8dIjHJkmRs/8MLz4Krfwz5BK
+hunq1T0xnp65OEZJd00VxA+VUXdEUHfaDehtSv7izCpq4lbXGCkEGFN7QwMBCAeI
+eAQYEwgAIAIbDBYhBAYzxfcqGY9R5lDkq9DIo9r54GNKBQJcVbCpAAoJENDIo9r5
+4GNK0MYA/2p5cq5smjSvKD/EGkosQwfcqkeygsQuEpDDLeEdsv8vAP9j+RHKX2tl
+W08zbayxGB0E+aCHuKCF8iLPeI4eroi/fw==
+=vsa4
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool384.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool384.asc
new file mode 100644
index 0000000..bdb20fe
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool384.asc
@@ -0,0 +1,17 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mHMEWsOEUBMJKyQDAwIIAQELAwMEivcvlPJsPmivhJcrfHx+ORxyum57GtRhWM49
+Yr8fJ48gyFqj9cLYOBdhEVvcfceyBPXmyt0TozWtjkGzgbF4LIvN1EB0DW0Rlsdn
+p72/hf0gnXvWZdD8euArX4RaAYuQtAllY2MtYnAzODSItAQTEwkAPAIbAwULCQgH
+AgMiAgEGFQoJCAsCBBYCAwECHgMCF4AWIQRbiiVMgjztmN7NEO1s8tzoVZmtogUC
+XFWwugAKCRBs8tzoVZmtoj1yAX9P1UV7FYpGUIP13aPP0d5Bx8HdQDAoexdXz3WW
+WPL/7OhSjPde23Q8TfgWyO21M2wBf1oWjOsDSjO5mDLCr7ypAFF6IJAgx76tSUe9
+Qmy7sL94OWDQ4+1Dccnc9GGiHLtRI7h3BFrDhFASCSskAwMCCAEBCwMDBETUkqGr
+7p8kX2dm38jzzxXRh1+OL7nmY168Zeo9yfwDbnyx8BoihP9ZgPWjGXmefT78GSfw
+ZDaYgC2NFQOcI/b8agh3PcjrXgZaFCZbUR9v2DnLUpCF8ZbxDJwEqweNTAMBCQmI
+mAQYEwkAIAIbDBYhBFuKJUyCPO2Y3s0Q7Wzy3OhVma2iBQJcVbDCAAoJEGzy3OhV
+ma2ig1IBfifduIiwdAlD45MOolSpHMX0AT7KoJHpt9ZFvWnjQkq9ZGEA/RA9vx7Z
+sLb7IsG1GgF/Sn+gtf/JIteXaZMnOhEOZ2oFUufij6o8gII8/9s8mkIjkrIICy//
+0n3q82ndFg0c
+=fcpz
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool512.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool512.asc
new file mode 100644
index 0000000..5b4bca2
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool512.asc
@@ -0,0 +1,19 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mJMEWsOEhxMJKyQDAwIIAQENBAMEA28ylDnn3LR7t6EQow5DszCpmUA+yup03LsT
+9o0Tw4/asi8nAz+1tlRY5HD49j53PziOlsGzKYa/jxGWjhVESgqLrJp/Eo65zK9v
+yDhX+iCkSYQ15WGKr3QaRUmBOUbX9PqE6dY+DDGQ1kewI93QIGCB1gn+OSmyKPm3
+YaVIbo60CWVjYy1icDUxMojUBBMTCgA8AhsDBQsJCAcCAyICAQYVCgkICwIEFgID
+AQIeAwIXgBYhBExZq5JyqmofYLhb0KpcWNFPe49IBQJcVbDYAAoJEKpcWNFPe49I
+F8UB/i8DwypbNusdbAqTbu1Twpn/QFMaVKHn8Ysgzqpimv+6hRq7uzekCvEOPOjl
+Oke5yLp8bpTTMPRKyfjNatQhdn8B/2+54qtXJuQd9oTSz7f2eFYvA8ZsMQgApYNl
+ksvKSw6dhSNX/DXK7JYIlgZXx7UGTkP4h3FQSiyCUJhVpClVVGa4lwRaw4SHEgkr
+JAMDAggBAQ0EAwRCtEqQkEgzQDxNGCj0duj0aGvnH+DHKlP4V6p9LJVIL0TuF1uZ
+BzP04efRvZT2vzCTcvvdE/G6G/txEZsR/989OchbkVWOPM/oCVktkaA02rBMcefh
+k9wKD+O9E3oHEN+tBt3yhmsv0MIR9IfPwi1GCsu55p4WUI//+ysB2T0YaQMBCgmI
+uAQYEwoAIAIbDBYhBExZq5JyqmofYLhb0KpcWNFPe49IBQJcVbDgAAoJEKpcWNFP
+e49IZQUB/R4U4aWBMimZSL8kyaK+/Y8NcInIGqRzrm5zPnTSHrgQeH55SVKahzsq
+j57D1Ec1HnUd4ctISVocOxpUfnJq5NAB/1fzbh+1RN2ZyNW6tAJlA/Irkwzzbil9
+6fAIvRolwwaGsUZNMEiCF3rTcFaenJg9UhQvX6BoqXCdwawqTZCRN6E=
+=h+On
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/dsa-elgamal.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/dsa-elgamal.asc
new file mode 100644
index 0000000..db06732
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/dsa-elgamal.asc
@@ -0,0 +1,44 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQMuBFrDf8YRCACHbPXT8jG3RNWdNms9xvdaiLrY+Iui1Gq2WGLSajPEZVASEWN+
+JuuX8k9d05rb+F2VAqLnW3CQreDW6unVNeRf52tdM8J4eXmeu/Bkk8y1Qx/HbGca
+sAGSIEKg34TuV5Ly5m4Z07bs3HPYUUQbmu0uclGfnX/ArZ+4Jp+uypC9bErdiXM0
+cM7d52tb9IvOlXNu23rzHDbgVP6qF/AxLSRD8SQPvshu3/5b0bvdBkHVk+dHoLO0
+fC5j476ibHGGZcnPMrTwqEIAxUCy5wQ3Lb/2/G31kuV55bAZ41tUNEvfzbiRN1L5
+1uiO+XX96bRqLN13t0Coaba9fq1aN5Zr6piXAQCuNzvj8aaLXAOEXVRej6a2k+/C
+Jny91MgjSM701twUDQf/RMWHwQuFPe6zSDQs4pWlxkHwXJw3AVidkoWg/DCwv2pJ
+5VYQxBXRwND2OhcZvmeDT94UzPws0dFbprWyymtA49ZXitPGzFARAFWHWxk/IsOf
+Idc6w5eHXDMHxLhiPFqfjKeNpibzO2P7LXP2bUKzwybkKZarz1N6pfanDXAtC4DU
+SC3qWNqywYlfINAGCdwsPu5qFUNSnkjTYxe/MiHb4kL1p/z8qFNWrvg6GryXygp5
+cLdqckjPaUHlR+B9wQZIVRzVdlFAbMDJ0EERLFG7FbIuY8dzy5x7n+oBOgRxee2I
+ytUpGVMLIJuecARLXNKsMXviCMYVE7Tj5hiSoM0TIgf8CwLLFsSa0EDm/wlXYZMj
+2gg3Z8iCz6ycxvFD9PXNt+8jyELO8CwS2pWu7ptBgaugkinqd40EQslQoP76CcHq
+bGQEohm4SnmfGsV8dicuziMVVKkVrYgbGvZ5cQ/ONGTTnSJuiTPN19oztwh8JOEc
+Jd4l+wFuVSm8OS1mj5eexeX1Tz3NfWQMT4deKh+jiTLe9Sw/57sSjxiw/8IczqhN
+Fu3YIy40d3Bv+OF6i8I+94WLbJPiX1ban1wqcA0bMaps1aYVtTRZ+mP0b3M9W7qa
+383/SLCBjUzQ7zm6PX/7uAXFyZOQfcyLaJ8Hc34yOE0git1DWmRS7U16Zv54v1Uh
+HbQGZHNhLWVniJQEExEIADwCGwMFCwkIBwIDIgIBBhUKCQgLAgQWAgMBAh4DAheA
+FiEECRxEzpz7w/9+x6ZNyKEKfXgnPhAFAlxVr2sACgkQyKEKfXgnPhABkgD7BfEd
+jhB+ApC9icNLs893i2jiHbAxZGSOQMRhCaJ7AzoBAIipcSIsBa3LJ4eTec1esiCY
+a8xzxquCTA+oANNoX7p6uQMNBFrDf8YQDADMPZ6+/YAIjXMLfQKX80jz6FZ4Kdfx
+Dc60m4O+ZElMv7eXtQJC2L/xOh4Th1fZUQIhSdtFiwaCSkCD8occfJwyt+lH3Dj0
+Qrh3mIycAfPrjj0Rgxz8nRQbBLDbLF1QGPimt0zP69ByJ3opLujVVi5ixwgwza9S
+eGffKwGdyb9uFcB9MVnC997zfLvx/uNV44BwLnCH6Tp68Lynf+FpuvSX+Rsj4li3
+UiLoVxEIbBZ/5Bn3ygc7aW4fM4bR4hKjWwJR0Hh1y/kt6A7dEAypVKBfSqAtAu2A
+zYAq3USsbtq1X1FaGEsmvcJY8IGa+aLTArq7dkhXzcv7K3EVdqOawS1zS/ARuG+B
+k6kct3zzyj1EitiTvdMAkGOoyk16qKVzUcbFRVC1HsZtxYj6OxU4Eazvh0LjvZ0A
++eft/XO/ZmN6vyRaF1/10z+uHPfj1FLMpS8Zn4SN6x7Qtsx3iLL1n9cKBDFRXCqD
+HDaxLVC3N4zAI2hMMmZid+fbTuhsqYbSX2cAAwUL/j/H4/9Ml7PUUCXoozX2V4K+
+gi6WEYmY+pXN/we9NuFulW4aURo7jK4wRYBu0BS3K9e8f8WUMAV5V6ShPWHXcobt
+iuSjLYJwdBJkgHbnKFWPZUozJ3Ftyp0Lh1M5bN7/ECofAxLHbRpCVrcOP2LC7vAU
+AeMgdiFDqEiLCnr/aGvqUOxbGO6Isi4jvaM/ZUfGjGe/Z6yVoqm6wEsNM7+9cFGQ
+QR1lRPeCPKcLeasCdbM5EIt1aLFNijZigWuDRLIgG5PuzA8Kpdk/u/UuCUeUFwJN
+ym8MEv2JJDiWHmb8IcgFMp40VenUs0fte0LWwrMjWVPpLsHKmkraRjQ1UtarRhT0
+ANYilGjZWCnCb11xGKhlM7r5IkLGY/L/Eh4vjLgg9T5rGwOF8p1jSgx9mA8SpHV0
+O0BoKNX1ApWEHayTLcyayCnTYbY/e4axnSKodixAI/NghOnJHqGr4LeZeKk/Q0mm
+GlljzFv3EAdoru4DVowWGFBmrwBy7o+GLgHs6K/+yIh4BBgRCAAgAhsMFiEECRxE
+zpz7w/9+x6ZNyKEKfXgnPhAFAlxVr64ACgkQyKEKfXgnPhCETQEApruWUqCwfibQ
+vyI/OZohPzljlvIoioj3rFjYNpufQD8A/RTaYtnPiEvsPynEZCj9zTV/SuHiKbHS
+v5BhpoOOm+jM
+=PnGk
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/ed25519.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/ed25519.asc
new file mode 100644
index 0000000..636a5a9
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/ed25519.asc
@@ -0,0 +1,9 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEWsN6MBYJKwYBBAHaRw8BAQdAAS+nkv9BdVi0JX7g6d+O201bdKhdowbielOo
+ugCpCfi0CWVjYy0yNTUxOYiUBBMWCAA8AhsDBQsJCAcCAyICAQYVCgkICwIEFgID
+AQIeAwIXgBYhBCH8aCdKrjtd45pCd8x4YniYGwcoBQJcVa/NAAoJEMx4YniYGwco
+lFAA/jMt3RUUb5xt63JW6HFcrYq0RrDAcYMsXAY73iZpPsEcAQDmKbH21LkwoClU
+9RrUJSYZnMla/pQdgOxd7/PjRCpbCg==
+=miZp
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/faked.key b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/faked.key
new file mode 100644
index 0000000..405afad
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/faked.key
@@ -0,0 +1,23 @@
+Meta-Description: Example from GPG file 'keyformat.txt'.
+Description: Key to sign all GnuPG released tarballs.
+  The key is actually stored on a smart card.
+Use-for-ssh: yes
+OpenSSH-cert: long base64 encoded string wrapped so that this
+  key file can be easily edited with a standard editor.
+Token: D2760001240102000005000011730000 OPENPGP.1
+Token: FF020001008A77C1 PIV.9C
+Key: (shadowed-private-key
+  (rsa
+  (n #00AA1AD2A55FD8C8FDE9E1941772D9CC903FA43B268CB1B5A1BAFDC900
+  2961D8AEA153424DC851EF13B83AC64FBE365C59DC1BD3E83017C90D4365B4
+  83E02859FC13DB5842A00E969480DB96CE6F7D1C03600392B8E08EF0C01FC7
+  19F9F9086B25AD39B4F1C2A2DF3E2BE317110CFFF21D4A11455508FE407997
+  601260816C8422297C0637BB291C3A079B9CB38A92CE9E551F80AA0EBF4F0E
+  72C3F250461E4D31F23A7087857FC8438324A013634563D34EFDDCBF2EA80D
+  F9662C9CCD4BEF2522D8BDFED24CEF78DC6B309317407EAC576D889F88ADA0
+  8C4FFB480981FB68C5C6CA27503381D41018E6CDC52AAAE46B166BDC10637A
+  E186A02BA2497FDC5D1221#)
+  (e #00010001#)
+  (shadowed t1-v1
+   (#D2760001240102000005000011730000# OPENPGP.1)
+  )))
\ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp256.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp256.asc
new file mode 100644
index 0000000..fd1509e
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp256.asc
@@ -0,0 +1,14 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mFIEWsOCNRMIKoZIzj0DAQcCAwQS5G6mn5dhamZ6678SXE1azavqf8BItWO9Qv8V
+dS1vEEoD14urr5OQKTLuHhDRjvSQdaxRtkf0sI51T7230sT3tAhlY2MtcDI1NoiU
+BBMTCAA8AhsDBQsJCAcCAyICAQYVCgkICwIEFgIDAQIeAwIXgBYhBLVP3ru2c0I6
+XQqlRCNnTyGyRBUnBQJcVa/nAAoJECNnTyGyRBUn1ycA+wVg9sEfHDBaGtLqlUSB
+WdGKURrHN7CJe2UTz1/7oQCBAQDDi4RQyLHs+TfOrBNSbLEswCu1oEh8VmHt/SN7
++mqNLbhWBFrDgjUSCCqGSM49AwEHAgMELDOArLIG85ABQu1IwgQMpiIuUwj+N7ib
+gGenTRck5dkBpX48eK3lbjovXn4YkBneA7z14iez3+Sdg6UFAMFV2QMBCAeIeAQY
+EwgAIAIbDBYhBLVP3ru2c0I6XQqlRCNnTyGyRBUnBQJcVa/vAAoJECNnTyGyRBUn
+ZKoBAJ64gv3w27nFBERvIsRqufvR6xcimqS7Gif+WehBU+P5AQC5bqoISh0oSQid
+adI84f60RuOaozpjvR3B1bPZiR6u7w==
+=H2xn
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp384.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp384.asc
new file mode 100644
index 0000000..b2b5995
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp384.asc
@@ -0,0 +1,16 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mG8EWsOCnBMFK4EEACIDAwTRTCLBHlq6SUTiXZfWR0vbUh/0VAlrePaqHVIE4LEK
+0UBhPCIQOGuGL4JIufc8UzBWhiMLJ0z7KRjBWufsMZR2+rqj+unOK2lLu7sc9or8
+X6B74hhP3Ili24PgGFAeAG+0CGVjYy1wMzg0iLQEExMJADwCGwMFCwkIBwIDIgIB
+BhUKCQgLAgQWAgMBAh4DAheAFiEEqyXLoELdkkw6zD7TJCo6peqF9EoFAlxVsBEA
+CgkQJCo6peqF9EooJQF7BPZelriXwZ/kJzaamImHBddkLFc7d2WbuSfDxEZQ+Mfw
+BAP3+QYUaFtfeqApjY69AX4w6LhTUgI2kl4O0Vc7ZOlqZBlwAc8CMV08TTfOEio2
+b51SItvhLdDrFRJ2K4jiO+a4cwRaw4KcEgUrgQQAIgMDBORWqhYflSrYzF04SK8q
+8Om+DYTvwRtUlr3Aoq44+gm5yBcmJmgT3TKrp/bx5Jg/zwzIASFn0agbxkqKpQqH
+sHeelWsSBROQzy98HXdCp3nVmghI2aDk8zdD6AV4m7c2ewMBCQmImAQYEwkAIAIb
+DBYhBKsly6BC3ZJMOsw+0yQqOqXqhfRKBQJcVbAZAAoJECQqOqXqhfRKgAIBf3Wk
+TsqUA1JXkPGetA9sjHglIICN+DZY5k+PwTJUxaW2zrkiPJ3BYEnKbmmBLzA7BgGA
+4RYatyl2WOUYh/poRLgu7JpE4oRqdmNA+QOpCILMId1AeXfj4W01RKFWaKeH+3Yy
+=2H/0
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp521.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp521.asc
new file mode 100644
index 0000000..db18f81
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp521.asc
@@ -0,0 +1,19 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mJMEWsODGxMFK4EEACMEIwQA8OZCJ8Iv4Qr2oRr0kqez0nPSW/vNxYBZRpCJ9ab8
+kVaRhW7+5vEsecm5XugZSePbAmERmk3oEiSgu35r6aovowcAGrOBfBm9fyIVqaoX
+veTS3yRHH6NEf044+pC+uBaaFukkrDmVTecdRvYr3Yrdc5ifyOve053znlpQ6a4n
+9bh4GGy0CGVjYy1wNTIxiNYEExMKADwCGwMFCwkIBwIDIgIBBhUKCQgLAgQWAgMB
+Ah4DAheAFiEET7Of9vpIV6S9fvW0IJLKgyQmO2oFAlxVsCkACgkQIJLKgyQmO2oK
+DwIGO72zo6otVkbHfeI9hWx/8FAOXh4MT4YtDicF/sj8QbHzdbEBHcLCByLYAnph
+8VVoCxpPcBLmNSHbNyreoksjEE0CB10P5kPrd/bYkdNx/HTu+1i8f7a448+Nr2rF
+PwdI9tOsghkT41qobZavjjnBlT/xv5DqXldJkEfaeiJxPHOKvMhWuJcEWsODGxIF
+K4EEACMEIwQBAY7ZCAjks1MWWxibg/EVaz5t6iEKJTwu8mGGKWdPZAQRKKNtNpf0
+pZAMV3I8ue/WQMsYKRYv5AGq1PnjV19DmLsA0aGw4MDM260coctkcn/2MAJQMC9+
+3Z+BJS3hqzwDuZ+LS13r0RLpgnt3ks+3ucG4II38ZZ1lTwKoIc+w/OuhsOIDAQoJ
+iLsEGBMKACACGwwWIQRPs5/2+khXpL1+9bQgksqDJCY7agUCXFWwLgAKCRAgksqD
+JCY7ahqbAgkBiXYtiBlp5dmSYnbc4JoIYWcxTBQ+/dGHyU6ZEfC5VQz2mrdJetK1
+bIID0rFSsd24/8IzAqM3L+nY9h9bULWroroCBjTohh0j2EbW+hFOrRqL01osnlY+
+1/G8e44blB5JqsPI9FqOZOUj6IzsUuV1N9gJbm1RHu/hSpm52d6rX4nOTbqt
+=3jnl
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/rsa.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/rsa.asc
new file mode 100644
index 0000000..e74df7a
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/rsa.asc
@@ -0,0 +1,40 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQGNBFrDgZ4BDACxg83nNkYvIAz6Nk4Np4BscWDrrL3tyiiQSz1yfCotxO8zUtVl
+JorSlqRurNdAYU2XJLakMqpQHE7VVIMI/WdXCC8CrQbULOBxIf/LGiQf0VDo8ukg
+iFFd5vUeMRRILWKnMc/GCmFkFOUHd1Y60h96oe5f/d286fRZQnCO8PGS8CgB1mDJ
+GBY8U1DCCkt6g9O6bfFkfcwetr1+kB2cBIY7uDzN1Sm8dz2VrkqPqtqt/F02giRN
+hD2GxX9HVYCkCKEE9DFsB+MDKR9z5lXrI3SAsL/3htXK/ukWgBe4DIFjTXbrzgP+
+nuWb4s1NxwSD7yjlnqVD7mZTGAwMsQbCjhwIeY4t5onsCJ4/40zm1jIZ5TJsfQ/b
+5gA7iu9kMKzxO0bPI2loJEB/K9aH7qsyQHgtt7G3+G8JrixLBUobtreY9V2QvhtC
+Q4OAHRO5nIKYIazDuKv9bxautZ0WuLOk/qmWMMoqFAn5bzQeVTTBD6kvr6l8/+MZ
+zaij9YPeUbeKJuMAEQEAAbQHcnNhLXJzYYkB0gQTAQIAPAIbAwULCQgHAgMiAgEG
+FQoJCAsCBBYCAwECHgMCF4AWIQRrwEpaPds1dmuaQNgvuReRGImOiwUCXFWwhwAK
+CRAvuReRGImOi1NiC/0RPjbQTWMw4/GxIMkRb9kmmsSUe7kCgqBCqZTxcI7rxZdn
+JFDbP5c6DAS/11bRQJ9OsoUjbDx0d81UuBlXB/mNsb9nCcXOrqAUHRqgWoNSDk4g
+9Oa1Kx77OM9BvRJbJGch2YW5Wcch5vcqQNu+6x3VGt7ipljYEJSQ6Dre+dgxYjXK
+60x63/ulFk2XImPQYjQ8VHbW/HDg/+DLf++phjVy9l58U1sUKSSdO8uuYoW6dBv2
+xRg18Sn3DWOU+mrkV8Ld95+NRRE1cSHTQv5hu4ELqrV+YdGNmv9DgQAMJOl3xy8i
+vOz4cpKaOBasm423wr5Y56nOTzLFN+dxnYR8tbqswLkCldRY6fvL1NsS77rj0yZp
+pecCyi0E6RAcmSiZJqpnOpcuI76AkZuWSDEP3Y3x5QBf5fu2uiQQsPXYN8ie6xcC
+zeYtXsHyNxF0oBh2c26po8fo4E4T70RSO8Oqs1XzXnjIIle8pKU9M5U5ISbWS3Hj
+vtOn5ZrLC5KYnVRna3G5AY0EWsOBngEMALmXpJoPC1m4THYrfHibtt2/OwAlDm20
+3xn+Klw69bkeXdc71wsLOAHVL3+7gXpip+IYmN7CBIyqlOCtsluu+gwP3MczPJZX
+vk1uXMMfLKiXl9Kodx/Fqq0Y26Tqse5PPMlagPStIvKyT0WTa3RCD28uVklapLuy
+1w1k4G5hIDPt6uKyxXq/HzneRSGWafmqoCWOmXQZzfOMG249bMXNOcPMJhOejPRS
+jREnnntbpvZ8DU/38+JFtqCUkPwuqYQkvGCKReSBifMiG3XAhHWOGzXPzdW1XdAi
+aA+NQP/kMUs45jS3hDdp4EObllYRBsQwtFpKPMNmwaVuOmVlbrXTP0YsDYGndkE6
+5nJ46/2xPhl8+nIgDLg3SBBzQdOiPOGtHYjs0bRKdwXTeAq4fDq0vCQXMJF2fwAQ
+LEYWs7kabKhcPpWzTtoCG78WzR3TgldEPhPjE0offvVQO56x1XDqMBctoiDWkWkS
+bdi03GhbFdK5A7uYBTJYEoo61Yp/2/MjyQARAQABiQG2BBgBAgAgAhsMFiEEa8BK
+Wj3bNXZrmkDYL7kXkRiJjosFAlxVsI8ACgkQL7kXkRiJjoumjwv/c6a9G1bi3sh7
+wRFhpsrFUoFfEBDI4eyI/haWhCIfI8n7p3lCSIy8lmf9yvUs75d5M3EQW08NQjIs
+/o8FcFoUBnKQv2whWSHTpx/BkuhcNVY+NIwyBKomU0WkFSm3+80ix0uh97KSlRlW
+Q5nMVExNxZ4mRFAhDQrJ2/pZ2DaddeO+4uZ7Twquaix+PMxpNKvkj2+757L23YjF
+QmHdk6E8burofpSCfBTB84eUSDvzs6Eb/34/KlbBZhKMYdffMDCSAZIMfIav6YVJ
+UDzT40kmS0vRW6bDIetSbpBM+GD1cSq0wKdlt+Giur9ZiaiyHIEqbPgr6WgdND25
+Vx/i23Ik2o8wMb0Ub8cKD9wjdGAk+Rt2r7d2RzyO/R3ThKbUOGkQX6acAAZAjhPs
+UGxt1dDojmQ3nF4l2hZ9PcsyD3pz226wUUPT4JA1eE6tdoVjzY2J7EhfNaVcQQlb
+bQJQ+BQcO4oP1mPRCx1GiSmB+jRNQ4npxVJxLO/j7T27CrSZbhT7
+=aibx
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/secp256k1.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/secp256k1.asc
new file mode 100644
index 0000000..837f8a8
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/secp256k1.asc
@@ -0,0 +1,14 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mE8EWsOE8xMFK4EEAAoCAwQVHqFZWqedXUIkNFs82PsQB3bsCDhrL/73xZca3+vo
+kB4T7jHcACThuMZYuUqUo9NzNTJioluOvZG+UdYXPdfdtAplY2MtcDI1NmsxiJQE
+ExMIADwCGwMFCwkIBwIDIgIBBhUKCQgLAgQWAgMBAh4DAheAFiEEgfdytX1Ov+cA
+CmYjPqW7b5aSwaAFAlxVsQ0ACgkQPqW7b5aSwaD2tQD/R4d15NBuSJ6IB1brH0E9
+nEWkqo892PaAY5akdCO/i9EBAMsjE5NPxBnCs03c+VHFU200k27ixdrWpUa+HZEI
+A5wSuFMEWsOE8xIFK4EEAAoCAwSUWwe7CaaOYRANiKet2evLiOumefIHuvRpyOSK
+hyRdclIWpBUCAWEnmalkEL/8cEM5fjtILtCOKXqCOBsPv45HAwEIB4h4BBgTCAAg
+AhsMFiEEgfdytX1Ov+cACmYjPqW7b5aSwaAFAlxVsRUACgkQPqW7b5aSwaCETgD/
+YXzCMYMbPGAU2oTitjAno8hDWmgTeaFWeCmqf6l9mP8BAKvpewWeFGZfWGAQcWPi
+E+jv7vadvEt1yMA8rmT041F5
+=mDCI
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/x25519.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/x25519.asc
new file mode 100644
index 0000000..d531d7a
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/x25519.asc
@@ -0,0 +1,13 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEW8SVGhYJKwYBBAHaRw8BAQdA9SMZ2uw0YugMFcl5TpEZeBRAGniEk9a42XNs
+7QA4Tky0DGVkZHNhLXgyNTUxOYiQBBMWCAA4FiEETJc4pvK+Thp5bJt7lBgioPwb
+MKUFAlvElRoCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQlBgioPwbMKUi
+1wEAgMq3X7o17OJBPfY3He/exDR6LhWwAAXrVQR/WdRiHkEBALd1Mj0BlZZLoKTr
+uJ4MD5CYZLicXTRwOv6e52F/DHwJuDgEW8SVGhIKKwYBBAGXVQEFAQEHQA0Lh2mG
+lB1O4xDYgztm/aX7+8AdHEGaMsCF1RQ6wVUeAwEIB4h4BBgWCAAgFiEETJc4pvK+
+Thp5bJt7lBgioPwbMKUFAlvElRoCGwwACgkQlBgioPwbMKXmlQD+KxVg2dGL8lRW
+rQajwzmuwMrJX1lvJylg5Ozk6SGrBeABANZrdt8bmArEqeRVxFO2F4P7btyIpf1w
+5aNpqqtvkRcB
+=EYfV
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocatorTest.java b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocatorTest.java
index 7446201..5f43378 100644
--- a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocatorTest.java
+++ b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocatorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2019, 2020 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
@@ -53,7 +53,7 @@
 		assertFalse(match(USER_ID, "<heinrichh>"));
 		assertFalse(match(USER_ID, "<uni-duesseldorf>"));
 		assertFalse(match(USER_ID, "<h@u>"));
-		assertFalse(match(USER_ID, "<HeinrichH@uni-duesseldorf.de>"));
+		assertTrue(match(USER_ID, "<HeinrichH@uni-duesseldorf.de>"));
 		assertFalse(match(USER_ID.substring(0, USER_ID.length() - 1),
 				"<heinrichh@uni-duesseldorf.de>"));
 		assertFalse(match("", "<>"));
@@ -72,8 +72,8 @@
 		assertFalse(match(USER_ID, "@ "));
 		assertFalse(match(USER_ID, "@"));
 		assertFalse(match(USER_ID, "@Heine"));
-		assertFalse(match(USER_ID, "@HeinrichH"));
-		assertFalse(match(USER_ID, "@Heinrich"));
+		assertTrue(match(USER_ID, "@HeinrichH"));
+		assertTrue(match(USER_ID, "@Heinrich"));
 		assertFalse(match("", "@"));
 		assertFalse(match("", "@h"));
 	}
@@ -110,6 +110,7 @@
 	public void testExplicitFingerprint() throws Exception {
 		assertFalse(match("John Fade <j.fade@example.com>", "0xfade"));
 		assertFalse(match("John Fade <0xfade@example.com>", "0xfade"));
+		assertFalse(match("John Fade <0xfade@example.com>", "0xFADE"));
 		assertFalse(match("", "0xfade"));
 	}
 
@@ -128,7 +129,7 @@
 		assertTrue(match("John Fade <0xfade@example.com>", "*0xfade"));
 		assertTrue(match("John Fade <0xfade@example.com>", "*0xFADE"));
 		assertTrue(match("John Fade <0xfade@example.com>", "@0xfade"));
-		assertFalse(match("John Fade <0xfade@example.com>", "@0xFADE"));
+		assertTrue(match("John Fade <0xfade@example.com>", "@0xFADE"));
 		assertFalse(match("", "0x"));
 	}
 }
diff --git a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip25519Test.java b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip25519Test.java
new file mode 100644
index 0000000..e300802
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip25519Test.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import static org.junit.Assert.assertEquals;
+
+import java.math.BigInteger;
+import java.util.Locale;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.util.encoders.Hex;
+import org.eclipse.jgit.util.sha1.SHA1;
+import org.junit.Test;
+
+public class KeyGrip25519Test {
+
+	interface Hash {
+		byte[] hash(SHA1 sha, BigInteger q) throws PGPException;
+	}
+
+	private void assertKeyGrip(String key, String expectedKeyGrip, Hash hash)
+			throws Exception {
+		SHA1 grip = SHA1.newInstance();
+		grip.setDetectCollision(false);
+		BigInteger pk = new BigInteger(key, 16);
+		byte[] keyGrip = hash.hash(grip, pk);
+		assertEquals("Keygrip should match", expectedKeyGrip,
+				Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT));
+	}
+
+	@Test
+	public void testCompressed() throws Exception {
+		assertKeyGrip("40"
+				+ "773E72848C1FD5F9652B29E2E7AF79571A04990E96F2016BF4E0EC1890C2B7DB",
+				"9DB6C64A38830F4960701789475520BE8C821F47",
+				KeyGrip::hashEd25519);
+	}
+
+	@Test
+	public void testCompressedNoPrefix() throws Exception {
+		assertKeyGrip(
+				"773E72848C1FD5F9652B29E2E7AF79571A04990E96F2016BF4E0EC1890C2B7DB",
+				"9DB6C64A38830F4960701789475520BE8C821F47",
+				KeyGrip::hashEd25519);
+	}
+
+	@Test
+	public void testCurve25519() throws Exception {
+		assertKeyGrip("40"
+				+ "918C1733127F6BF2646FAE3D081A18AE77111C903B906310B077505EFFF12740",
+				"0F89A565D3EA187CE839332398F5D480677DF49C",
+				KeyGrip::hashCurve25519);
+	}
+}
diff --git a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGripTest.java b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGripTest.java
new file mode 100644
index 0000000..a4aaf40
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGripTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Security;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.function.Consumer;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.bouncycastle.util.encoders.Hex;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class KeyGripTest {
+
+	@BeforeClass
+	public static void ensureBC() {
+		if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+			Security.addProvider(new BouncyCastleProvider());
+		}
+	}
+
+	protected static class TestData {
+
+		String filename;
+
+		String[] expectedKeyGrips;
+
+		TestData(String filename, String... keyGrips) {
+			this.filename = filename;
+			this.expectedKeyGrips = keyGrips;
+		}
+
+		@Override
+		public String toString() {
+			return filename;
+		}
+	}
+
+	@Parameters(name = "{0}")
+	public static TestData[] initTestData() {
+		return new TestData[] {
+				new TestData("rsa.asc",
+						"D148210FAF36468055B83D0F5A6DEB83FBC8E864",
+						"A5E4CD2CBBE44A16E4D6EC05C2E3C3A599DC763C"),
+				new TestData("dsa-elgamal.asc",
+						"552286BEB2999F0A9E26A50385B90D9724001187",
+						"CED7034A8EB5F4CE90DF99147EC33D86FCD3296C"),
+				new TestData("brainpool256.asc",
+						"A01BAA22A72F09A0FF0A1D4CBCE70844DD52DDD7",
+						"C1678B7DE5F144C93B89468D5F9764ACE182ED36"),
+				new TestData("brainpool384.asc",
+						"2F25DB025DEBF3EA2715350209B985829B04F50A",
+						"B6BD8B81F75AF914163D97DF8DE8F6FC64C283F8"),
+				new TestData("brainpool512.asc",
+						"5A484F56AB4B8B6583B6365034999F6543FAE1AE",
+						"9133E4A7E8FC8515518DF444C3F2F247EEBBADEC"),
+				new TestData("nistp256.asc",
+						"FC81AECE90BCE6E54D0D637D266109783AC8DAC0",
+						"A56DC8DB8355747A809037459B4258B8A743EAB5"),
+				new TestData("nistp384.asc",
+						"A1338230AED1C9C125663518470B49056C9D1733",
+						"797A83FE041FFE06A7F4B1D32C6F4AE0F6D87ADF"),
+				new TestData("nistp521.asc",
+						"D91B789603EC9138AA20342A2B6DC86C81B70F5D",
+						"FD048B2CA1919CB241DC8A2C7FA3E742EF343DCA"),
+				new TestData("secp256k1.asc",
+						"498B89C485489BA16B40755C0EBA580166393074",
+						"48FFED40D018747363BDEFFDD404D1F4870F8064"),
+				new TestData("ed25519.asc",
+						"940D97D75C306D737A59A98EAFF1272832CEDC0B"),
+				new TestData("x25519.asc",
+						"A77DC8173DA6BEE126F5BD6F5A14E01200B52FCE",
+						"636C983EDB558527BA82780B52CB5DAE011BE46B")
+		};
+	}
+
+	// Injected by JUnit
+	@Parameter
+	public TestData data;
+
+	private void readAsc(InputStream in, Consumer<PGPPublicKey> process)
+			throws IOException, PGPException {
+		PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
+			PGPUtil.getDecoderStream(in), new JcaKeyFingerprintCalculator());
+
+		Iterator<PGPPublicKeyRing> keyRings = pgpPub.getKeyRings();
+		while (keyRings.hasNext()) {
+			PGPPublicKeyRing keyRing = keyRings.next();
+
+			Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
+			while (keys.hasNext()) {
+				process.accept(keys.next());
+			}
+		}
+	}
+
+	@Test
+	public void testGrip() throws Exception {
+		try (InputStream in = this.getClass()
+				.getResourceAsStream(data.filename)) {
+			int index[] = { 0 };
+			readAsc(in, key -> {
+				byte[] keyGrip = null;
+				try {
+					keyGrip = KeyGrip.getKeyGrip(key);
+				} catch (PGPException e) {
+					throw new RuntimeException(e);
+				}
+				assertTrue("More keys than expected",
+						index[0] < data.expectedKeyGrips.length);
+				assertEquals("Wrong keygrip", data.expectedKeyGrips[index[0]++],
+						Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT));
+			});
+			assertEquals("Missing keys", data.expectedKeyGrips.length,
+					index[0]);
+		}
+	}
+}
diff --git a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java
new file mode 100644
index 0000000..5e5e303
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeysTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Security;
+import java.util.Iterator;
+
+import javax.crypto.Cipher;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SecretKeysTest {
+
+	@BeforeClass
+	public static void ensureBC() {
+		if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+			Security.addProvider(new BouncyCastleProvider());
+		}
+	}
+
+	private static volatile Boolean haveOCB;
+
+	private static boolean ocbAvailable() {
+		Boolean haveIt = haveOCB;
+		if (haveIt != null) {
+			return haveIt.booleanValue();
+		}
+		try {
+			Cipher c = Cipher.getInstance("AES/OCB/NoPadding"); //$NON-NLS-1$
+			if (c == null) {
+				haveOCB = Boolean.FALSE;
+				return false;
+			}
+		} catch (NoClassDefFoundError | Exception e) {
+			haveOCB = Boolean.FALSE;
+			return false;
+		}
+		haveOCB = Boolean.TRUE;
+		return true;
+	}
+
+	private static class TestData {
+
+		final String name;
+
+		final boolean encrypted;
+
+		final boolean keyValue;
+
+		TestData(String name, boolean encrypted, boolean keyValue) {
+			this.name = name;
+			this.encrypted = encrypted;
+			this.keyValue = keyValue;
+		}
+
+		@Override
+		public String toString() {
+			return name;
+		}
+	}
+
+	@Parameters(name = "{0}")
+	public static TestData[] initTestData() {
+		return new TestData[] {
+				new TestData("AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11", false, false),
+				new TestData("2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A", false, true),
+				new TestData("66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9", true, true),
+				new TestData("F727FAB884DA3BD402B6E0F5472E108D21033124", true, true),
+				new TestData("faked", false, true) };
+	}
+
+	private static byte[] readTestKey(String filename) throws Exception {
+		try (InputStream in = new BufferedInputStream(
+				SecretKeysTest.class.getResourceAsStream(filename))) {
+			return SecretKeys.keyFromNameValueFormat(in);
+		}
+	}
+
+	private static PGPPublicKey readAsc(InputStream in)
+			throws IOException, PGPException {
+		PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
+			PGPUtil.getDecoderStream(in), new JcaKeyFingerprintCalculator());
+
+		Iterator<PGPPublicKeyRing> keyRings = pgpPub.getKeyRings();
+		while (keyRings.hasNext()) {
+			PGPPublicKeyRing keyRing = keyRings.next();
+
+			Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
+			if (keys.hasNext()) {
+				return keys.next();
+			}
+		}
+		return null;
+	}
+
+	// Injected by JUnit
+	@Parameter
+	public TestData data;
+
+	@Test
+	public void testKeyRead() throws Exception {
+		if (data.keyValue) {
+			byte[] bytes = readTestKey(data.name + ".key");
+			assertEquals('(', bytes[0]);
+			assertEquals(')', bytes[bytes.length - 1]);
+		}
+		try (InputStream pubIn = this.getClass()
+				.getResourceAsStream(data.name + ".asc")) {
+			if (pubIn != null) {
+				PGPPublicKey publicKey = readAsc(pubIn);
+				// Do a full test trying to load the secret key.
+				PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
+						.build();
+				try (InputStream in = new BufferedInputStream(this.getClass()
+						.getResourceAsStream(data.name + ".key"))) {
+					PGPSecretKey secretKey = SecretKeys.readSecretKey(in,
+							calculatorProvider,
+							data.encrypted ? () -> "nonsense".toCharArray()
+									: null,
+							publicKey);
+					assertNotNull(secretKey);
+				} catch (PGPException e) {
+					// Currently we may not be able to load OCB-encrypted keys.
+					assertTrue(e.getMessage().contains("OCB"));
+					assertTrue(data.encrypted);
+					assertFalse(ocbAvailable());
+				}
+			}
+		}
+	}
+
+}
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
index a75778f..3fbdb2a 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
@@ -3,27 +3,35 @@
 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="[5.10.1,5.11.0)"
+Fragment-Host: org.eclipse.jgit;bundle-version="[5.11.2,5.12.0)"
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.bouncycastle.bcpg;version="[1.65.0,2.0.0)",
+Import-Package: org.bouncycastle.asn1;version="[1.65.0,2.0.0)",
+ org.bouncycastle.asn1.cryptlib;version="[1.65.0,2.0.0)",
+ org.bouncycastle.asn1.x9;version="[1.65.0,2.0.0)",
+ org.bouncycastle.bcpg;version="[1.65.0,2.0.0)",
+ org.bouncycastle.bcpg.sig;version="[1.65.0,2.0.0)",
+ org.bouncycastle.crypto.ec;version="[1.65.0,2.0.0)",
  org.bouncycastle.gpg;version="[1.65.0,2.0.0)",
  org.bouncycastle.gpg.keybox;version="[1.65.0,2.0.0)",
  org.bouncycastle.gpg.keybox.jcajce;version="[1.65.0,2.0.0)",
+ org.bouncycastle.jcajce.interfaces;version="[1.65.0,2.0.0)",
+ org.bouncycastle.jcajce.util;version="[1.65.0,2.0.0)",
  org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
+ org.bouncycastle.math.ec;version="[1.65.0,2.0.0)",
+ org.bouncycastle.math.field;version="[1.65.0,2.0.0)",
  org.bouncycastle.openpgp;version="[1.65.0,2.0.0)",
+ org.bouncycastle.openpgp.jcajce;version="[1.65.0,2.0.0)",
  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;version="[1.65.0,2.0.0)",
  org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)",
- org.eclipse.jgit.annotations;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+ org.bouncycastle.util.io;version="[1.65.0,2.0.0)",
+ org.eclipse.jgit.annotations;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api.errors;version="[5.11.2,5.12.0)",
  org.slf4j;version="[1.7.0,2.0.0)"
-Export-Package: org.eclipse.jgit.gpg.bc.internal;version="5.10.1";
-  x-friends:="org.eclipse.jgit.gpg.bc.test"
+Export-Package: org.eclipse.jgit.gpg.bc;version="5.11.2",
+ org.eclipse.jgit.gpg.bc.internal;version="5.11.2";x-friends:="org.eclipse.jgit.gpg.bc.test",
+ org.eclipse.jgit.gpg.bc.internal.keys;version="5.11.2";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 96e71c7..dde429d 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.gpg.bc;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.gpg.bc;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.gpg.bc/about.html b/org.eclipse.jgit.gpg.bc/about.html
index f971af1..fc527d5 100644
--- a/org.eclipse.jgit.gpg.bc/about.html
+++ b/org.eclipse.jgit.gpg.bc/about.html
@@ -11,7 +11,7 @@
     margin: 0.25in 0.5in 0.25in 0.5in;
     tab-interval: 0.5in;
     }
-  p {  	
+  p {
     margin-left: auto;
     margin-top:  0.5em;
     margin-bottom: 0.5em;
@@ -36,60 +36,53 @@
 <p>Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. </p>
 
 <p>All rights reserved.</p>
-<p>Redistribution and use in source and binary forms, with or without modification, 
+<p>Redistribution and use in source and binary forms, with or without modification,
 	are permitted provided that the following conditions are met:
-<ul><li>Redistributions of source code must retain the above copyright notice, 
+<ul><li>Redistributions of source code must retain the above copyright notice,
 	this list of conditions and the following disclaimer. </li>
-<li>Redistributions in binary form must reproduce the above copyright notice, 
-	this list of conditions and the following disclaimer in the documentation 
+<li>Redistributions in binary form must reproduce the above copyright notice,
+	this list of conditions and the following disclaimer in the documentation
 	and/or other materials provided with the distribution. </li>
-<li>Neither the name of the Eclipse Foundation, Inc. nor the names of its 
-	contributors may be used to endorse or promote products derived from 
+<li>Neither the name of the Eclipse Foundation, Inc. nor the names of its
+	contributors may be used to endorse or promote products derived from
 	this software without specific prior written permission. </li></ul>
 </p>
-<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
-IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
-INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
-NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
-PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
-WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
+<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.</p>
 
 <hr>
-<p><b>SHA-1 UbcCheck - MIT</b></p>
+<p><b>org.eclipse.jgit.gpg.bc.internal.keys.SExprParser - MIT</b></p>
 
-<p>Copyright (c) 2017:</p>
-<div class="ubc-name">
-Marc Stevens
-Cryptology Group
-Centrum Wiskunde & Informatica
-P.O. Box 94079, 1090 GB Amsterdam, Netherlands
-marc@marc-stevens.nl
-</div>
-<div class="ubc-name">
-Dan Shumow
-Microsoft Research
-danshu@microsoft.com
-</div>
-<p>Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+<p>Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc.
+(<a href="https://www.bouncycastle.org">https://www.bouncycastle.org</a>)</p>
+
+<p>
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+and associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
 </p>
-<ul><li>The above copyright notice and this permission notice shall be included
-in all copies or substantial portions of the Software.</li></ul>
-<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.</p>
+<p>
+The above copyright notice and this permission notice shall be included in all copies or substantial
+portions of the Software.
+</p>
+<p>
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+</p>
 
 </body>
 
diff --git a/org.eclipse.jgit.gpg.bc/pom.xml b/org.eclipse.jgit.gpg.bc/pom.xml
index ff4eeb4..c8d4ac5 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.gpg.bc</artifactId>
diff --git a/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory
new file mode 100644
index 0000000..17ab30f
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory
@@ -0,0 +1 @@
+org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSignatureVerifierFactory
\ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
index 1441c63..e4b1bab 100644
--- a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
+++ b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
@@ -1,11 +1,36 @@
+corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0}
 credentialPassphrase=Passphrase
-gpgFailedToParseSecretKey=Failed to parse secret key file in directory: {0}. Is the entered passphrase correct?
+cryptCipherError=Cannot create cipher to decrypt: {0}
+cryptWrongDecryptedLength=Decrypted key has wrong length; expected {0} bytes, got only {1} bytes
+gpgFailedToParseSecretKey=Failed to parse secret key file {0}. Is the entered passphrase correct?
 gpgNoCredentialsProvider=missing credentials provider
+gpgNoKeygrip=Cannot find key {0}: cannot determine key grip
 gpgNoKeyring=neither pubring.kbx nor secring.gpg files found
 gpgNoKeyInLegacySecring=no matching secret key found in legacy secring.gpg for key or user id: {0}
 gpgNoPublicKeyFound=Unable to find a public-key with key or user id: {0}
 gpgNoSecretKeyForPublicKey=unable to find associated secret key for public key: {0}
+gpgNoSuchAlgorithm=Cannot decrypt encrypted secret key: encryption algorithm {0} is not available
 gpgNotASigningKey=Secret key ({0}) is not suitable for signing
 gpgKeyInfo=GPG Key (fingerprint {0})
 gpgSigningCancelled=Signing was cancelled
+nonSignatureError=Signature does not decode into a signature object
+secretKeyTooShort=Secret key file corrupt; only {0} bytes read
+sexprHexNotClosed=Hex number in s-expression not closed
+sexprHexOdd=Hex number in s-expression has an odd number of digits
+sexprStringInvalidEscape=Invalid escape {0} in s-expression
+sexprStringInvalidEscapeAtEnd=Invalid s-expression: quoted string ends with escape character
+sexprStringInvalidHexEscape=Invalid hex escape in s-expression
+sexprStringInvalidOctalEscape=Invalid octal escape in s-expression
+sexprStringNotClosed=String in s-expression not closed
+sexprUnhandled=Unhandled token {0} in s-expression
+signatureInconsistent=Inconsistent signature; key ID {0} does not match issuer fingerprint {1}
+signatureKeyLookupError=Error occurred while looking for public key
+signatureNoKeyInfo=No way to determine a public key from the signature
+signatureNoPublicKey=No public key found to verify the signature
+signatureParseError=Signature cannot be parsed
+signatureVerificationError=Signature verification failed
 unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available.
+uncompressed25519Key=Cannot handle ed25519 public key with uncompressed data: {0}
+unknownCurve=Unknown curve {0}
+unknownCurveParameters=Curve {0} does not have a prime field
+unknownKeyType=Unknown key type {0}
\ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java
new file mode 100644
index 0000000..fdd1a2b
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc;
+
+import org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSigner;
+import org.eclipse.jgit.lib.GpgSigner;
+
+/**
+ * Factory for creating a {@link GpgSigner} based on Bouncy Castle.
+ *
+ * @since 5.11
+ */
+public final class BouncyCastleGpgSignerFactory {
+
+	private BouncyCastleGpgSignerFactory() {
+		// No instantiation
+	}
+
+	/**
+	 * Creates a new {@link GpgSigner}.
+	 *
+	 * @return the {@link GpgSigner}
+	 */
+	public static GpgSigner create() {
+		return new BouncyCastleGpgSigner();
+	}
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
index 1a00b0f..aedf8a5 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
@@ -1,3 +1,12 @@
+/*
+ * Copyright (C) 2018, 2021 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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
 package org.eclipse.jgit.gpg.bc.internal;
 
 import org.eclipse.jgit.nls.NLS;
@@ -18,16 +27,41 @@
 	}
 
 	// @formatter:off
+	/***/ public String corrupt25519Key;
 	/***/ public String credentialPassphrase;
+	/***/ public String cryptCipherError;
+	/***/ public String cryptWrongDecryptedLength;
 	/***/ public String gpgFailedToParseSecretKey;
 	/***/ public String gpgNoCredentialsProvider;
+	/***/ public String gpgNoKeygrip;
 	/***/ public String gpgNoKeyring;
 	/***/ public String gpgNoKeyInLegacySecring;
 	/***/ public String gpgNoPublicKeyFound;
 	/***/ public String gpgNoSecretKeyForPublicKey;
+	/***/ public String gpgNoSuchAlgorithm;
 	/***/ public String gpgNotASigningKey;
 	/***/ public String gpgKeyInfo;
 	/***/ public String gpgSigningCancelled;
+	/***/ public String nonSignatureError;
+	/***/ public String secretKeyTooShort;
+	/***/ public String sexprHexNotClosed;
+	/***/ public String sexprHexOdd;
+	/***/ public String sexprStringInvalidEscape;
+	/***/ public String sexprStringInvalidEscapeAtEnd;
+	/***/ public String sexprStringInvalidHexEscape;
+	/***/ public String sexprStringInvalidOctalEscape;
+	/***/ public String sexprStringNotClosed;
+	/***/ public String sexprUnhandled;
+	/***/ public String signatureInconsistent;
+	/***/ public String signatureKeyLookupError;
+	/***/ public String signatureNoKeyInfo;
+	/***/ public String signatureNoPublicKey;
+	/***/ public String signatureParseError;
+	/***/ public String signatureVerificationError;
 	/***/ public String unableToSignCommitNoSecretKey;
+	/***/ public String uncompressed25519Key;
+	/***/ public String unknownCurve;
+	/***/ public String unknownCurveParameters;
+	/***/ public String unknownKeyType;
 
 }
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
index eca4507..cf4d3d2 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2020 Salesforce and others
+ * Copyright (C) 2018, 2021 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
@@ -14,25 +14,22 @@
 
 import java.io.BufferedInputStream;
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URISyntaxException;
 import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
 import java.nio.file.InvalidPathException;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
 import java.text.MessageFormat;
-import java.util.ArrayList;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Locale;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
-import org.bouncycastle.gpg.SExprParser;
 import org.bouncycastle.gpg.keybox.BlobType;
 import org.bouncycastle.gpg.keybox.KeyBlob;
 import org.bouncycastle.gpg.keybox.KeyBox;
@@ -50,15 +47,15 @@
 import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
 import org.bouncycastle.openpgp.PGPSignature;
 import org.bouncycastle.openpgp.PGPUtil;
-import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
 import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
 import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
 import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
-import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
 import org.bouncycastle.util.encoders.Hex;
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.gpg.bc.internal.keys.KeyGrip;
+import org.eclipse.jgit.gpg.bc.internal.keys.SecretKeys;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.SystemReader;
@@ -78,17 +75,10 @@
 
 	}
 
-	/** Thrown if we try to read an encrypted private key without password. */
-	private static class EncryptedPgpKeyException extends RuntimeException {
-
-		private static final long serialVersionUID = 1L;
-
-	}
-
 	private static final Logger log = LoggerFactory
 			.getLogger(BouncyCastleGpgKeyLocator.class);
 
-	private static final Path GPG_DIRECTORY = findGpgDirectory();
+	static final Path GPG_DIRECTORY = findGpgDirectory();
 
 	private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
 			.resolve("pubring.kbx"); //$NON-NLS-1$
@@ -155,16 +145,13 @@
 
 	private PGPSecretKey attemptParseSecretKey(Path keyFile,
 			PGPDigestCalculatorProvider calculatorProvider,
-			PBEProtectionRemoverFactory passphraseProvider,
-			PGPPublicKey publicKey) {
+			SecretKeys.PassphraseSupplier passphraseSupplier,
+			PGPPublicKey publicKey)
+			throws IOException, PGPException, CanceledException,
+			UnsupportedCredentialItem, URISyntaxException {
 		try (InputStream in = newInputStream(keyFile)) {
-			return new SExprParser(calculatorProvider).parseSecretKey(
-					new BufferedInputStream(in), passphraseProvider, publicKey);
-		} catch (IOException | PGPException | ClassCastException e) {
-			if (log.isDebugEnabled())
-				log.debug("Ignoring unreadable file '{}': {}", keyFile, //$NON-NLS-1$
-						e.getMessage(), e);
-			return null;
+			return SecretKeys.readSecretKey(in, calculatorProvider,
+					passphraseSupplier, publicKey);
 		}
 	}
 
@@ -219,33 +206,60 @@
 			int stop = toMatch.indexOf('>');
 			return begin >= 0 && end > begin + 1 && stop > 0
 					&& userId.substring(begin + 1, end)
-							.equals(toMatch.substring(0, stop));
+							.equalsIgnoreCase(toMatch.substring(0, stop));
 		}
 		case '@': {
 			int begin = userId.indexOf('<');
 			int end = userId.indexOf('>', begin + 1);
 			return begin >= 0 && end > begin + 1
-					&& userId.substring(begin + 1, end).contains(toMatch);
+					&& containsIgnoreCase(userId.substring(begin + 1, end),
+							toMatch);
 		}
 		default:
 			if (toMatch.trim().isEmpty()) {
 				return false;
 			}
-			return userId.toLowerCase(Locale.ROOT)
-					.contains(toMatch.toLowerCase(Locale.ROOT));
+			return containsIgnoreCase(userId, toMatch);
 		}
 	}
 
-	private String toFingerprint(String keyId) {
+	private static boolean containsIgnoreCase(String a, String b) {
+		int alength = a.length();
+		int blength = b.length();
+		for (int i = 0; i + blength <= alength; i++) {
+			if (a.regionMatches(true, i, b, 0, blength)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private static String toFingerprint(String keyId) {
 		if (keyId.startsWith("0x")) { //$NON-NLS-1$
 			return keyId.substring(2);
 		}
 		return keyId;
 	}
 
-	private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob)
+	static PGPPublicKey findPublicKey(String fingerprint, String keySpec)
+			throws IOException, PGPException {
+		PGPPublicKey result = findPublicKeyInPubring(USER_PGP_PUBRING_FILE,
+				fingerprint, keySpec);
+		if (result == null && exists(USER_KEYBOX_PATH)) {
+			try {
+				result = findPublicKeyInKeyBox(USER_KEYBOX_PATH, fingerprint,
+						keySpec);
+			} catch (NoSuchAlgorithmException | NoSuchProviderException
+					| IOException | NoOpenPgpKeyException e) {
+				log.error(e.getMessage(), e);
+			}
+		}
+		return result;
+	}
+
+	private static PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob,
+			String keyId)
 			throws IOException {
-		String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT);
 		if (keyId.isEmpty()) {
 			return null;
 		}
@@ -259,10 +273,11 @@
 		return null;
 	}
 
-	private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob)
+	private static PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob,
+			String keySpec)
 			throws IOException {
 		for (UserID userID : keyBlob.getUserIds()) {
-			if (containsSigningKey(userID.getUserIDAsString(), signingKey)) {
+			if (containsSigningKey(userID.getUserIDAsString(), keySpec)) {
 				return getSigningPublicKey(keyBlob);
 			}
 		}
@@ -274,6 +289,10 @@
 	 *
 	 * @param keyboxFile
 	 *            the KeyBox file
+	 * @param keyId
+	 *            to look for, may be null
+	 * @param keySpec
+	 *            to look for
 	 * @return publicKey the public key (maybe <code>null</code>)
 	 * @throws IOException
 	 *             in case of problems reading the file
@@ -282,19 +301,22 @@
 	 * @throws NoOpenPgpKeyException
 	 *             if the file does not contain any OpenPGP key
 	 */
-	private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
+	private static PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile,
+			String keyId, String keySpec)
 			throws IOException, NoSuchAlgorithmException,
 			NoSuchProviderException, NoOpenPgpKeyException {
 		KeyBox keyBox = readKeyBoxFile(keyboxFile);
+		String id = keyId != null ? keyId
+				: toFingerprint(keySpec).toLowerCase(Locale.ROOT);
 		boolean hasOpenPgpKey = false;
 		for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
 			if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
 				hasOpenPgpKey = true;
-				PGPPublicKey key = findPublicKeyByKeyId(keyBlob);
+				PGPPublicKey key = findPublicKeyByKeyId(keyBlob, id);
 				if (key != null) {
 					return key;
 				}
-				key = findPublicKeyByUserId(keyBlob);
+				key = findPublicKeyByUserId(keyBlob, keySpec);
 				if (key != null) {
 					return key;
 				}
@@ -338,7 +360,8 @@
 			// pubring.gpg also try secring.gpg to find the secret key.
 			if (exists(USER_KEYBOX_PATH)) {
 				try {
-					publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH);
+					publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH, null,
+							signingKey);
 					if (publicKey != null) {
 						key = findSecretKeyForKeyBoxPublicKey(publicKey,
 								USER_KEYBOX_PATH);
@@ -361,7 +384,8 @@
 				}
 			}
 			if (exists(USER_PGP_PUBRING_FILE)) {
-				publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE);
+				publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE, null,
+						signingKey);
 				if (publicKey != null) {
 					// GPG < 2.1 may have both; the agent using the directory
 					// and gpg using secring.gpg. GPG >= 2.1 delegates all
@@ -433,67 +457,59 @@
 			PGPPublicKey publicKey, Path userKeyboxPath)
 			throws PGPException, CanceledException, UnsupportedCredentialItem,
 			URISyntaxException {
-		/*
-		 * this is somewhat brute-force but there doesn't seem to be another
-		 * way; we have to walk all private key files we find and try to open
-		 * them
-		 */
-
-		PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
-				.build();
-
-		try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) {
-			List<Path> allPaths = keyFiles.filter(Files::isRegularFile)
-					.collect(Collectors.toCollection(ArrayList::new));
-			if (allPaths.isEmpty()) {
-				return null;
+		byte[] keyGrip = null;
+		try {
+			keyGrip = KeyGrip.getKeyGrip(publicKey);
+		} catch (PGPException e) {
+			throw new PGPException(
+					MessageFormat.format(BCText.get().gpgNoKeygrip,
+							Hex.toHexString(publicKey.getFingerprint())),
+					e);
+		}
+		String filename = Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT)
+				+ ".key"; //$NON-NLS-1$
+		Path keyFile = USER_SECRET_KEY_DIR.resolve(filename);
+		if (!Files.exists(keyFile)) {
+			return null;
+		}
+		boolean clearPrompt = false;
+		try {
+			PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
+					.build();
+			clearPrompt = true;
+			PGPSecretKey secretKey = null;
+			try {
+				secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
+						() -> passphrasePrompt.getPassphrase(
+								publicKey.getFingerprint(), userKeyboxPath),
+						publicKey);
+			} catch (PGPException e) {
+				throw new PGPException(MessageFormat.format(
+						BCText.get().gpgFailedToParseSecretKey,
+						keyFile.toAbsolutePath()), e);
 			}
-			PBEProtectionRemoverFactory passphraseProvider = p -> {
-				throw new EncryptedPgpKeyException();
-			};
-			for (int attempts = 0; attempts < 2; attempts++) {
-				// Second pass will traverse only the encrypted keys with a real
-				// passphrase provider.
-				Iterator<Path> pathIterator = allPaths.iterator();
-				while (pathIterator.hasNext()) {
-					Path keyFile = pathIterator.next();
-					try {
-						PGPSecretKey secretKey = attemptParseSecretKey(keyFile,
-								calculatorProvider, passphraseProvider,
-								publicKey);
-						pathIterator.remove();
-						if (secretKey != null) {
-							if (!secretKey.isSigningKey()) {
-								throw new PGPException(MessageFormat.format(
-										BCText.get().gpgNotASigningKey,
-										signingKey));
-							}
-							return new BouncyCastleGpgKey(secretKey,
-									userKeyboxPath);
-						}
-					} catch (EncryptedPgpKeyException e) {
-						// Ignore; we'll try again.
-					}
+			if (secretKey != null) {
+				if (!secretKey.isSigningKey()) {
+					throw new PGPException(MessageFormat.format(
+							BCText.get().gpgNotASigningKey, signingKey));
 				}
-				if (attempts > 0 || allPaths.isEmpty()) {
-					break;
-				}
-				// allPaths contains only the encrypted keys now.
-				passphraseProvider = new JcePBEProtectionRemoverFactory(
-						passphrasePrompt.getPassphrase(
-								publicKey.getFingerprint(), userKeyboxPath));
+				clearPrompt = false;
+				return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
 			}
-
-			passphrasePrompt.clear();
 			return null;
 		} catch (RuntimeException e) {
-			passphrasePrompt.clear();
 			throw e;
+		} catch (FileNotFoundException | NoSuchFileException e) {
+			clearPrompt = false;
+			return null;
 		} catch (IOException e) {
-			passphrasePrompt.clear();
 			throw new PGPException(MessageFormat.format(
 					BCText.get().gpgFailedToParseSecretKey,
-					USER_SECRET_KEY_DIR.toAbsolutePath()), e);
+					keyFile.toAbsolutePath()), e);
+		} finally {
+			if (clearPrompt) {
+				passphrasePrompt.clear();
+			}
 		}
 	}
 
@@ -551,6 +567,11 @@
 	 * Return the first public key matching the key id ({@link #signingKey}.
 	 *
 	 * @param pubringFile
+	 *            to search
+	 * @param keyId
+	 *            to look for, may be null
+	 * @param keySpec
+	 *            to look for
 	 *
 	 * @return the PGP public key, or {@code null} if none found
 	 * @throws IOException
@@ -558,14 +579,16 @@
 	 * @throws PGPException
 	 *             on BouncyCastle errors
 	 */
-	private PGPPublicKey findPublicKeyInPubring(Path pubringFile)
+	private static PGPPublicKey findPublicKeyInPubring(Path pubringFile,
+			String keyId, String keySpec)
 			throws IOException, PGPException {
 		try (InputStream in = newInputStream(pubringFile)) {
 			PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
 					new BufferedInputStream(in),
 					new JcaKeyFingerprintCalculator());
 
-			String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT);
+			String id = keyId != null ? keyId
+					: toFingerprint(keySpec).toLowerCase(Locale.ROOT);
 			Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
 			while (keyrings.hasNext()) {
 				PGPPublicKeyRing keyRing = keyrings.next();
@@ -575,30 +598,33 @@
 					// try key id
 					String fingerprint = Hex.toHexString(key.getFingerprint())
 							.toLowerCase(Locale.ROOT);
-					if (fingerprint.endsWith(keyId)) {
+					if (fingerprint.endsWith(id)) {
 						return key;
 					}
 					// try user id
 					Iterator<String> userIDs = key.getUserIDs();
 					while (userIDs.hasNext()) {
 						String userId = userIDs.next();
-						if (containsSigningKey(userId, signingKey)) {
+						if (containsSigningKey(userId, keySpec)) {
 							return key;
 						}
 					}
 				}
 			}
+		} catch (FileNotFoundException | NoSuchFileException e) {
+			// Ignore and return null
 		}
 		return null;
 	}
 
-	private PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
+	private static PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
 			throws IOException {
 		return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing()
 				.getPublicKey(fingerprint);
 	}
 
-	private PGPPublicKey getSigningPublicKey(KeyBlob blob) throws IOException {
+	private static PGPPublicKey getSigningPublicKey(KeyBlob blob)
+			throws IOException {
 		PGPPublicKey masterKey = null;
 		Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob)
 				.getPGPPublicKeyRing().getPublicKeys();
@@ -618,7 +644,7 @@
 		return masterKey;
 	}
 
-	private boolean isSigningKey(PGPPublicKey key) {
+	private static boolean isSigningKey(PGPPublicKey key) {
 		Iterator signatures = key.getSignatures();
 		while (signatures.hasNext()) {
 			PGPSignature sig = (PGPSignature) signatures.next();
@@ -630,7 +656,7 @@
 		return false;
 	}
 
-	private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
+	private static KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
 			NoSuchAlgorithmException, NoSuchProviderException,
 			NoOpenPgpKeyException {
 		if (keyboxFile.toFile().length() == 0) {
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
index e47f64f..6144195 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
@@ -17,8 +17,8 @@
 import org.bouncycastle.util.encoders.Hex;
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
-import org.eclipse.jgit.transport.CredentialItem.CharArrayType;
 import org.eclipse.jgit.transport.CredentialItem.InformationalMessage;
+import org.eclipse.jgit.transport.CredentialItem.Password;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.transport.URIish;
 
@@ -31,7 +31,7 @@
  */
 class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable {
 
-	private CharArrayType passphrase;
+	private Password passphrase;
 
 	private CredentialsProvider credentialsProvider;
 
@@ -78,8 +78,7 @@
 			throws PGPException, CanceledException, UnsupportedCredentialItem,
 			URISyntaxException {
 		if (passphrase == null) {
-			passphrase = new CharArrayType(BCText.get().credentialPassphrase,
-					true);
+			passphrase = new Password(BCText.get().credentialPassphrase);
 		}
 
 		if (credentialsProvider == null) {
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java
new file mode 100644
index 0000000..7161895
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Security;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Locale;
+
+import org.bouncycastle.bcpg.sig.IssuerFingerprint;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openpgp.PGPCompressedData;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
+import org.bouncycastle.util.encoders.Hex;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.util.LRUMap;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * A {@link GpgSignatureVerifier} to verify GPG signatures using BouncyCastle.
+ */
+public class BouncyCastleGpgSignatureVerifier implements GpgSignatureVerifier {
+
+	private static void registerBouncyCastleProviderIfNecessary() {
+		if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+			Security.addProvider(new BouncyCastleProvider());
+		}
+	}
+
+	/**
+	 * Creates a new instance and registers the BouncyCastle security provider
+	 * if needed.
+	 */
+	public BouncyCastleGpgSignatureVerifier() {
+		registerBouncyCastleProviderIfNecessary();
+	}
+
+	// To support more efficient signature verification of multiple objects we
+	// cache public keys once found in a LRU cache.
+
+	private static final Object NO_KEY = new Object();
+
+	private LRUMap<String, Object> byFingerprint = new LRUMap<>(16, 200);
+
+	private LRUMap<String, Object> bySigner = new LRUMap<>(16, 200);
+
+	@Override
+	public String getName() {
+		return "bc"; //$NON-NLS-1$
+	}
+
+	@Override
+	@Nullable
+	public SignatureVerification verifySignature(@NonNull RevObject object,
+			@NonNull GpgConfig config) throws IOException {
+		if (object instanceof RevCommit) {
+			RevCommit commit = (RevCommit) object;
+			byte[] signatureData = commit.getRawGpgSignature();
+			if (signatureData == null) {
+				return null;
+			}
+			byte[] raw = commit.getRawBuffer();
+			// Now remove the GPG signature
+			byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' };
+			int start = RawParseUtils.headerStart(header, raw, 0);
+			if (start < 0) {
+				return null;
+			}
+			int end = RawParseUtils.headerEnd(raw, start);
+			// start is at the beginning of the header's content
+			start -= header.length + 1;
+			// end is on the terminating LF; we need to skip that, too
+			if (end < raw.length) {
+				end++;
+			}
+			byte[] data = new byte[raw.length - (end - start)];
+			System.arraycopy(raw, 0, data, 0, start);
+			System.arraycopy(raw, end, data, start, raw.length - end);
+			return verify(data, signatureData);
+		} else if (object instanceof RevTag) {
+			RevTag tag = (RevTag) object;
+			byte[] signatureData = tag.getRawGpgSignature();
+			if (signatureData == null) {
+				return null;
+			}
+			byte[] raw = tag.getRawBuffer();
+			// The signature is just tacked onto the end of the message, which
+			// is last in the buffer.
+			byte[] data = Arrays.copyOfRange(raw, 0,
+					raw.length - signatureData.length);
+			return verify(data, signatureData);
+		}
+		return null;
+	}
+
+	static PGPSignature parseSignature(InputStream in)
+			throws IOException, PGPException {
+		try (InputStream sigIn = PGPUtil.getDecoderStream(in)) {
+			JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(sigIn);
+			Object obj = pgpFactory.nextObject();
+			if (obj instanceof PGPCompressedData) {
+				obj = new JcaPGPObjectFactory(
+						((PGPCompressedData) obj).getDataStream()).nextObject();
+			}
+			if (obj instanceof PGPSignatureList) {
+				return ((PGPSignatureList) obj).get(0);
+			}
+			return null;
+		}
+	}
+
+	@Override
+	public SignatureVerification verify(byte[] data, byte[] signatureData)
+			throws IOException {
+		PGPSignature signature = null;
+		String fingerprint = null;
+		String signer = null;
+		String keyId = null;
+		try (InputStream sigIn = new ByteArrayInputStream(signatureData)) {
+			signature = parseSignature(sigIn);
+			if (signature != null) {
+				// Try to figure out something to find the public key with.
+				if (signature.hasSubpackets()) {
+					PGPSignatureSubpacketVector packets = signature
+							.getHashedSubPackets();
+					IssuerFingerprint fingerprintPacket = packets
+							.getIssuerFingerprint();
+					if (fingerprintPacket != null) {
+						fingerprint = Hex
+								.toHexString(fingerprintPacket.getFingerprint())
+								.toLowerCase(Locale.ROOT);
+					}
+					signer = packets.getSignerUserID();
+					if (signer != null) {
+						signer = BouncyCastleGpgSigner.extractSignerId(signer);
+					}
+				}
+				keyId = Long.toUnsignedString(signature.getKeyID(), 16)
+						.toLowerCase(Locale.ROOT);
+			} else {
+				throw new JGitInternalException(BCText.get().nonSignatureError);
+			}
+		} catch (PGPException e) {
+			throw new JGitInternalException(BCText.get().signatureParseError,
+					e);
+		}
+		Date signatureCreatedAt = signature.getCreationTime();
+		if (fingerprint == null && signer == null && keyId == null) {
+			return new VerificationResult(signatureCreatedAt, null, null, null,
+					false, false, TrustLevel.UNKNOWN,
+					BCText.get().signatureNoKeyInfo);
+		}
+		if (fingerprint != null && keyId != null
+				&& !fingerprint.endsWith(keyId)) {
+			return new VerificationResult(signatureCreatedAt, signer, fingerprint,
+					null, false, false, TrustLevel.UNKNOWN,
+					MessageFormat.format(BCText.get().signatureInconsistent,
+							keyId, fingerprint));
+		}
+		if (fingerprint == null && keyId != null) {
+			fingerprint = keyId;
+		}
+		// Try to find the public key
+		String keySpec = '<' + signer + '>';
+		Object cached = null;
+		PGPPublicKey publicKey = null;
+		try {
+			cached = byFingerprint.get(fingerprint);
+			if (cached != null) {
+				if (cached instanceof PGPPublicKey) {
+					publicKey = (PGPPublicKey) cached;
+				}
+			} else if (!StringUtils.isEmptyOrNull(signer)) {
+				cached = bySigner.get(signer);
+				if (cached != null) {
+					if (cached instanceof PGPPublicKey) {
+						publicKey = (PGPPublicKey) cached;
+					}
+				}
+			}
+			if (cached == null) {
+				publicKey = BouncyCastleGpgKeyLocator.findPublicKey(fingerprint,
+						keySpec);
+			}
+		} catch (IOException | PGPException e) {
+			throw new JGitInternalException(
+					BCText.get().signatureKeyLookupError, e);
+		}
+		if (publicKey == null) {
+			if (cached == null) {
+				byFingerprint.put(fingerprint, NO_KEY);
+				byFingerprint.put(keyId, NO_KEY);
+				if (signer != null) {
+					bySigner.put(signer, NO_KEY);
+				}
+			}
+			return new VerificationResult(signatureCreatedAt, signer,
+					fingerprint, null, false, false, TrustLevel.UNKNOWN,
+					BCText.get().signatureNoPublicKey);
+		}
+		if (cached == null) {
+			byFingerprint.put(fingerprint, publicKey);
+			byFingerprint.put(keyId, publicKey);
+			if (signer != null) {
+				bySigner.put(signer, publicKey);
+			}
+		}
+		String user = null;
+		Iterator<String> userIds = publicKey.getUserIDs();
+		if (!StringUtils.isEmptyOrNull(signer)) {
+			while (userIds.hasNext()) {
+				String userId = userIds.next();
+				if (BouncyCastleGpgKeyLocator.containsSigningKey(userId,
+						keySpec)) {
+					user = userId;
+					break;
+				}
+			}
+		}
+		if (user == null) {
+			userIds = publicKey.getUserIDs();
+			if (userIds.hasNext()) {
+				user = userIds.next();
+			}
+		}
+		boolean expired = false;
+		long validFor = publicKey.getValidSeconds();
+		if (validFor > 0 && signatureCreatedAt != null) {
+			Instant expiredAt = publicKey.getCreationTime().toInstant()
+					.plusSeconds(validFor);
+			expired = expiredAt.isBefore(signatureCreatedAt.toInstant());
+		}
+		// Trust data is not defined in OpenPGP; the format is implementation
+		// specific. We don't use the GPG trustdb but simply the trust packet
+		// on the public key, if present. Even if present, it may or may not
+		// be set.
+		byte[] trustData = publicKey.getTrustData();
+		TrustLevel trust = parseGpgTrustPacket(trustData);
+		boolean verified = false;
+		try {
+			signature.init(
+					new JcaPGPContentVerifierBuilderProvider()
+							.setProvider(BouncyCastleProvider.PROVIDER_NAME),
+					publicKey);
+			signature.update(data);
+			verified = signature.verify();
+		} catch (PGPException e) {
+			throw new JGitInternalException(
+					BCText.get().signatureVerificationError, e);
+		}
+		return new VerificationResult(signatureCreatedAt, signer, fingerprint, user,
+				verified, expired, trust, null);
+	}
+
+	private TrustLevel parseGpgTrustPacket(byte[] packet) {
+		if (packet == null || packet.length < 6) {
+			// A GPG trust packet has at least 6 bytes.
+			return TrustLevel.UNKNOWN;
+		}
+		if (packet[2] != 'g' || packet[3] != 'p' || packet[4] != 'g') {
+			// Not a GPG trust packet
+			return TrustLevel.UNKNOWN;
+		}
+		int trust = packet[0] & 0x0F;
+		switch (trust) {
+		case 0: // No determined/set
+		case 1: // Trust expired; i.e., calculation outdated or key expired
+		case 2: // Undefined: not enough information to set
+			return TrustLevel.UNKNOWN;
+		case 3:
+			return TrustLevel.NEVER;
+		case 4:
+			return TrustLevel.MARGINAL;
+		case 5:
+			return TrustLevel.FULL;
+		case 6:
+			return TrustLevel.ULTIMATE;
+		default:
+			return TrustLevel.UNKNOWN;
+		}
+	}
+
+	@Override
+	public void clear() {
+		byFingerprint.clear();
+		bySigner.clear();
+	}
+
+	private static class VerificationResult implements SignatureVerification {
+
+		private final Date creationDate;
+
+		private final String signer;
+
+		private final String keyUser;
+
+		private final String fingerprint;
+
+		private final boolean verified;
+
+		private final boolean expired;
+
+		private final @NonNull TrustLevel trustLevel;
+
+		private final String message;
+
+		public VerificationResult(Date creationDate, String signer,
+				String fingerprint, String user, boolean verified,
+				boolean expired, @NonNull TrustLevel trust, String message) {
+			this.creationDate = creationDate;
+			this.signer = signer;
+			this.fingerprint = fingerprint;
+			this.keyUser = user;
+			this.verified = verified;
+			this.expired = expired;
+			this.trustLevel = trust;
+			this.message = message;
+		}
+
+		@Override
+		public Date getCreationDate() {
+			return creationDate;
+		}
+
+		@Override
+		public String getSigner() {
+			return signer;
+		}
+
+		@Override
+		public String getKeyUser() {
+			return keyUser;
+		}
+
+		@Override
+		public String getKeyFingerprint() {
+			return fingerprint;
+		}
+
+		@Override
+		public boolean isExpired() {
+			return expired;
+		}
+
+		@Override
+		public TrustLevel getTrustLevel() {
+			return trustLevel;
+		}
+
+		@Override
+		public String getMessage() {
+			return message;
+		}
+
+		@Override
+		public boolean getVerified() {
+			return verified;
+		}
+	}
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java
new file mode 100644
index 0000000..ae82b75
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal;
+
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
+
+/**
+ * A {@link GpgSignatureVerifierFactory} that creates
+ * {@link GpgSignatureVerifier} instances that verify GPG signatures using
+ * BouncyCastle and that do cache public keys.
+ */
+public final class BouncyCastleGpgSignatureVerifierFactory
+		extends GpgSignatureVerifierFactory {
+
+	@Override
+	public GpgSignatureVerifier getVerifier() {
+		return new BouncyCastleGpgSignatureVerifier();
+	}
+
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
index ea159c5..211bd7b 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2020, Salesforce and others
+ * Copyright (C) 2018, 2021, 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
@@ -34,18 +34,25 @@
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgObjectSigner;
 import org.eclipse.jgit.lib.GpgSignature;
 import org.eclipse.jgit.lib.GpgSigner;
+import org.eclipse.jgit.lib.ObjectBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.util.StringUtils;
 
 /**
- * GPG Signer using BouncyCastle library
+ * GPG Signer using the BouncyCastle library.
  */
-public class BouncyCastleGpgSigner extends GpgSigner {
+public class BouncyCastleGpgSigner extends GpgSigner
+		implements GpgObjectSigner {
 
 	private static void registerBouncyCastleProviderIfNecessary() {
 		if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
@@ -67,13 +74,32 @@
 	public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
 			PersonIdent committer, CredentialsProvider credentialsProvider)
 			throws CanceledException {
+		try {
+			return canLocateSigningKey(gpgSigningKey, committer,
+					credentialsProvider, null);
+		} catch (UnsupportedSigningFormatException e) {
+			// Cannot occur with a null config
+			return false;
+		}
+	}
+
+	@Override
+	public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
+			PersonIdent committer, CredentialsProvider credentialsProvider,
+			GpgConfig config)
+			throws CanceledException, UnsupportedSigningFormatException {
+		if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
+			throw new UnsupportedSigningFormatException(
+					JGitText.get().onlyOpenPgpSupportedForSigning);
+		}
 		try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
 				credentialsProvider)) {
 			BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
 					committer, passphrasePrompt);
 			return gpgKey != null;
-		} catch (PGPException | IOException | NoSuchAlgorithmException
-				| NoSuchProviderException | URISyntaxException e) {
+		} catch (CanceledException e) {
+			throw e;
+		} catch (Exception e) {
 			return false;
 		}
 	}
@@ -98,10 +124,28 @@
 	public void sign(@NonNull CommitBuilder commit,
 			@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
 			CredentialsProvider credentialsProvider) throws CanceledException {
+		try {
+			signObject(commit, gpgSigningKey, committer, credentialsProvider,
+					null);
+		} catch (UnsupportedSigningFormatException e) {
+			// Cannot occur with a null config
+		}
+	}
+
+	@Override
+	public void signObject(@NonNull ObjectBuilder object,
+			@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
+			CredentialsProvider credentialsProvider, GpgConfig config)
+			throws CanceledException, UnsupportedSigningFormatException {
+		if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
+			throw new UnsupportedSigningFormatException(
+					JGitText.get().onlyOpenPgpSupportedForSigning);
+		}
 		try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
 				credentialsProvider)) {
 			BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
-					committer, passphrasePrompt);
+					committer,
+						passphrasePrompt);
 			PGPSecretKey secretKey = gpgKey.getSecretKey();
 			if (secretKey == null) {
 				throw new JGitInternalException(
@@ -158,17 +202,17 @@
 			ByteArrayOutputStream buffer = new ByteArrayOutputStream();
 			try (BCPGOutputStream out = new BCPGOutputStream(
 					new ArmoredOutputStream(buffer))) {
-				signatureGenerator.update(commit.build());
+				signatureGenerator.update(object.build());
 				signatureGenerator.generate().encode(out);
 			}
-			commit.setGpgSignature(new GpgSignature(buffer.toByteArray()));
+			object.setGpgSignature(new GpgSignature(buffer.toByteArray()));
 		} catch (PGPException | IOException | NoSuchAlgorithmException
 				| NoSuchProviderException | URISyntaxException e) {
 			throw new JGitInternalException(e.getMessage(), e);
 		}
 	}
 
-	private String extractSignerId(String pgpUserId) {
+	static String extractSignerId(String pgpUserId) {
 		int from = pgpUserId.indexOf('<');
 		if (from >= 0) {
 			int to = pgpUserId.indexOf('>', from + 1);
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java
new file mode 100644
index 0000000..b1d4446
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Arrays;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.cryptlib.CryptlibObjectIdentifiers;
+import org.bouncycastle.asn1.x9.ECNamedCurveTable;
+import org.bouncycastle.asn1.x9.X9ECParameters;
+import org.bouncycastle.bcpg.DSAPublicBCPGKey;
+import org.bouncycastle.bcpg.ECPublicBCPGKey;
+import org.bouncycastle.bcpg.ElGamalPublicBCPGKey;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
+import org.bouncycastle.bcpg.RSAPublicBCPGKey;
+import org.bouncycastle.crypto.ec.CustomNamedCurves;
+import org.bouncycastle.math.ec.ECAlgorithms;
+import org.bouncycastle.math.field.FiniteField;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.util.encoders.Hex;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.gpg.bc.internal.BCText;
+import org.eclipse.jgit.util.sha1.SHA1;
+
+/**
+ * Utilities to compute the <em>keygrip</em> of a key. A keygrip is a SHA1 hash
+ * over the public key parameters and is used internally by the gpg-agent to
+ * find the secret key belonging to a public key: the secret key is stored in a
+ * file under ~/.gnupg/private-keys-v1.d/ with a name "&lt;keygrip>.key". While
+ * this storage organization is an implementation detail of GPG, the way
+ * keygrips are computed is not; they are computed by libgcrypt and their
+ * definition is stable.
+ */
+public final class KeyGrip {
+
+	// Some OIDs apparently unknown to BouncyCastle.
+
+	private static String OID_OPENPGP_ED25519 = "1.3.6.1.4.1.11591.15.1"; //$NON-NLS-1$
+
+	private static String OID_RFC8410_CURVE25519 = "1.3.101.110"; //$NON-NLS-1$
+
+	private static String OID_RFC8410_ED25519 = "1.3.101.112"; //$NON-NLS-1$
+
+	private KeyGrip() {
+		// No instantiation
+	}
+
+	/**
+	 * Computes the keygrip for a {@link PGPPublicKey}.
+	 *
+	 * @param publicKey
+	 *            to get the keygrip of
+	 * @return the keygrip
+	 * @throws PGPException
+	 *             if an unknown key type is encountered.
+	 */
+	@NonNull
+	public static byte[] getKeyGrip(PGPPublicKey publicKey)
+			throws PGPException {
+		SHA1 grip = SHA1.newInstance();
+		grip.setDetectCollision(false);
+
+		switch (publicKey.getAlgorithm()) {
+		case PublicKeyAlgorithmTags.RSA_GENERAL:
+		case PublicKeyAlgorithmTags.RSA_ENCRYPT:
+		case PublicKeyAlgorithmTags.RSA_SIGN:
+			BigInteger modulus = ((RSAPublicBCPGKey) publicKey
+					.getPublicKeyPacket().getKey()).getModulus();
+			hash(grip, modulus.toByteArray());
+			break;
+		case PublicKeyAlgorithmTags.DSA:
+			DSAPublicBCPGKey dsa = (DSAPublicBCPGKey) publicKey
+					.getPublicKeyPacket().getKey();
+			hash(grip, dsa.getP().toByteArray(), 'p', true);
+			hash(grip, dsa.getQ().toByteArray(), 'q', true);
+			hash(grip, dsa.getG().toByteArray(), 'g', true);
+			hash(grip, dsa.getY().toByteArray(), 'y', true);
+			break;
+		case PublicKeyAlgorithmTags.ELGAMAL_GENERAL:
+		case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT:
+			ElGamalPublicBCPGKey eg = (ElGamalPublicBCPGKey) publicKey
+					.getPublicKeyPacket().getKey();
+			hash(grip, eg.getP().toByteArray(), 'p', true);
+			hash(grip, eg.getG().toByteArray(), 'g', true);
+			hash(grip, eg.getY().toByteArray(), 'y', true);
+			break;
+		case PublicKeyAlgorithmTags.ECDH:
+		case PublicKeyAlgorithmTags.ECDSA:
+		case PublicKeyAlgorithmTags.EDDSA:
+			ECPublicBCPGKey ec = (ECPublicBCPGKey) publicKey
+					.getPublicKeyPacket().getKey();
+			ASN1ObjectIdentifier curveOID = ec.getCurveOID();
+			// BC doesn't know these OIDs.
+			if (OID_OPENPGP_ED25519.equals(curveOID.getId())
+					|| OID_RFC8410_ED25519.equals(curveOID.getId())) {
+				return hashEd25519(grip, ec.getEncodedPoint());
+			} else if (CryptlibObjectIdentifiers.curvey25519.equals(curveOID)
+					|| OID_RFC8410_CURVE25519.equals(curveOID.getId())) {
+				// curvey25519 actually is the OpenPGP OID for Curve25519 and is
+				// known to BC, but the parameters are for the short Weierstrass
+				// form. See https://github.com/bcgit/bc-java/issues/399 .
+				// libgcrypt uses Montgomery form.
+				return hashCurve25519(grip, ec.getEncodedPoint());
+			}
+			X9ECParameters params = getX9Parameters(curveOID);
+			if (params == null) {
+				throw new PGPException(MessageFormat
+						.format(BCText.get().unknownCurve, curveOID.getId()));
+			}
+			// Need to write p, a, b, g, n, q
+			BigInteger q = ec.getEncodedPoint();
+			byte[] g = params.getG().getEncoded(false);
+			BigInteger a = params.getCurve().getA().toBigInteger();
+			BigInteger b = params.getCurve().getB().toBigInteger();
+			BigInteger n = params.getN();
+			BigInteger p = null;
+			FiniteField field = params.getCurve().getField();
+			if (ECAlgorithms.isFpField(field)) {
+				p = field.getCharacteristic();
+			}
+			if (p == null) {
+				// Don't know...
+				throw new PGPException(MessageFormat.format(
+						BCText.get().unknownCurveParameters, curveOID.getId()));
+			}
+			hash(grip, p.toByteArray(), 'p', false);
+			hash(grip, a.toByteArray(), 'a', false);
+			hash(grip, b.toByteArray(), 'b', false);
+			hash(grip, g, 'g', false);
+			hash(grip, n.toByteArray(), 'n', false);
+			if (publicKey.getAlgorithm() == PublicKeyAlgorithmTags.EDDSA) {
+				hashQ25519(grip, q);
+			} else {
+				hash(grip, q.toByteArray(), 'q', false);
+			}
+			break;
+		default:
+			throw new PGPException(
+					MessageFormat.format(BCText.get().unknownKeyType,
+							Integer.toString(publicKey.getAlgorithm())));
+		}
+		return grip.digest();
+	}
+
+	private static void hash(SHA1 grip, byte[] data) {
+		// Need to skip leading zero bytes
+		int i = 0;
+		while (i < data.length && data[i] == 0) {
+			i++;
+		}
+		int length = data.length - i;
+		if (i < data.length) {
+			if ((data[i] & 0x80) != 0) {
+				grip.update((byte) 0);
+			}
+			grip.update(data, i, length);
+		}
+	}
+
+	private static void hash(SHA1 grip, byte[] data, char id, boolean zeroPad) {
+		// Need to skip leading zero bytes
+		int i = 0;
+		while (i < data.length && data[i] == 0) {
+			i++;
+		}
+		int length = data.length - i;
+		boolean addZero = false;
+		if (i < data.length && zeroPad && (data[i] & 0x80) != 0) {
+			addZero = true;
+		}
+		// libgcrypt includes an SExp in the hash
+		String prefix = "(1:" + id + (addZero ? length + 1 : length) + ':'; //$NON-NLS-1$
+		grip.update(prefix.getBytes(StandardCharsets.US_ASCII));
+		// For some items, gcrypt prepends a zero byte if the high bit is set
+		if (addZero) {
+			grip.update((byte) 0);
+		}
+		if (i < data.length) {
+			grip.update(data, i, length);
+		}
+		grip.update((byte) ')');
+	}
+
+	private static void hashQ25519(SHA1 grip, BigInteger q)
+			throws PGPException {
+		byte[] data = q.toByteArray();
+		switch (data[0]) {
+		case 0x04:
+			if (data.length != 65) {
+				throw new PGPException(MessageFormat.format(
+						BCText.get().corrupt25519Key, Hex.toHexString(data)));
+			}
+			// Uncompressed: should not occur with ed25519 or curve25519
+			throw new PGPException(MessageFormat.format(
+					BCText.get().uncompressed25519Key, Hex.toHexString(data)));
+		case 0x40:
+			if (data.length != 33) {
+				throw new PGPException(MessageFormat.format(
+						BCText.get().corrupt25519Key, Hex.toHexString(data)));
+			}
+			// Compressed; normal case. Skip prefix.
+			hash(grip, Arrays.copyOfRange(data, 1, data.length), 'q', false);
+			break;
+		default:
+			if (data.length != 32) {
+				throw new PGPException(MessageFormat.format(
+						BCText.get().corrupt25519Key, Hex.toHexString(data)));
+			}
+			// Compressed format without prefix. Should not occur?
+			hash(grip, data, 'q', false);
+			break;
+		}
+	}
+
+	/**
+	 * Computes the keygrip for an ed25519 public key.
+	 * <p>
+	 * Package-visible for tests only.
+	 * </p>
+	 *
+	 * @param grip
+	 *            initialized {@link SHA1}
+	 * @param q
+	 *            the public key's EC point
+	 * @return the keygrip
+	 * @throws PGPException
+	 *             if q indicates uncompressed format
+	 */
+	@SuppressWarnings("nls")
+	static byte[] hashEd25519(SHA1 grip, BigInteger q) throws PGPException {
+		// For the values, see RFC 7748: https://tools.ietf.org/html/rfc7748
+		// p = 2^255 - 19
+		hash(grip, Hex.decodeStrict(
+				"7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED"),
+				'p', false);
+		// Field: a = 1
+		hash(grip, new byte[] { 0x01 }, 'a', false);
+		// Field: b = 121665/121666 (mod p)
+		// See Berstein et.al., "Twisted Edwards Curves",
+		// https://doi.org/10.1007/978-3-540-68164-9_26
+		hash(grip, Hex.decodeStrict(
+				"2DFC9311D490018C7338BF8688861767FF8FF5B2BEBE27548A14B235ECA6874A"),
+				'b', false);
+		// Generator point with affine X,Y
+		// @formatter:off
+		// X(P) = 15112221349535400772501151409588531511454012693041857206046113283949847762202
+		// Y(P) = 46316835694926478169428394003475163141307993866256225615783033603165251855960
+		// the "04" signifies uncompressed format.
+		// @formatter:on
+		hash(grip, Hex.decodeStrict("04"
+				+ "216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A"
+				+ "6666666666666666666666666666666666666666666666666666666666666658"),
+				'g', false);
+		// order = 2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed
+		hash(grip, Hex.decodeStrict(
+				"1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED"),
+				'n', false);
+		hashQ25519(grip, q);
+		return grip.digest();
+	}
+
+	/**
+	 * Computes the keygrip for a curve25519 public key.
+	 * <p>
+	 * Package-visible for tests only.
+	 * </p>
+	 *
+	 * @param grip
+	 *            initialized {@link SHA1}
+	 * @param q
+	 *            the public key's EC point
+	 * @return the keygrip
+	 * @throws PGPException
+	 *             if q indicates uncompressed format
+	 */
+	@SuppressWarnings("nls")
+	static byte[] hashCurve25519(SHA1 grip, BigInteger q) throws PGPException {
+		hash(grip, Hex.decodeStrict(
+				"7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED"),
+				'p', false);
+		// Unclear: RFC 7748 says A = 486662. This value here is (A-2)/4 =
+		// 121665. Compare ecc-curves.c in libgcrypt:
+		// https://github.com/gpg/libgcrypt/blob/361a058/cipher/ecc-curves.c#L146
+		hash(grip, new byte[] { 0x01, (byte) 0xDB, 0x41 }, 'a', false);
+		hash(grip, new byte[] { 0x01 }, 'b', false);
+		// libgcrypt uses the old g.y value before the erratum to RFC 7748 for
+		// the keygrip. The new value would be
+		// 5F51E65E475F794B1FE122D388B72EB36DC2B28192839E4DD6163A5D81312C14. See
+		// https://www.rfc-editor.org/errata/eid4730 and
+		// https://github.com/gpg/libgcrypt/commit/f67b6492e0b0
+		hash(grip, Hex.decodeStrict("04"
+				+ "0000000000000000000000000000000000000000000000000000000000000009"
+				+ "20AE19A1B8A086B4E01EDD2C7748D14C923D4D7E6D7C61B229E9C5A27ECED3D9"),
+				'g', false);
+		hash(grip, Hex.decodeStrict(
+				"1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED"),
+				'n', false);
+		hashQ25519(grip, q);
+		return grip.digest();
+	}
+
+	private static X9ECParameters getX9Parameters(
+			ASN1ObjectIdentifier curveOID) {
+		X9ECParameters params = CustomNamedCurves.getByOID(curveOID);
+		if (params == null) {
+			params = ECNamedCurveTable.getByOID(curveOID);
+		}
+		return params;
+	}
+
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
new file mode 100644
index 0000000..68f8a45
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import java.security.NoSuchAlgorithmException;
+import java.text.MessageFormat;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
+import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.util.Arrays;
+import org.eclipse.jgit.gpg.bc.internal.BCText;
+
+/**
+ * A {@link PBEProtectionRemoverFactory} using AES/OCB/NoPadding for decryption.
+ * It accepts an AAD in the factory's constructor, so the factory can be used to
+ * create a {@link PBESecretKeyDecryptor} only for a particular input.
+ * <p>
+ * For JGit's needs, this is sufficient, but for a general upstream
+ * implementation that limitation might not be acceptable.
+ * </p>
+ */
+class OCBPBEProtectionRemoverFactory
+		implements PBEProtectionRemoverFactory {
+
+	private final PGPDigestCalculatorProvider calculatorProvider;
+
+	private final char[] passphrase;
+
+	private final byte[] aad;
+
+	/**
+	 * Creates a new factory instance with the given parameters.
+	 * <p>
+	 * Because the AAD is given at factory level, the {@link PBESecretKeyDecryptor}s
+	 * created by the factory can be used to decrypt only a particular input
+	 * matching this AAD.
+	 * </p>
+	 *
+	 * @param passphrase         to use for secret key derivation
+	 * @param calculatorProvider for computing digests
+	 * @param aad                for the OCB decryption
+	 */
+	OCBPBEProtectionRemoverFactory(char[] passphrase,
+			PGPDigestCalculatorProvider calculatorProvider, byte[] aad) {
+		this.calculatorProvider = calculatorProvider;
+		this.passphrase = passphrase;
+		this.aad = aad;
+	}
+
+	@Override
+	public PBESecretKeyDecryptor createDecryptor(String protection)
+			throws PGPException {
+		return new PBESecretKeyDecryptor(passphrase, calculatorProvider) {
+
+			@Override
+			public byte[] recoverKeyData(int encAlgorithm, byte[] key,
+					byte[] iv, byte[] encrypted, int encryptedOffset,
+					int encryptedLength) throws PGPException {
+				String algorithmName = PGPUtil
+						.getSymmetricCipherName(encAlgorithm);
+				byte[] decrypted = null;
+				try {
+					Cipher c = Cipher
+							.getInstance(algorithmName + "/OCB/NoPadding"); //$NON-NLS-1$
+					SecretKey secretKey = new SecretKeySpec(key, algorithmName);
+					c.init(Cipher.DECRYPT_MODE, secretKey,
+							new IvParameterSpec(iv));
+					c.updateAAD(aad);
+					decrypted = new byte[c.getOutputSize(encryptedLength)];
+					int decryptedLength = c.update(encrypted, encryptedOffset,
+							encryptedLength, decrypted);
+					// doFinal() for OCB will check the MAC and throw an
+					// exception if it doesn't match
+					decryptedLength += c.doFinal(decrypted, decryptedLength);
+					if (decryptedLength != decrypted.length) {
+						throw new PGPException(MessageFormat.format(
+								BCText.get().cryptWrongDecryptedLength,
+								Integer.valueOf(decryptedLength),
+								Integer.valueOf(decrypted.length)));
+					}
+					byte[] result = decrypted;
+					decrypted = null; // Don't clear in finally
+					return result;
+				} catch (NoClassDefFoundError e) {
+					String msg = MessageFormat.format(
+							BCText.get().gpgNoSuchAlgorithm,
+							algorithmName + "/OCB"); //$NON-NLS-1$
+					throw new PGPException(msg,
+							new NoSuchAlgorithmException(msg, e));
+				} catch (PGPException e) {
+					throw e;
+				} catch (Exception e) {
+					throw new PGPException(
+							MessageFormat.format(BCText.get().cryptCipherError,
+									e.getLocalizedMessage()),
+							e);
+				} finally {
+					if (decrypted != null) {
+						// Prevent halfway decrypted data leaking.
+						Arrays.fill(decrypted, (byte) 0);
+					}
+				}
+			}
+		};
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
new file mode 100644
index 0000000..a9bb22c
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
@@ -0,0 +1,826 @@
+/*
+ * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
+ * <p>
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
+ *including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ * </p>
+ * <p>
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ * </p>
+ * <p>
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </p>
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.util.Date;
+
+import org.bouncycastle.asn1.x9.ECNamedCurveTable;
+import org.bouncycastle.bcpg.DSAPublicBCPGKey;
+import org.bouncycastle.bcpg.DSASecretBCPGKey;
+import org.bouncycastle.bcpg.ECDSAPublicBCPGKey;
+import org.bouncycastle.bcpg.ECPublicBCPGKey;
+import org.bouncycastle.bcpg.ECSecretBCPGKey;
+import org.bouncycastle.bcpg.ElGamalPublicBCPGKey;
+import org.bouncycastle.bcpg.ElGamalSecretBCPGKey;
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
+import org.bouncycastle.bcpg.PublicKeyPacket;
+import org.bouncycastle.bcpg.RSAPublicBCPGKey;
+import org.bouncycastle.bcpg.RSASecretBCPGKey;
+import org.bouncycastle.bcpg.S2K;
+import org.bouncycastle.bcpg.SecretKeyPacket;
+import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
+import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
+import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculator;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.util.Arrays;
+import org.bouncycastle.util.Strings;
+
+/**
+ * A parser for secret keys stored in s-expressions. Original BouncyCastle code
+ * modified by the JGit team to:
+ * <ul>
+ * <li>handle unencrypted DSA, EC, and ElGamal keys (upstream only handles
+ * unencrypted RSA), and</li>
+ * <li>handle secret keys using AES/OCB as encryption (those don't have a
+ * hash).</li>
+ * </ul>
+ */
+@SuppressWarnings("nls")
+public class SExprParser {
+	private final PGPDigestCalculatorProvider digestProvider;
+
+	/**
+	 * Base constructor.
+	 *
+	 * @param digestProvider
+	 *            a provider for digest calculations. Used to confirm key
+	 *            protection hashes.
+	 */
+	public SExprParser(PGPDigestCalculatorProvider digestProvider) {
+		this.digestProvider = digestProvider;
+	}
+
+	/**
+	 * Parse a secret key from one of the GPG S expression keys associating it
+	 * with the passed in public key.
+	 *
+	 * @param inputStream
+	 *            to read from
+	 * @param keyProtectionRemoverFactory
+	 *            for decrypting encrypted keys
+	 * @param pubKey
+	 *            the private key should belong to
+	 *
+	 * @return a secret key object.
+	 * @throws IOException
+	 * @throws PGPException
+	 */
+	public PGPSecretKey parseSecretKey(InputStream inputStream,
+			PBEProtectionRemoverFactory keyProtectionRemoverFactory,
+			PGPPublicKey pubKey) throws IOException, PGPException {
+		SXprUtils.skipOpenParenthesis(inputStream);
+
+		String type;
+
+		type = SXprUtils.readString(inputStream, inputStream.read());
+		if (type.equals("protected-private-key")
+				|| type.equals("private-key")) {
+			SXprUtils.skipOpenParenthesis(inputStream);
+
+			String keyType = SXprUtils.readString(inputStream,
+					inputStream.read());
+			if (keyType.equals("ecc")) {
+				SXprUtils.skipOpenParenthesis(inputStream);
+
+				String curveID = SXprUtils.readString(inputStream,
+						inputStream.read());
+				String curveName = SXprUtils.readString(inputStream,
+						inputStream.read());
+
+				SXprUtils.skipCloseParenthesis(inputStream);
+
+				byte[] qVal;
+
+				SXprUtils.skipOpenParenthesis(inputStream);
+
+				type = SXprUtils.readString(inputStream, inputStream.read());
+				if (type.equals("q")) {
+					qVal = SXprUtils.readBytes(inputStream, inputStream.read());
+				} else {
+					throw new PGPException("no q value found");
+				}
+
+				SXprUtils.skipCloseParenthesis(inputStream);
+
+				BigInteger d = processECSecretKey(inputStream, curveID,
+						curveName, qVal, keyProtectionRemoverFactory);
+
+				if (curveName.startsWith("NIST ")) {
+					curveName = curveName.substring("NIST ".length());
+				}
+
+				ECPublicBCPGKey basePubKey = new ECDSAPublicBCPGKey(
+						ECNamedCurveTable.getOID(curveName),
+						new BigInteger(1, qVal));
+				ECPublicBCPGKey assocPubKey = (ECPublicBCPGKey) pubKey
+						.getPublicKeyPacket().getKey();
+				if (!basePubKey.getCurveOID().equals(assocPubKey.getCurveOID())
+						|| !basePubKey.getEncodedPoint()
+								.equals(assocPubKey.getEncodedPoint())) {
+					throw new PGPException(
+							"passed in public key does not match secret key");
+				}
+
+				return new PGPSecretKey(
+						new SecretKeyPacket(pubKey.getPublicKeyPacket(),
+								SymmetricKeyAlgorithmTags.NULL, null, null,
+								new ECSecretBCPGKey(d).getEncoded()),
+						pubKey);
+			} else if (keyType.equals("dsa")) {
+				BigInteger p = readBigInteger("p", inputStream);
+				BigInteger q = readBigInteger("q", inputStream);
+				BigInteger g = readBigInteger("g", inputStream);
+
+				BigInteger y = readBigInteger("y", inputStream);
+
+				BigInteger x = processDSASecretKey(inputStream, p, q, g, y,
+						keyProtectionRemoverFactory);
+
+				DSAPublicBCPGKey basePubKey = new DSAPublicBCPGKey(p, q, g, y);
+				DSAPublicBCPGKey assocPubKey = (DSAPublicBCPGKey) pubKey
+						.getPublicKeyPacket().getKey();
+				if (!basePubKey.getP().equals(assocPubKey.getP())
+						|| !basePubKey.getQ().equals(assocPubKey.getQ())
+						|| !basePubKey.getG().equals(assocPubKey.getG())
+						|| !basePubKey.getY().equals(assocPubKey.getY())) {
+					throw new PGPException(
+							"passed in public key does not match secret key");
+				}
+				return new PGPSecretKey(
+						new SecretKeyPacket(pubKey.getPublicKeyPacket(),
+								SymmetricKeyAlgorithmTags.NULL, null, null,
+								new DSASecretBCPGKey(x).getEncoded()),
+						pubKey);
+			} else if (keyType.equals("elg")) {
+				BigInteger p = readBigInteger("p", inputStream);
+				BigInteger g = readBigInteger("g", inputStream);
+
+				BigInteger y = readBigInteger("y", inputStream);
+
+				BigInteger x = processElGamalSecretKey(inputStream, p, g, y,
+						keyProtectionRemoverFactory);
+
+				ElGamalPublicBCPGKey basePubKey = new ElGamalPublicBCPGKey(p, g,
+						y);
+				ElGamalPublicBCPGKey assocPubKey = (ElGamalPublicBCPGKey) pubKey
+						.getPublicKeyPacket().getKey();
+				if (!basePubKey.getP().equals(assocPubKey.getP())
+						|| !basePubKey.getG().equals(assocPubKey.getG())
+						|| !basePubKey.getY().equals(assocPubKey.getY())) {
+					throw new PGPException(
+							"passed in public key does not match secret key");
+				}
+
+				return new PGPSecretKey(
+						new SecretKeyPacket(pubKey.getPublicKeyPacket(),
+								SymmetricKeyAlgorithmTags.NULL, null, null,
+								new ElGamalSecretBCPGKey(x).getEncoded()),
+						pubKey);
+			} else if (keyType.equals("rsa")) {
+				BigInteger n = readBigInteger("n", inputStream);
+				BigInteger e = readBigInteger("e", inputStream);
+
+				BigInteger[] values = processRSASecretKey(inputStream, n, e,
+						keyProtectionRemoverFactory);
+
+				// TODO: type of RSA key?
+				RSAPublicBCPGKey basePubKey = new RSAPublicBCPGKey(n, e);
+				RSAPublicBCPGKey assocPubKey = (RSAPublicBCPGKey) pubKey
+						.getPublicKeyPacket().getKey();
+				if (!basePubKey.getModulus().equals(assocPubKey.getModulus())
+						|| !basePubKey.getPublicExponent()
+								.equals(assocPubKey.getPublicExponent())) {
+					throw new PGPException(
+							"passed in public key does not match secret key");
+				}
+
+				return new PGPSecretKey(new SecretKeyPacket(
+						pubKey.getPublicKeyPacket(),
+						SymmetricKeyAlgorithmTags.NULL, null, null,
+						new RSASecretBCPGKey(values[0], values[1], values[2])
+								.getEncoded()),
+						pubKey);
+			} else {
+				throw new PGPException("unknown key type: " + keyType);
+			}
+		}
+
+		throw new PGPException("unknown key type found");
+	}
+
+	/**
+	 * Parse a secret key from one of the GPG S expression keys.
+	 *
+	 * @param inputStream
+	 *            to read from
+	 * @param keyProtectionRemoverFactory
+	 *            for decrypting encrypted keys
+	 * @param fingerPrintCalculator
+	 *            for calculating key fingerprints
+	 *
+	 * @return a secret key object.
+	 * @throws IOException
+	 * @throws PGPException
+	 */
+	public PGPSecretKey parseSecretKey(InputStream inputStream,
+			PBEProtectionRemoverFactory keyProtectionRemoverFactory,
+			KeyFingerPrintCalculator fingerPrintCalculator)
+			throws IOException, PGPException {
+		SXprUtils.skipOpenParenthesis(inputStream);
+
+		String type;
+
+		type = SXprUtils.readString(inputStream, inputStream.read());
+		if (type.equals("protected-private-key")
+				|| type.equals("private-key")) {
+			SXprUtils.skipOpenParenthesis(inputStream);
+
+			String keyType = SXprUtils.readString(inputStream,
+					inputStream.read());
+			if (keyType.equals("ecc")) {
+				SXprUtils.skipOpenParenthesis(inputStream);
+
+				String curveID = SXprUtils.readString(inputStream,
+						inputStream.read());
+				String curveName = SXprUtils.readString(inputStream,
+						inputStream.read());
+
+				if (curveName.startsWith("NIST ")) {
+					curveName = curveName.substring("NIST ".length());
+				}
+
+				SXprUtils.skipCloseParenthesis(inputStream);
+
+				byte[] qVal;
+
+				SXprUtils.skipOpenParenthesis(inputStream);
+
+				type = SXprUtils.readString(inputStream, inputStream.read());
+				if (type.equals("q")) {
+					qVal = SXprUtils.readBytes(inputStream, inputStream.read());
+				} else {
+					throw new PGPException("no q value found");
+				}
+
+				PublicKeyPacket pubPacket = new PublicKeyPacket(
+						PublicKeyAlgorithmTags.ECDSA, new Date(),
+						new ECDSAPublicBCPGKey(
+								ECNamedCurveTable.getOID(curveName),
+								new BigInteger(1, qVal)));
+
+				SXprUtils.skipCloseParenthesis(inputStream);
+
+				BigInteger d = processECSecretKey(inputStream, curveID,
+						curveName, qVal, keyProtectionRemoverFactory);
+
+				return new PGPSecretKey(
+						new SecretKeyPacket(pubPacket,
+								SymmetricKeyAlgorithmTags.NULL, null, null,
+								new ECSecretBCPGKey(d).getEncoded()),
+						new PGPPublicKey(pubPacket, fingerPrintCalculator));
+			} else if (keyType.equals("dsa")) {
+				BigInteger p = readBigInteger("p", inputStream);
+				BigInteger q = readBigInteger("q", inputStream);
+				BigInteger g = readBigInteger("g", inputStream);
+
+				BigInteger y = readBigInteger("y", inputStream);
+
+				BigInteger x = processDSASecretKey(inputStream, p, q, g, y,
+						keyProtectionRemoverFactory);
+
+				PublicKeyPacket pubPacket = new PublicKeyPacket(
+						PublicKeyAlgorithmTags.DSA, new Date(),
+						new DSAPublicBCPGKey(p, q, g, y));
+
+				return new PGPSecretKey(
+						new SecretKeyPacket(pubPacket,
+								SymmetricKeyAlgorithmTags.NULL, null, null,
+								new DSASecretBCPGKey(x).getEncoded()),
+						new PGPPublicKey(pubPacket, fingerPrintCalculator));
+			} else if (keyType.equals("elg")) {
+				BigInteger p = readBigInteger("p", inputStream);
+				BigInteger g = readBigInteger("g", inputStream);
+
+				BigInteger y = readBigInteger("y", inputStream);
+
+				BigInteger x = processElGamalSecretKey(inputStream, p, g, y,
+						keyProtectionRemoverFactory);
+
+				PublicKeyPacket pubPacket = new PublicKeyPacket(
+						PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, new Date(),
+						new ElGamalPublicBCPGKey(p, g, y));
+
+				return new PGPSecretKey(
+						new SecretKeyPacket(pubPacket,
+								SymmetricKeyAlgorithmTags.NULL, null, null,
+								new ElGamalSecretBCPGKey(x).getEncoded()),
+						new PGPPublicKey(pubPacket, fingerPrintCalculator));
+			} else if (keyType.equals("rsa")) {
+				BigInteger n = readBigInteger("n", inputStream);
+				BigInteger e = readBigInteger("e", inputStream);
+
+				BigInteger[] values = processRSASecretKey(inputStream, n, e,
+						keyProtectionRemoverFactory);
+
+				// TODO: type of RSA key?
+				PublicKeyPacket pubPacket = new PublicKeyPacket(
+						PublicKeyAlgorithmTags.RSA_GENERAL, new Date(),
+						new RSAPublicBCPGKey(n, e));
+
+				return new PGPSecretKey(
+						new SecretKeyPacket(pubPacket,
+								SymmetricKeyAlgorithmTags.NULL, null, null,
+								new RSASecretBCPGKey(values[0], values[1],
+										values[2]).getEncoded()),
+						new PGPPublicKey(pubPacket, fingerPrintCalculator));
+			} else {
+				throw new PGPException("unknown key type: " + keyType);
+			}
+		}
+
+		throw new PGPException("unknown key type found");
+	}
+
+	private BigInteger readBigInteger(String expectedType,
+			InputStream inputStream) throws IOException, PGPException {
+		SXprUtils.skipOpenParenthesis(inputStream);
+
+		String type = SXprUtils.readString(inputStream, inputStream.read());
+		if (!type.equals(expectedType)) {
+			throw new PGPException(expectedType + " value expected");
+		}
+
+		byte[] nBytes = SXprUtils.readBytes(inputStream, inputStream.read());
+		BigInteger v = new BigInteger(1, nBytes);
+
+		SXprUtils.skipCloseParenthesis(inputStream);
+
+		return v;
+	}
+
+	private static byte[][] extractData(InputStream inputStream,
+			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+			throws PGPException, IOException {
+		byte[] data;
+		byte[] protectedAt = null;
+
+		SXprUtils.skipOpenParenthesis(inputStream);
+
+		String type = SXprUtils.readString(inputStream, inputStream.read());
+		if (type.equals("protected")) {
+			String protection = SXprUtils.readString(inputStream,
+					inputStream.read());
+
+			SXprUtils.skipOpenParenthesis(inputStream);
+
+			S2K s2k = SXprUtils.parseS2K(inputStream);
+
+			byte[] iv = SXprUtils.readBytes(inputStream, inputStream.read());
+
+			SXprUtils.skipCloseParenthesis(inputStream);
+
+			byte[] secKeyData = SXprUtils.readBytes(inputStream,
+					inputStream.read());
+
+			SXprUtils.skipCloseParenthesis(inputStream);
+
+			PBESecretKeyDecryptor keyDecryptor = keyProtectionRemoverFactory
+					.createDecryptor(protection);
+
+			// TODO: recognise other algorithms
+			byte[] key = keyDecryptor.makeKeyFromPassPhrase(
+					SymmetricKeyAlgorithmTags.AES_128, s2k);
+
+			data = keyDecryptor.recoverKeyData(
+					SymmetricKeyAlgorithmTags.AES_128, key, iv, secKeyData, 0,
+					secKeyData.length);
+
+			// check if protected at is present
+			if (inputStream.read() == '(') {
+				ByteArrayOutputStream bOut = new ByteArrayOutputStream();
+
+				bOut.write('(');
+				int ch;
+				while ((ch = inputStream.read()) >= 0 && ch != ')') {
+					bOut.write(ch);
+				}
+
+				if (ch != ')') {
+					throw new IOException("unexpected end to SExpr");
+				}
+
+				bOut.write(')');
+
+				protectedAt = bOut.toByteArray();
+			}
+
+			SXprUtils.skipCloseParenthesis(inputStream);
+			SXprUtils.skipCloseParenthesis(inputStream);
+		} else if (type.equals("d") || type.equals("x")) {
+			// JGit modification: unencrypted DSA or ECC keys can have an "x"
+			// here
+			return null;
+		} else {
+			throw new PGPException("protected block not found");
+		}
+
+		return new byte[][] { data, protectedAt };
+	}
+
+	private BigInteger processDSASecretKey(InputStream inputStream,
+			BigInteger p, BigInteger q, BigInteger g, BigInteger y,
+			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+			throws IOException, PGPException {
+		String type;
+		byte[][] basicData = extractData(inputStream,
+				keyProtectionRemoverFactory);
+
+		// JGit modification: handle unencrypted DSA keys
+		if (basicData == null) {
+			byte[] nBytes = SXprUtils.readBytes(inputStream,
+					inputStream.read());
+			BigInteger x = new BigInteger(1, nBytes);
+			SXprUtils.skipCloseParenthesis(inputStream);
+			return x;
+		}
+
+		byte[] keyData = basicData[0];
+		byte[] protectedAt = basicData[1];
+
+		//
+		// parse the secret key S-expr
+		//
+		InputStream keyIn = new ByteArrayInputStream(keyData);
+
+		SXprUtils.skipOpenParenthesis(keyIn);
+		SXprUtils.skipOpenParenthesis(keyIn);
+
+		BigInteger x = readBigInteger("x", keyIn);
+
+		SXprUtils.skipCloseParenthesis(keyIn);
+
+		// JGit modification: OCB-encrypted keys don't have and don't need a
+		// hash
+		if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
+			return x;
+		}
+
+		SXprUtils.skipOpenParenthesis(keyIn);
+		type = SXprUtils.readString(keyIn, keyIn.read());
+
+		if (!type.equals("hash")) {
+			throw new PGPException("hash keyword expected");
+		}
+		type = SXprUtils.readString(keyIn, keyIn.read());
+
+		if (!type.equals("sha1")) {
+			throw new PGPException("hash keyword expected");
+		}
+
+		byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
+
+		SXprUtils.skipCloseParenthesis(keyIn);
+
+		if (digestProvider != null) {
+			PGPDigestCalculator digestCalculator = digestProvider
+					.get(HashAlgorithmTags.SHA1);
+
+			OutputStream dOut = digestCalculator.getOutputStream();
+
+			dOut.write(Strings.toByteArray("(3:dsa"));
+			writeCanonical(dOut, "p", p);
+			writeCanonical(dOut, "q", q);
+			writeCanonical(dOut, "g", g);
+			writeCanonical(dOut, "y", y);
+			writeCanonical(dOut, "x", x);
+
+			// check protected-at
+			if (protectedAt != null) {
+				dOut.write(protectedAt);
+			}
+
+			dOut.write(Strings.toByteArray(")"));
+
+			byte[] check = digestCalculator.getDigest();
+			if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
+				throw new PGPException(
+						"checksum on protected data failed in SExpr");
+			}
+		}
+
+		return x;
+	}
+
+	private BigInteger processElGamalSecretKey(InputStream inputStream,
+			BigInteger p, BigInteger g, BigInteger y,
+			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+			throws IOException, PGPException {
+		String type;
+		byte[][] basicData = extractData(inputStream,
+				keyProtectionRemoverFactory);
+
+		// JGit modification: handle unencrypted EC keys
+		if (basicData == null) {
+			byte[] nBytes = SXprUtils.readBytes(inputStream,
+					inputStream.read());
+			BigInteger x = new BigInteger(1, nBytes);
+			SXprUtils.skipCloseParenthesis(inputStream);
+			return x;
+		}
+
+		byte[] keyData = basicData[0];
+		byte[] protectedAt = basicData[1];
+
+		//
+		// parse the secret key S-expr
+		//
+		InputStream keyIn = new ByteArrayInputStream(keyData);
+
+		SXprUtils.skipOpenParenthesis(keyIn);
+		SXprUtils.skipOpenParenthesis(keyIn);
+
+		BigInteger x = readBigInteger("x", keyIn);
+
+		SXprUtils.skipCloseParenthesis(keyIn);
+
+		// JGit modification: OCB-encrypted keys don't have and don't need a
+		// hash
+		if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
+			return x;
+		}
+
+		SXprUtils.skipOpenParenthesis(keyIn);
+		type = SXprUtils.readString(keyIn, keyIn.read());
+
+		if (!type.equals("hash")) {
+			throw new PGPException("hash keyword expected");
+		}
+		type = SXprUtils.readString(keyIn, keyIn.read());
+
+		if (!type.equals("sha1")) {
+			throw new PGPException("hash keyword expected");
+		}
+
+		byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
+
+		SXprUtils.skipCloseParenthesis(keyIn);
+
+		if (digestProvider != null) {
+			PGPDigestCalculator digestCalculator = digestProvider
+					.get(HashAlgorithmTags.SHA1);
+
+			OutputStream dOut = digestCalculator.getOutputStream();
+
+			dOut.write(Strings.toByteArray("(3:elg"));
+			writeCanonical(dOut, "p", p);
+			writeCanonical(dOut, "g", g);
+			writeCanonical(dOut, "y", y);
+			writeCanonical(dOut, "x", x);
+
+			// check protected-at
+			if (protectedAt != null) {
+				dOut.write(protectedAt);
+			}
+
+			dOut.write(Strings.toByteArray(")"));
+
+			byte[] check = digestCalculator.getDigest();
+			if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
+				throw new PGPException(
+						"checksum on protected data failed in SExpr");
+			}
+		}
+
+		return x;
+	}
+
+	private BigInteger processECSecretKey(InputStream inputStream,
+			String curveID, String curveName, byte[] qVal,
+			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+			throws IOException, PGPException {
+		String type;
+
+		byte[][] basicData = extractData(inputStream,
+				keyProtectionRemoverFactory);
+
+		// JGit modification: handle unencrypted EC keys
+		if (basicData == null) {
+			byte[] nBytes = SXprUtils.readBytes(inputStream,
+					inputStream.read());
+			BigInteger d = new BigInteger(1, nBytes);
+			SXprUtils.skipCloseParenthesis(inputStream);
+			return d;
+		}
+
+		byte[] keyData = basicData[0];
+		byte[] protectedAt = basicData[1];
+
+		//
+		// parse the secret key S-expr
+		//
+		InputStream keyIn = new ByteArrayInputStream(keyData);
+
+		SXprUtils.skipOpenParenthesis(keyIn);
+		SXprUtils.skipOpenParenthesis(keyIn);
+		BigInteger d = readBigInteger("d", keyIn);
+		SXprUtils.skipCloseParenthesis(keyIn);
+
+		// JGit modification: OCB-encrypted keys don't have and don't need a
+		// hash
+		if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
+			return d;
+		}
+
+		SXprUtils.skipOpenParenthesis(keyIn);
+
+		type = SXprUtils.readString(keyIn, keyIn.read());
+
+		if (!type.equals("hash")) {
+			throw new PGPException("hash keyword expected");
+		}
+		type = SXprUtils.readString(keyIn, keyIn.read());
+
+		if (!type.equals("sha1")) {
+			throw new PGPException("hash keyword expected");
+		}
+
+		byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
+
+		SXprUtils.skipCloseParenthesis(keyIn);
+
+		if (digestProvider != null) {
+			PGPDigestCalculator digestCalculator = digestProvider
+					.get(HashAlgorithmTags.SHA1);
+
+			OutputStream dOut = digestCalculator.getOutputStream();
+
+			dOut.write(Strings.toByteArray("(3:ecc"));
+
+			dOut.write(Strings.toByteArray("(" + curveID.length() + ":"
+					+ curveID + curveName.length() + ":" + curveName + ")"));
+
+			writeCanonical(dOut, "q", qVal);
+			writeCanonical(dOut, "d", d);
+
+			// check protected-at
+			if (protectedAt != null) {
+				dOut.write(protectedAt);
+			}
+
+			dOut.write(Strings.toByteArray(")"));
+
+			byte[] check = digestCalculator.getDigest();
+
+			if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
+				throw new PGPException(
+						"checksum on protected data failed in SExpr");
+			}
+		}
+
+		return d;
+	}
+
+	private BigInteger[] processRSASecretKey(InputStream inputStream,
+			BigInteger n, BigInteger e,
+			PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+			throws IOException, PGPException {
+		String type;
+		byte[][] basicData = extractData(inputStream,
+				keyProtectionRemoverFactory);
+
+		byte[] keyData;
+		byte[] protectedAt = null;
+
+		InputStream keyIn;
+		BigInteger d;
+
+		if (basicData == null) {
+			keyIn = inputStream;
+			byte[] nBytes = SXprUtils.readBytes(inputStream,
+					inputStream.read());
+			d = new BigInteger(1, nBytes);
+
+			SXprUtils.skipCloseParenthesis(inputStream);
+
+		} else {
+			keyData = basicData[0];
+			protectedAt = basicData[1];
+
+			keyIn = new ByteArrayInputStream(keyData);
+
+			SXprUtils.skipOpenParenthesis(keyIn);
+			SXprUtils.skipOpenParenthesis(keyIn);
+			d = readBigInteger("d", keyIn);
+		}
+
+		//
+		// parse the secret key S-expr
+		//
+
+		BigInteger p = readBigInteger("p", keyIn);
+		BigInteger q = readBigInteger("q", keyIn);
+		BigInteger u = readBigInteger("u", keyIn);
+
+		// JGit modification: OCB-encrypted keys don't have and don't need a
+		// hash
+		if (basicData == null
+				|| keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
+			return new BigInteger[] { d, p, q, u };
+		}
+
+		SXprUtils.skipCloseParenthesis(keyIn);
+
+		SXprUtils.skipOpenParenthesis(keyIn);
+		type = SXprUtils.readString(keyIn, keyIn.read());
+
+		if (!type.equals("hash")) {
+			throw new PGPException("hash keyword expected");
+		}
+		type = SXprUtils.readString(keyIn, keyIn.read());
+
+		if (!type.equals("sha1")) {
+			throw new PGPException("hash keyword expected");
+		}
+
+		byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
+
+		SXprUtils.skipCloseParenthesis(keyIn);
+
+		if (digestProvider != null) {
+			PGPDigestCalculator digestCalculator = digestProvider
+					.get(HashAlgorithmTags.SHA1);
+
+			OutputStream dOut = digestCalculator.getOutputStream();
+
+			dOut.write(Strings.toByteArray("(3:rsa"));
+
+			writeCanonical(dOut, "n", n);
+			writeCanonical(dOut, "e", e);
+			writeCanonical(dOut, "d", d);
+			writeCanonical(dOut, "p", p);
+			writeCanonical(dOut, "q", q);
+			writeCanonical(dOut, "u", u);
+
+			// check protected-at
+			if (protectedAt != null) {
+				dOut.write(protectedAt);
+			}
+
+			dOut.write(Strings.toByteArray(")"));
+
+			byte[] check = digestCalculator.getDigest();
+
+			if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
+				throw new PGPException(
+						"checksum on protected data failed in SExpr");
+			}
+		}
+
+		return new BigInteger[] { d, p, q, u };
+	}
+
+	private void writeCanonical(OutputStream dOut, String label, BigInteger i)
+			throws IOException {
+		writeCanonical(dOut, label, i.toByteArray());
+	}
+
+	private void writeCanonical(OutputStream dOut, String label, byte[] data)
+			throws IOException {
+		dOut.write(Strings.toByteArray(
+				"(" + label.length() + ":" + label + data.length + ":"));
+		dOut.write(data);
+		dOut.write(Strings.toByteArray(")"));
+	}
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
new file mode 100644
index 0000000..220aa28
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
+ * <p>
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
+ *including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ * </p>
+ * <p>
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ * </p>
+ * <p>
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </p>
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+// This class is an unmodified copy from Bouncy Castle; needed because it's package-visible only and used by SExprParser.
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.bcpg.S2K;
+import org.bouncycastle.util.io.Streams;
+
+/**
+ * Utility functions for looking a S-expression keys. This class will move when
+ * it finds a better home!
+ * <p>
+ * Format documented here:
+ * http://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=agent/keyformat.txt;h=42c4b1f06faf1bbe71ffadc2fee0fad6bec91a97;hb=refs/heads/master
+ * </p>
+ */
+class SXprUtils {
+	private static int readLength(InputStream in, int ch) throws IOException {
+		int len = ch - '0';
+
+		while ((ch = in.read()) >= 0 && ch != ':') {
+			len = len * 10 + ch - '0';
+		}
+
+		return len;
+	}
+
+	static String readString(InputStream in, int ch) throws IOException {
+		int len = readLength(in, ch);
+
+		char[] chars = new char[len];
+
+		for (int i = 0; i != chars.length; i++) {
+			chars[i] = (char) in.read();
+		}
+
+		return new String(chars);
+	}
+
+	static byte[] readBytes(InputStream in, int ch) throws IOException {
+		int len = readLength(in, ch);
+
+		byte[] data = new byte[len];
+
+		Streams.readFully(in, data);
+
+		return data;
+	}
+
+	static S2K parseS2K(InputStream in) throws IOException {
+		skipOpenParenthesis(in);
+
+		// Algorithm is hard-coded to SHA1 below anyway.
+		readString(in, in.read());
+		byte[] iv = readBytes(in, in.read());
+		final long iterationCount = Long.parseLong(readString(in, in.read()));
+
+		skipCloseParenthesis(in);
+
+		// we have to return the actual iteration count provided.
+		S2K s2k = new S2K(HashAlgorithmTags.SHA1, iv, (int) iterationCount) {
+			@Override
+			public long getIterationCount() {
+				return iterationCount;
+			}
+		};
+
+		return s2k;
+	}
+
+	static void skipOpenParenthesis(InputStream in) throws IOException {
+		int ch = in.read();
+		if (ch != '(') {
+			throw new IOException(
+					"unknown character encountered: " + (char) ch); //$NON-NLS-1$
+		}
+	}
+
+	static void skipCloseParenthesis(InputStream in) throws IOException {
+		int ch = in.read();
+		if (ch != ')') {
+			throw new IOException("unknown character encountered"); //$NON-NLS-1$
+		}
+	}
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java
new file mode 100644
index 0000000..269a1ba
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StreamCorruptedException;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Arrays;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
+import org.bouncycastle.util.io.Streams;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.gpg.bc.internal.BCText;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Utilities for reading GPG secret keys from a gpg-agent key file.
+ */
+public final class SecretKeys {
+
+	private SecretKeys() {
+		// No instantiation.
+	}
+
+	/**
+	 * Something that can supply a passphrase to decrypt an encrypted secret
+	 * key.
+	 */
+	public interface PassphraseSupplier {
+
+		/**
+		 * Supplies a passphrase.
+		 *
+		 * @return the passphrase
+		 * @throws PGPException
+		 *             if no passphrase can be obtained
+		 * @throws CanceledException
+		 *             if the user canceled passphrase entry
+		 * @throws UnsupportedCredentialItem
+		 *             if an internal error occurred
+		 * @throws URISyntaxException
+		 *             if an internal error occurred
+		 */
+		char[] getPassphrase() throws PGPException, CanceledException,
+				UnsupportedCredentialItem, URISyntaxException;
+	}
+
+	private static final byte[] PROTECTED_KEY = "protected-private-key" //$NON-NLS-1$
+			.getBytes(StandardCharsets.US_ASCII);
+
+	private static final byte[] OCB_PROTECTED = "openpgp-s2k3-ocb-aes" //$NON-NLS-1$
+			.getBytes(StandardCharsets.US_ASCII);
+
+	/**
+	 * Reads a GPG secret key from the given stream.
+	 *
+	 * @param in
+	 *            {@link InputStream} to read from, doesn't need to be buffered
+	 * @param calculatorProvider
+	 *            for checking digests
+	 * @param passphraseSupplier
+	 *            for decrypting encrypted keys
+	 * @param publicKey
+	 *            the secret key should be for
+	 * @return the secret key
+	 * @throws IOException
+	 *             if the stream cannot be parsed
+	 * @throws PGPException
+	 *             if thrown by the underlying S-Expression parser, for instance
+	 *             when the passphrase is wrong
+	 * @throws CanceledException
+	 *             if thrown by the {@code passphraseSupplier}
+	 * @throws UnsupportedCredentialItem
+	 *             if thrown by the {@code passphraseSupplier}
+	 * @throws URISyntaxException
+	 *             if thrown by the {@code passphraseSupplier}
+	 */
+	public static PGPSecretKey readSecretKey(InputStream in,
+			PGPDigestCalculatorProvider calculatorProvider,
+			PassphraseSupplier passphraseSupplier, PGPPublicKey publicKey)
+			throws IOException, PGPException, CanceledException,
+			UnsupportedCredentialItem, URISyntaxException {
+		byte[] data = Streams.readAll(in);
+		if (data.length == 0) {
+			throw new EOFException();
+		} else if (data.length < 4 + PROTECTED_KEY.length) {
+			// +4 for "(21:" for a binary protected key
+			throw new IOException(
+					MessageFormat.format(BCText.get().secretKeyTooShort,
+							Integer.toUnsignedString(data.length)));
+		}
+		SExprParser parser = new SExprParser(calculatorProvider);
+		byte firstChar = data[0];
+		try {
+			if (firstChar == '(') {
+				// Binary format.
+				PBEProtectionRemoverFactory decryptor = null;
+				if (matches(data, 4, PROTECTED_KEY)) {
+					// AES/CBC encrypted.
+					decryptor = new JcePBEProtectionRemoverFactory(
+							passphraseSupplier.getPassphrase(),
+							calculatorProvider);
+				}
+				try (InputStream sIn = new ByteArrayInputStream(data)) {
+					return parser.parseSecretKey(sIn, decryptor, publicKey);
+				}
+			}
+			// Assume it's the new key-value format.
+			try (ByteArrayInputStream keyIn = new ByteArrayInputStream(data)) {
+				byte[] rawData = keyFromNameValueFormat(keyIn);
+				if (!matches(rawData, 1, PROTECTED_KEY)) {
+					// Not encrypted human-readable format.
+					try (InputStream sIn = new ByteArrayInputStream(
+							convertSexpression(rawData))) {
+						return parser.parseSecretKey(sIn, null, publicKey);
+					}
+				}
+				// An encrypted key from a key-value file. Most likely AES/OCB
+				// encrypted.
+				boolean isOCB[] = { false };
+				byte[] sExp = convertSexpression(rawData, isOCB);
+				PBEProtectionRemoverFactory decryptor;
+				if (isOCB[0]) {
+					decryptor = new OCBPBEProtectionRemoverFactory(
+							passphraseSupplier.getPassphrase(),
+							calculatorProvider, getAad(sExp));
+				} else {
+					decryptor = new JcePBEProtectionRemoverFactory(
+							passphraseSupplier.getPassphrase(),
+							calculatorProvider);
+				}
+				try (InputStream sIn = new ByteArrayInputStream(sExp)) {
+					return parser.parseSecretKey(sIn, decryptor, publicKey);
+				}
+			}
+		} catch (IOException e) {
+			throw new PGPException(e.getLocalizedMessage(), e);
+		}
+	}
+
+	/**
+	 * Extract the AAD for the OCB decryption from an s-expression.
+	 *
+	 * @param sExp
+	 *            buffer containing a valid binary s-expression
+	 * @return the AAD
+	 */
+	private static byte[] getAad(byte[] sExp) {
+		// Given a key
+		// @formatter:off
+		// (protected-private-key (rsa ... (protected openpgp-s2k3-ocb-aes ... )(protected-at ...)))
+		//                        A        B                                    C                  D
+		// The AAD is [A..B)[C..D). (From the binary serialized form.)
+		// @formatter:on
+		int i = 1; // Skip initial '('
+		while (sExp[i] != '(') {
+			i++;
+		}
+		int aadStart = i++;
+		int aadEnd = skip(sExp, aadStart);
+		byte[] protectedPrefix = "(9:protected" //$NON-NLS-1$
+				.getBytes(StandardCharsets.US_ASCII);
+		while (!matches(sExp, i, protectedPrefix)) {
+			i++;
+		}
+		int protectedStart = i;
+		int protectedEnd = skip(sExp, protectedStart);
+		byte[] aadData = new byte[aadEnd - aadStart
+				- (protectedEnd - protectedStart)];
+		System.arraycopy(sExp, aadStart, aadData, 0, protectedStart - aadStart);
+		System.arraycopy(sExp, protectedEnd, aadData, protectedStart - aadStart,
+				aadEnd - protectedEnd);
+		return aadData;
+	}
+
+	/**
+	 * Skips a list including nested lists.
+	 *
+	 * @param sExp
+	 *            buffer containing valid binary s-expression data
+	 * @param start
+	 *            index of the opening '(' of the list to skip
+	 * @return the index after the closing ')' of the skipped list
+	 */
+	private static int skip(byte[] sExp, int start) {
+		int i = start + 1;
+		int depth = 1;
+		while (depth > 0) {
+			switch (sExp[i]) {
+			case '(':
+				depth++;
+				break;
+			case ')':
+				depth--;
+				break;
+			default:
+				// We must be on a length
+				int j = i;
+				while (sExp[j] >= '0' && sExp[j] <= '9') {
+					j++;
+				}
+				// j is on the colon
+				int length = Integer.parseInt(
+						new String(sExp, i, j - i, StandardCharsets.US_ASCII));
+				i = j + length;
+			}
+			i++;
+		}
+		return i;
+	}
+
+	/**
+	 * Checks whether the {@code needle} matches {@code src} at offset
+	 * {@code from}.
+	 *
+	 * @param src
+	 *            to match against {@code needle}
+	 * @param from
+	 *            position in {@code src} to start matching
+	 * @param needle
+	 *            to match against
+	 * @return {@code true} if {@code src} contains {@code needle} at position
+	 *         {@code from}, {@code false} otherwise
+	 */
+	private static boolean matches(byte[] src, int from, byte[] needle) {
+		if (from < 0 || from + needle.length > src.length) {
+			return false;
+		}
+		return org.bouncycastle.util.Arrays.constantTimeAreEqual(needle.length,
+				src, from, needle, 0);
+	}
+
+	/**
+	 * Converts a human-readable serialized s-expression into a binary
+	 * serialized s-expression.
+	 *
+	 * @param humanForm
+	 *            to convert
+	 * @return the converted s-expression
+	 * @throws IOException
+	 *             if the conversion fails
+	 */
+	private static byte[] convertSexpression(byte[] humanForm)
+			throws IOException {
+		boolean[] isOCB = { false };
+		return convertSexpression(humanForm, isOCB);
+	}
+
+	/**
+	 * Converts a human-readable serialized s-expression into a binary
+	 * serialized s-expression.
+	 *
+	 * @param humanForm
+	 *            to convert
+	 * @param isOCB
+	 *            returns whether the s-expression specified AES/OCB encryption
+	 * @return the converted s-expression
+	 * @throws IOException
+	 *             if the conversion fails
+	 */
+	private static byte[] convertSexpression(byte[] humanForm, boolean[] isOCB)
+			throws IOException {
+		int pos = 0;
+		try (ByteArrayOutputStream out = new ByteArrayOutputStream(
+				humanForm.length)) {
+			while (pos < humanForm.length) {
+				byte b = humanForm[pos];
+				if (b == '(' || b == ')') {
+					out.write(b);
+					pos++;
+				} else if (isGpgSpace(b)) {
+					pos++;
+				} else if (b == '#') {
+					// Hex value follows up to the next #
+					int i = ++pos;
+					while (i < humanForm.length && isHex(humanForm[i])) {
+						i++;
+					}
+					if (i == pos || humanForm[i] != '#') {
+						throw new StreamCorruptedException(
+								BCText.get().sexprHexNotClosed);
+					}
+					if ((i - pos) % 2 != 0) {
+						throw new StreamCorruptedException(
+								BCText.get().sexprHexOdd);
+					}
+					int l = (i - pos) / 2;
+					out.write(Integer.toString(l)
+							.getBytes(StandardCharsets.US_ASCII));
+					out.write(':');
+					while (pos < i) {
+						int x = (nibble(humanForm[pos]) << 4)
+								| nibble(humanForm[pos + 1]);
+						pos += 2;
+						out.write(x);
+					}
+					pos = i + 1;
+				} else if (isTokenChar(b)) {
+					// Scan the token
+					int start = pos++;
+					while (pos < humanForm.length
+							&& isTokenChar(humanForm[pos])) {
+						pos++;
+					}
+					int l = pos - start;
+					if (pos - start == OCB_PROTECTED.length
+							&& matches(humanForm, start, OCB_PROTECTED)) {
+						isOCB[0] = true;
+					}
+					out.write(Integer.toString(l)
+							.getBytes(StandardCharsets.US_ASCII));
+					out.write(':');
+					out.write(humanForm, start, pos - start);
+				} else if (b == '"') {
+					// Potentially quoted string.
+					int start = ++pos;
+					boolean escaped = false;
+					while (pos < humanForm.length
+							&& (escaped || humanForm[pos] != '"')) {
+						int ch = humanForm[pos++];
+						escaped = !escaped && ch == '\\';
+					}
+					if (pos >= humanForm.length) {
+						throw new StreamCorruptedException(
+								BCText.get().sexprStringNotClosed);
+					}
+					// start is on the first character of the string, pos on the
+					// closing quote.
+					byte[] dq = dequote(humanForm, start, pos);
+					out.write(Integer.toString(dq.length)
+							.getBytes(StandardCharsets.US_ASCII));
+					out.write(':');
+					out.write(dq);
+					pos++;
+				} else {
+					throw new StreamCorruptedException(
+							MessageFormat.format(BCText.get().sexprUnhandled,
+									Integer.toHexString(b & 0xFF)));
+				}
+			}
+			return out.toByteArray();
+		}
+	}
+
+	/**
+	 * GPG-style string de-quoting, which is basically C-style, with some
+	 * literal CR/LF escaping.
+	 *
+	 * @param in
+	 *            buffer containing the quoted string
+	 * @param from
+	 *            index after the opening quote in {@code in}
+	 * @param to
+	 *            index of the closing quote in {@code in}
+	 * @return the dequoted raw string value
+	 * @throws StreamCorruptedException
+	 */
+	private static byte[] dequote(byte[] in, int from, int to)
+			throws StreamCorruptedException {
+		// Result must be shorter or have the same length
+		byte[] out = new byte[to - from];
+		int j = 0;
+		int i = from;
+		while (i < to) {
+			byte b = in[i++];
+			if (b != '\\') {
+				out[j++] = b;
+				continue;
+			}
+			if (i == to) {
+				throw new StreamCorruptedException(
+						BCText.get().sexprStringInvalidEscapeAtEnd);
+			}
+			b = in[i++];
+			switch (b) {
+			case 'b':
+				out[j++] = '\b';
+				break;
+			case 'f':
+				out[j++] = '\f';
+				break;
+			case 'n':
+				out[j++] = '\n';
+				break;
+			case 'r':
+				out[j++] = '\r';
+				break;
+			case 't':
+				out[j++] = '\t';
+				break;
+			case 'v':
+				out[j++] = 0x0B;
+				break;
+			case '"':
+			case '\'':
+			case '\\':
+				out[j++] = b;
+				break;
+			case '\r':
+				// Escaped literal line end. If an LF is following, skip that,
+				// too.
+				if (i < to && in[i] == '\n') {
+					i++;
+				}
+				break;
+			case '\n':
+				// Same for LF possibly followed by CR.
+				if (i < to && in[i] == '\r') {
+					i++;
+				}
+				break;
+			case 'x':
+				if (i + 1 >= to || !isHex(in[i]) || !isHex(in[i + 1])) {
+					throw new StreamCorruptedException(
+							BCText.get().sexprStringInvalidHexEscape);
+				}
+				out[j++] = (byte) ((nibble(in[i]) << 4) | nibble(in[i + 1]));
+				i += 2;
+				break;
+			case '0':
+			case '1':
+			case '2':
+			case '3':
+				if (i + 2 >= to || !isOctal(in[i]) || !isOctal(in[i + 1])
+						|| !isOctal(in[i + 2])) {
+					throw new StreamCorruptedException(
+							BCText.get().sexprStringInvalidOctalEscape);
+				}
+				out[j++] = (byte) (((((in[i] - '0') << 3)
+						| (in[i + 1] - '0')) << 3) | (in[i + 2] - '0'));
+				i += 3;
+				break;
+			default:
+				throw new StreamCorruptedException(MessageFormat.format(
+						BCText.get().sexprStringInvalidEscape,
+						Integer.toHexString(b & 0xFF)));
+			}
+		}
+		return Arrays.copyOf(out, j);
+	}
+
+	/**
+	 * Extracts the key from a GPG name-value-pair key file.
+	 * <p>
+	 * Package-visible for tests only.
+	 * </p>
+	 *
+	 * @param in
+	 *            {@link InputStream} to read from; should be buffered
+	 * @return the raw key data as extracted from the file
+	 * @throws IOException
+	 *             if the {@code in} stream cannot be read or does not contain a
+	 *             key
+	 */
+	static byte[] keyFromNameValueFormat(InputStream in) throws IOException {
+		// It would be nice if we could use RawParseUtils here, but GPG compares
+		// names case-insensitively. We're only interested in the "Key:"
+		// name-value pair.
+		int[] nameLow = { 'k', 'e', 'y', ':' };
+		int[] nameCap = { 'K', 'E', 'Y', ':' };
+		int nameIdx = 0;
+		for (;;) {
+			int next = in.read();
+			if (next < 0) {
+				throw new EOFException();
+			}
+			if (next == '\n') {
+				nameIdx = 0;
+			} else if (nameIdx >= 0) {
+				if (nameLow[nameIdx] == next || nameCap[nameIdx] == next) {
+					nameIdx++;
+					if (nameIdx == nameLow.length) {
+						break;
+					}
+				} else {
+					nameIdx = -1;
+				}
+			}
+		}
+		// We're after "Key:". Read the value as continuation lines.
+		int last = ':';
+		byte[] rawData;
+		try (ByteArrayOutputStream out = new ByteArrayOutputStream(8192)) {
+			for (;;) {
+				int next = in.read();
+				if (next < 0) {
+					break;
+				}
+				if (last == '\n') {
+					if (next == ' ' || next == '\t') {
+						// Continuation line; skip this whitespace
+						last = next;
+						continue;
+					}
+					break; // Not a continuation line
+				}
+				out.write(next);
+				last = next;
+			}
+			rawData = out.toByteArray();
+		}
+		// GPG trims off trailing whitespace, and a line having only whitespace
+		// is a single LF.
+		try (ByteArrayOutputStream out = new ByteArrayOutputStream(
+				rawData.length)) {
+			int lineStart = 0;
+			boolean trimLeading = true;
+			while (lineStart < rawData.length) {
+				int nextLineStart = RawParseUtils.nextLF(rawData, lineStart);
+				if (trimLeading) {
+					while (lineStart < nextLineStart
+							&& isGpgSpace(rawData[lineStart])) {
+						lineStart++;
+					}
+				}
+				// Trim trailing
+				int i = nextLineStart - 1;
+				while (lineStart < i && isGpgSpace(rawData[i])) {
+					i--;
+				}
+				if (i <= lineStart) {
+					// Empty line signifies LF
+					out.write('\n');
+					trimLeading = true;
+				} else {
+					out.write(rawData, lineStart, i - lineStart + 1);
+					trimLeading = false;
+				}
+				lineStart = nextLineStart;
+			}
+			return out.toByteArray();
+		}
+	}
+
+	private static boolean isGpgSpace(int ch) {
+		return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
+	}
+
+	private static boolean isTokenChar(int ch) {
+		switch (ch) {
+		case '-':
+		case '.':
+		case '/':
+		case '_':
+		case ':':
+		case '*':
+		case '+':
+		case '=':
+			return true;
+		default:
+			if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
+					|| (ch >= '0' && ch <= '9')) {
+				return true;
+			}
+			return false;
+		}
+	}
+
+	private static boolean isHex(int ch) {
+		return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')
+				|| (ch >= 'a' && ch <= 'f');
+	}
+
+	private static boolean isOctal(int ch) {
+		return (ch >= '0' && ch <= '7');
+	}
+
+	private static int nibble(int ch) {
+		if (ch >= '0' && ch <= '9') {
+			return ch - '0';
+		} else if (ch >= 'A' && ch <= 'F') {
+			return ch - 'A' + 10;
+		} else if (ch >= 'a' && ch <= 'f') {
+			return ch - 'a' + 10;
+		}
+		return -1;
+	}
+}
diff --git a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
index f521697..f2bed8d 100644
--- a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
@@ -3,33 +3,33 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.http.apache
 Bundle-SymbolicName: org.eclipse.jgit.http.apache
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
 Import-Package: org.apache.http;version="[4.3.0,5.0.0)",
- org.apache.http.client;version="[4.3.0,5.0.0)",
- org.apache.http.client.config;version="[4.3.0,5.0.0)",
- org.apache.http.client.methods;version="[4.3.0,5.0.0)",
- org.apache.http.client.params;version="[4.3.0,5.0.0)",
+ org.apache.http.client;version="[4.4.0,5.0.0)",
+ org.apache.http.client.config;version="[4.4.0,5.0.0)",
+ org.apache.http.client.methods;version="[4.4.0,5.0.0)",
+ org.apache.http.client.params;version="[4.4.0,5.0.0)",
  org.apache.http.config;version="[4.3.0,5.0.0)",
- org.apache.http.conn;version="[4.3.0,5.0.0)",
- org.apache.http.conn.params;version="[4.3.0,5.0.0)",
- org.apache.http.conn.scheme;version="[4.3.0,5.0.0)",
- org.apache.http.conn.socket;version="[4.3.0,5.0.0)",
- org.apache.http.conn.ssl;version="[4.3.0,5.0.0)",
- org.apache.http.conn.util;version="[4.3.0,5.0.0)",
+ org.apache.http.conn;version="[4.4.0,5.0.0)",
+ org.apache.http.conn.params;version="[4.4.0,5.0.0)",
+ org.apache.http.conn.scheme;version="[4.4.0,5.0.0)",
+ org.apache.http.conn.socket;version="[4.4.0,5.0.0)",
+ org.apache.http.conn.ssl;version="[4.4.0,5.0.0)",
+ org.apache.http.conn.util;version="[4.4.0,5.0.0)",
  org.apache.http.entity;version="[4.3.0,5.0.0)",
- org.apache.http.impl.client;version="[4.3.0,5.0.0)",
- org.apache.http.impl.conn;version="[4.3.0,5.0.0)",
+ org.apache.http.impl.client;version="[4.4.0,5.0.0)",
+ 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="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.http;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)"
-Export-Package: org.eclipse.jgit.transport.http.apache;version="5.10.1";
+ org.eclipse.jgit.annotations;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.http;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)"
+Export-Package: org.eclipse.jgit.transport.http.apache;version="5.11.2";
   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 fd37981..9ca1e9b 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.http.apache;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.http.apache;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.http.apache/pom.xml b/org.eclipse.jgit.http.apache/pom.xml
index d107c3d2..631859e 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.apache</artifactId>
diff --git a/org.eclipse.jgit.http.apache/resources/org/eclipse/jgit/transport/http/apache/internal/HttpApacheText.properties b/org.eclipse.jgit.http.apache/resources/org/eclipse/jgit/transport/http/apache/internal/HttpApacheText.properties
index d2e5216..b7b9af0 100644
--- a/org.eclipse.jgit.http.apache/resources/org/eclipse/jgit/transport/http/apache/internal/HttpApacheText.properties
+++ b/org.eclipse.jgit.http.apache/resources/org/eclipse/jgit/transport/http/apache/internal/HttpApacheText.properties
@@ -1 +1,2 @@
+httpWrongConnectionType=Wrong connection type: expected {0}, got {1}.
 unexpectedSSLContextException=unexpected exception when searching for the TLS protocol
diff --git a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java
index ed05f0a..90348f5 100644
--- a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java
+++ b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java
@@ -57,9 +57,7 @@
 import org.apache.http.config.RegistryBuilder;
 import org.apache.http.conn.socket.ConnectionSocketFactory;
 import org.apache.http.conn.socket.PlainConnectionSocketFactory;
-import org.apache.http.conn.ssl.DefaultHostnameVerifier;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
-import org.apache.http.conn.util.PublicSuffixMatcherLoader;
 import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.impl.client.SystemDefaultCredentialsProvider;
@@ -103,7 +101,11 @@
 
 	private HostnameVerifier hostnameverifier;
 
-	SSLContext ctx;
+	private SSLContext ctx;
+
+	private SSLConnectionSocketFactory socketFactory;
+
+	private boolean usePooling = true;
 
 	private HttpClient getClient() {
 		if (client == null) {
@@ -125,11 +127,18 @@
 				configBuilder
 						.setRedirectsEnabled(followRedirects.booleanValue());
 			}
-			SSLConnectionSocketFactory sslConnectionFactory = getSSLSocketFactory();
+			boolean pooled = true;
+			SSLConnectionSocketFactory sslConnectionFactory;
+			if (socketFactory != null) {
+				pooled = usePooling;
+				sslConnectionFactory = socketFactory;
+			} else {
+				// Legacy implementation.
+				pooled = (hostnameverifier == null);
+				sslConnectionFactory = getSSLSocketFactory();
+			}
 			clientBuilder.setSSLSocketFactory(sslConnectionFactory);
-			if (hostnameverifier != null) {
-				// Using a custom verifier: we don't want pooled connections
-				// with this.
+			if (!pooled) {
 				Registry<ConnectionSocketFactory> registry = RegistryBuilder
 						.<ConnectionSocketFactory> create()
 						.register("https", sslConnectionFactory)
@@ -147,14 +156,19 @@
 		return client;
 	}
 
+	void setSSLSocketFactory(@NonNull SSLConnectionSocketFactory factory,
+			boolean isDefault) {
+		socketFactory = factory;
+		usePooling = isDefault;
+	}
+
 	private SSLConnectionSocketFactory getSSLSocketFactory() {
 		HostnameVerifier verifier = hostnameverifier;
 		SSLContext context;
 		if (verifier == null) {
 			// Use defaults
-			context = SSLContexts.createDefault();
-			verifier = new DefaultHostnameVerifier(
-					PublicSuffixMatcherLoader.getDefault());
+			context = SSLContexts.createSystemDefault();
+			verifier = SSLConnectionSocketFactory.getDefaultHostnameVerifier();
 		} else {
 			// Using a custom verifier. Attention: configure() must have been
 			// called already, otherwise one gets a "context not initialized"
diff --git a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnectionFactory.java b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnectionFactory.java
index 3c05cde..4de3e47 100644
--- a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnectionFactory.java
+++ b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnectionFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2013, 2020 Christian Halstrick <christian.halstrick@sap.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -12,27 +12,100 @@
 import java.io.IOException;
 import java.net.Proxy;
 import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.text.MessageFormat;
 
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 import org.eclipse.jgit.transport.http.HttpConnection;
-import org.eclipse.jgit.transport.http.HttpConnectionFactory;
+import org.eclipse.jgit.transport.http.HttpConnectionFactory2;
+import org.eclipse.jgit.transport.http.NoCheckX509TrustManager;
+import org.eclipse.jgit.transport.http.apache.internal.HttpApacheText;
+import org.eclipse.jgit.util.HttpSupport;
 
 /**
- * A factory returning instances of
- * {@link org.eclipse.jgit.transport.http.apache.HttpClientConnection}
+ * A factory returning instances of {@link HttpClientConnection}.
  *
  * @since 3.3
  */
-public class HttpClientConnectionFactory implements HttpConnectionFactory {
-	/** {@inheritDoc} */
+public class HttpClientConnectionFactory implements HttpConnectionFactory2 {
+
 	@Override
 	public HttpConnection create(URL url) throws IOException {
 		return new HttpClientConnection(url.toString());
 	}
 
-	/** {@inheritDoc} */
 	@Override
-	public HttpConnection create(URL url, Proxy proxy)
-			throws IOException {
+	public HttpConnection create(URL url, Proxy proxy) throws IOException {
 		return new HttpClientConnection(url.toString(), proxy);
 	}
+
+	@Override
+	public GitSession newSession() {
+		return new HttpClientSession();
+	}
+
+	private static class HttpClientSession implements GitSession {
+
+		private SSLContext securityContext;
+
+		private SSLConnectionSocketFactory socketFactory;
+
+		private boolean isDefault;
+
+		@Override
+		public HttpClientConnection configure(HttpConnection connection,
+				boolean sslVerify)
+				throws IOException, GeneralSecurityException {
+			if (!(connection instanceof HttpClientConnection)) {
+				throw new IllegalArgumentException(MessageFormat.format(
+						HttpApacheText.get().httpWrongConnectionType,
+						HttpClientConnection.class.getName(),
+						connection.getClass().getName()));
+			}
+			HttpClientConnection conn = (HttpClientConnection) connection;
+			String scheme = conn.getURL().getProtocol();
+			if (!"https".equals(scheme)) { //$NON-NLS-1$
+				return conn;
+			}
+			if (securityContext == null || isDefault != sslVerify) {
+				isDefault = sslVerify;
+				HostnameVerifier verifier;
+				if (sslVerify) {
+					securityContext = SSLContext.getDefault();
+					verifier = SSLConnectionSocketFactory
+							.getDefaultHostnameVerifier();
+				} else {
+					securityContext = SSLContext.getInstance("TLS");
+					TrustManager[] trustAllCerts = {
+							new NoCheckX509TrustManager() };
+					securityContext.init(null, trustAllCerts, null);
+					verifier = (name, session) -> true;
+				}
+				socketFactory = new SSLConnectionSocketFactory(securityContext,
+						verifier) {
+
+					@Override
+					protected void prepareSocket(SSLSocket socket)
+							throws IOException {
+						super.prepareSocket(socket);
+						HttpSupport.configureTLS(socket);
+					}
+				};
+			}
+			conn.setSSLSocketFactory(socketFactory, isDefault);
+			return conn;
+		}
+
+		@Override
+		public void close() {
+			securityContext = null;
+			socketFactory = null;
+		}
+
+	}
 }
diff --git a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/internal/HttpApacheText.java b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/internal/HttpApacheText.java
index 907ab98..677d7d7 100644
--- a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/internal/HttpApacheText.java
+++ b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/internal/HttpApacheText.java
@@ -27,5 +27,6 @@
 	}
 
 	// @formatter:off
+	/***/ public String httpWrongConnectionType;
 	/***/ public String unexpectedSSLContextException;
 }
diff --git a/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
index 0d6e5d8..8b418d4 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.http.server;version="5.10.1",
- org.eclipse.jgit.http.server.glue;version="5.10.1";
+Export-Package: org.eclipse.jgit.http.server;version="5.11.2",
+ org.eclipse.jgit.http.server.glue;version="5.11.2";
   uses:="javax.servlet,javax.servlet.http",
- org.eclipse.jgit.http.server.resolver;version="5.10.1";
+ org.eclipse.jgit.http.server.resolver;version="5.11.2";
   uses:="org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.transport,
@@ -18,14 +18,14 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: javax.servlet;version="[2.5.0,3.2.0)",
  javax.servlet.http;version="[2.5.0,3.2.0)",
- org.eclipse.jgit.annotations;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.transport.parser;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.resolver;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)"
+ org.eclipse.jgit.annotations;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.transport.parser;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.resolver;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.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 a4eabce..fbbde74 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.http.server;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.http.server;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.http.server/pom.xml b/org.eclipse.jgit.http.server/pom.xml
index 9857129..2094f04 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.server</artifactId>
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoPacksServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoPacksServlet.java
index c3d7255..e90580b 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoPacksServlet.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoPacksServlet.java
@@ -20,7 +20,7 @@
 import javax.servlet.http.HttpServletResponse;
 
 import org.eclipse.jgit.internal.storage.file.ObjectDirectory;
-import org.eclipse.jgit.internal.storage.file.PackFile;
+import org.eclipse.jgit.internal.storage.file.Pack;
 import org.eclipse.jgit.lib.ObjectDatabase;
 
 /** Sends the current list of pack files, sorted most recent first. */
@@ -38,7 +38,7 @@
 		final StringBuilder out = new StringBuilder();
 		final ObjectDatabase db = getRepository(req).getObjectDatabase();
 		if (db instanceof ObjectDirectory) {
-			for (PackFile pack : ((ObjectDirectory) db).getPacks()) {
+			for (Pack pack : ((ObjectDirectory) db).getPacks()) {
 				out.append("P ");
 				out.append(pack.getPackFile().getName());
 				out.append('\n');
diff --git a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
index 05a82ac..c700271 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
@@ -12,7 +12,7 @@
  org.apache.commons.codec;version="[1.6.0,2.0.0)",
  org.apache.commons.codec.binary;version="[1.6.0,2.0.0)",
  org.apache.http;version="[4.3.0,5.0.0)",
- org.apache.http.client;version="[4.3.0,5.0.0)",
+ org.apache.http.client;version="[4.4.0,5.0.0)",
  org.apache.http.message;version="[4.3.0,5.0.0)",
  org.eclipse.jetty.continuation;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.http;version="[9.4.5,10.0.0)",
@@ -28,25 +28,26 @@
  org.eclipse.jetty.util.log;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.util.security;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.util.thread;version="[9.4.5,10.0.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.http.server;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.http.server.glue;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.http.server.resolver;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit.http;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.http;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.http.apache;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.resolver;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.http.server;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.http.server.glue;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.http.server.resolver;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit.http;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.http;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.http.apache;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.resolver;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
  org.hamcrest;version="[1.1.0,2.0.0)",
  org.hamcrest.core;version="[1.1.0,2.0.0)",
  org.junit;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.http.test/build.properties b/org.eclipse.jgit.http.test/build.properties
index e8bacac..a909f13 100644
--- a/org.eclipse.jgit.http.test/build.properties
+++ b/org.eclipse.jgit.http.test/build.properties
@@ -4,3 +4,5 @@
 bin.includes = META-INF/,\
                .,\
                plugin.properties
+additional.bundles = org.apache.log4j,\
+                     org.slf4j.binding.log4j12
diff --git a/org.eclipse.jgit.http.test/pom.xml b/org.eclipse.jgit.http.test/pom.xml
index 68d87c5..2985051 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.test</artifactId>
diff --git a/org.eclipse.jgit.http.test/src/org/eclipse/jgit/http/test/RefsUnreadableInMemoryRepository.java b/org.eclipse.jgit.http.test/src/org/eclipse/jgit/http/test/RefsUnreadableInMemoryRepository.java
index 80cbe87..4167b03 100644
--- a/org.eclipse.jgit.http.test/src/org/eclipse/jgit/http/test/RefsUnreadableInMemoryRepository.java
+++ b/org.eclipse.jgit.http.test/src/org/eclipse/jgit/http/test/RefsUnreadableInMemoryRepository.java
@@ -85,6 +85,17 @@
 
 		/** {@inheritDoc} */
 		@Override
+		public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes)
+				throws IOException {
+			if (failing) {
+				throw new IOException("disk failed, no refs found");
+			}
+
+			return super.getRefsByPrefixWithExclusions(include, excludes);
+		}
+
+		/** {@inheritDoc} */
+		@Override
 		public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException {
 			if (failing) {
 				throw new IOException("disk failed, no refs found");
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/AllProtocolsHttpTestCase.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/AllProtocolsHttpTestCase.java
new file mode 100644
index 0000000..c6931ad
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/AllProtocolsHttpTestCase.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020, 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.http.test;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jgit.junit.http.HttpTestCase;
+import org.eclipse.jgit.transport.HttpTransport;
+import org.eclipse.jgit.transport.http.HttpConnectionFactory;
+import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory;
+import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Abstract test base class for running HTTP-related tests with all connection
+ * factories provided in JGit and with both protocol V0 and V2.
+ */
+@Ignore
+@RunWith(Parameterized.class)
+public abstract class AllProtocolsHttpTestCase extends HttpTestCase {
+
+	protected static class TestParameters {
+
+		public final HttpConnectionFactory factory;
+
+		public final boolean enableProtocolV2;
+
+		public TestParameters(HttpConnectionFactory factory,
+				boolean enableProtocolV2) {
+			this.factory = factory;
+			this.enableProtocolV2 = enableProtocolV2;
+		}
+
+		@Override
+		public String toString() {
+			return factory.toString() + " protocol "
+					+ (enableProtocolV2 ? "V2" : "V0");
+		}
+	}
+
+	@Parameters(name = "{0}")
+	public static Collection<TestParameters> data() {
+		// run all tests with both connection factories we have
+		HttpConnectionFactory[] factories = new HttpConnectionFactory[] {
+				new JDKHttpConnectionFactory() {
+
+					@Override
+					public String toString() {
+						return this.getClass().getSuperclass().getName();
+					}
+				}, new HttpClientConnectionFactory() {
+
+					@Override
+					public String toString() {
+						return this.getClass().getSuperclass().getName();
+					}
+				} };
+		List<TestParameters> result = new ArrayList<>();
+		for (HttpConnectionFactory factory : factories) {
+			result.add(new TestParameters(factory, false));
+			result.add(new TestParameters(factory, true));
+		}
+		return result;
+	}
+
+	protected final boolean enableProtocolV2;
+
+	protected AllProtocolsHttpTestCase(TestParameters params) {
+		HttpTransport.setConnectionFactory(params.factory);
+		enableProtocolV2 = params.enableProtocolV2;
+	}
+
+	private static HttpConnectionFactory originalFactory;
+
+	@BeforeClass
+	public static void saveConnectionFactory() {
+		originalFactory = HttpTransport.getConnectionFactory();
+	}
+
+	@AfterClass
+	public static void restoreConnectionFactory() {
+		HttpTransport.setConnectionFactory(originalFactory);
+	}
+
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
index 6da5f86..8b28c42 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
@@ -35,6 +35,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.FetchConnection;
@@ -77,6 +78,9 @@
 
 		remoteRepository = src.getRepository();
 		remoteURI = toURIish(app, srcGit.getName());
+		StoredConfig cfg = remoteRepository.getConfig();
+		cfg.setInt("protocol", null, "version", 0);
+		cfg.save();
 
 		A_txt = src.blob("A");
 		A = src.commit().add("A_txt", A_txt).create();
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java
index ccde1fe..986b5ca 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java
@@ -35,6 +35,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.FetchConnection;
@@ -42,11 +43,10 @@
 import org.eclipse.jgit.transport.Transport;
 import org.eclipse.jgit.transport.TransportHttp;
 import org.eclipse.jgit.transport.URIish;
-import org.eclipse.jgit.transport.http.HttpConnectionFactory;
 import org.junit.Before;
 import org.junit.Test;
 
-public class DumbClientSmartServerTest extends AllFactoriesHttpTestCase {
+public class DumbClientSmartServerTest extends AllProtocolsHttpTestCase {
 	private Repository remoteRepository;
 
 	private URIish remoteURI;
@@ -55,8 +55,8 @@
 
 	private RevCommit A, B;
 
-	public DumbClientSmartServerTest(HttpConnectionFactory cf) {
-		super(cf);
+	public DumbClientSmartServerTest(TestParameters params) {
+		super(params);
 	}
 
 	@Override
@@ -76,6 +76,9 @@
 
 		remoteRepository = src.getRepository();
 		remoteURI = toURIish(app, srcName);
+		StoredConfig cfg = remoteRepository.getConfig();
+		cfg.setInt("protocol", null, "version", enableProtocolV2 ? 2 : 0);
+		cfg.save();
 
 		A_txt = src.blob("A");
 		A = src.commit().add("A_txt", A_txt).create();
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
index 26a453b..df093c1 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
@@ -38,7 +38,6 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.junit.http.AccessEvent;
 import org.eclipse.jgit.junit.http.AppServer;
-import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -50,7 +49,6 @@
 import org.eclipse.jgit.transport.PacketLineIn;
 import org.eclipse.jgit.transport.PacketLineOut;
 import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.transport.TransferConfig;
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
 import org.eclipse.jgit.transport.http.HttpConnection;
@@ -326,7 +324,22 @@
 	}
 
 	@Test
-	public void testHttpClientWantsV2ButServerNotConfigured() throws Exception {
+	public void testHttpClientWantsV2AndServerNotConfigured() throws Exception {
+		String url = smartAuthNoneURI.toString() + "/info/refs?service=git-upload-pack";
+		HttpConnection c = HttpTransport.getConnectionFactory()
+				.create(new URL(url));
+		c.setRequestMethod("GET");
+		c.setRequestProperty("Git-Protocol", "version=2");
+		assertEquals(200, c.getResponseCode());
+
+		PacketLineIn pckIn = new PacketLineIn(c.getInputStream());
+		assertThat(pckIn.readString(), is("version 2"));
+	}
+
+	@Test
+	public void testHttpServerConfiguredToV0() throws Exception {
+		remoteRepository.getRepository().getConfig().setInt(
+			"protocol", null, "version", 0);
 		String url = smartAuthNoneURI.toString() + "/info/refs?service=git-upload-pack";
 		HttpConnection c = HttpTransport.getConnectionFactory()
 				.create(new URL(url));
@@ -344,11 +357,6 @@
 
 	@Test
 	public void testV2HttpFirstResponse() throws Exception {
-		remoteRepository.getRepository().getConfig().setString(
-				ConfigConstants.CONFIG_PROTOCOL_SECTION, null,
-				ConfigConstants.CONFIG_KEY_VERSION,
-				TransferConfig.ProtocolVersion.V2.version());
-
 		String url = smartAuthNoneURI.toString() + "/info/refs?service=git-upload-pack";
 		HttpConnection c = HttpTransport.getConnectionFactory()
 				.create(new URL(url));
@@ -368,11 +376,6 @@
 
 	@Test
 	public void testV2HttpSubsequentResponse() throws Exception {
-		remoteRepository.getRepository().getConfig().setString(
-				ConfigConstants.CONFIG_PROTOCOL_SECTION, null,
-				ConfigConstants.CONFIG_KEY_VERSION,
-				TransferConfig.ProtocolVersion.V2.version());
-
 		String url = smartAuthNoneURI.toString() + "/git-upload-pack";
 		HttpConnection c = HttpTransport.getConnectionFactory()
 				.create(new URL(url));
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerSslTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerSslTest.java
index 597fb2e..7bc50ca 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerSslTest.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerSslTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2017, 2020 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
@@ -41,6 +41,7 @@
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.CredentialItem;
@@ -48,7 +49,6 @@
 import org.eclipse.jgit.transport.Transport;
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
-import org.eclipse.jgit.transport.http.HttpConnectionFactory;
 import org.eclipse.jgit.util.HttpSupport;
 import org.junit.Before;
 import org.junit.Test;
@@ -56,7 +56,7 @@
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
-public class SmartClientSmartServerSslTest extends AllFactoriesHttpTestCase {
+public class SmartClientSmartServerSslTest extends AllProtocolsHttpTestCase {
 
 	// We run these tests with a server on localhost with a self-signed
 	// certificate. We don't do authentication tests here, so there's no need
@@ -112,8 +112,8 @@
 
 	private RevCommit A, B;
 
-	public SmartClientSmartServerSslTest(HttpConnectionFactory cf) {
-		super(cf);
+	public SmartClientSmartServerSslTest(TestParameters params) {
+		super(params);
 	}
 
 	@Override
@@ -128,10 +128,11 @@
 
 		final TestRepository<Repository> src = createTestRepository();
 		final String srcName = src.getRepository().getDirectory().getName();
-		src.getRepository()
-				.getConfig()
-				.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+		StoredConfig cfg = src.getRepository().getConfig();
+		cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+		cfg.setInt("protocol", null, "version", enableProtocolV2 ? 2 : 0);
+		cfg.save();
 
 		GitServlet gs = new GitServlet();
 
@@ -238,7 +239,7 @@
 		fsck(dst, B);
 
 		List<AccessEvent> requests = getRequests();
-		assertEquals(2, requests.size());
+		assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
 	}
 
 	@Test
@@ -256,7 +257,7 @@
 		fsck(dst, B);
 
 		List<AccessEvent> requests = getRequests();
-		assertEquals(3, requests.size());
+		assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
index 8d1870a..887e970 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, 2017 Google Inc. and others
+ * Copyright (C) 2010, 2020 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
@@ -22,10 +22,17 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.io.Writer;
+import java.net.Proxy;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.text.MessageFormat;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -48,6 +55,8 @@
 import org.eclipse.jetty.servlet.FilterHolder;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.TransportConfigCallback;
 import org.eclipse.jgit.errors.RemoteRepositoryException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
@@ -70,6 +79,7 @@
 import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -86,13 +96,14 @@
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.transport.UploadPack;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.eclipse.jgit.transport.http.HttpConnection;
 import org.eclipse.jgit.transport.http.HttpConnectionFactory;
 import org.eclipse.jgit.util.HttpSupport;
 import org.eclipse.jgit.util.SystemReader;
 import org.junit.Before;
 import org.junit.Test;
 
-public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
+public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase {
 	private static final String HDR_TRANSFER_ENCODING = "Transfer-Encoding";
 
 	private AdvertiseRefsHook advertiseRefsHook;
@@ -120,8 +131,8 @@
 
 	private RevCommit A, B, unreachableCommit;
 
-	public SmartClientSmartServerTest(HttpConnectionFactory cf) {
-		super(cf);
+	public SmartClientSmartServerTest(TestParameters params) {
+		super(params);
 	}
 
 	@Override
@@ -131,10 +142,11 @@
 
 		final TestRepository<Repository> src = createTestRepository();
 		final String srcName = src.getRepository().getDirectory().getName();
-		src.getRepository()
-				.getConfig()
-				.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+		StoredConfig cfg = src.getRepository().getConfig();
+		cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+		cfg.setInt("protocol", null, "version", enableProtocolV2 ? 2 : 0);
+		cfg.save();
 
 		GitServlet gs = new GitServlet();
 		gs.setUploadPackFactory((HttpServletRequest req, Repository db) -> {
@@ -448,7 +460,7 @@
 		assertEquals(B, map.get(Constants.HEAD).getObjectId());
 
 		List<AccessEvent> requests = getRequests();
-		assertEquals(1, requests.size());
+		assertEquals(enableProtocolV2 ? 2 : 1, requests.size());
 
 		AccessEvent info = requests.get(0);
 		assertEquals("GET", info.getMethod());
@@ -458,7 +470,22 @@
 		assertEquals(200, info.getStatus());
 		assertEquals("application/x-git-upload-pack-advertisement", info
 				.getResponseHeader(HDR_CONTENT_TYPE));
-		assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		if (!enableProtocolV2) {
+			assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		} else {
+			AccessEvent lsRefs = requests.get(1);
+			assertEquals("POST", lsRefs.getMethod());
+			assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+			assertEquals(0, lsRefs.getParameters().size());
+			assertNotNull("has content-length",
+					lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+			assertNull("not chunked",
+					lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+			assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+			assertEquals(200, lsRefs.getStatus());
+			assertEquals("application/x-git-upload-pack-result",
+					lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+		}
 	}
 
 	@Test
@@ -576,9 +603,10 @@
 		}
 
 		List<AccessEvent> requests = getRequests();
-		assertEquals(2, requests.size());
+		assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
 
-		AccessEvent info = requests.get(0);
+		int requestNumber = 0;
+		AccessEvent info = requests.get(requestNumber++);
 		assertEquals("GET", info.getMethod());
 		assertEquals(join(remoteURI, "info/refs"), info.getPath());
 		assertEquals(1, info.getParameters().size());
@@ -586,9 +614,24 @@
 		assertEquals(200, info.getStatus());
 		assertEquals("application/x-git-upload-pack-advertisement", info
 				.getResponseHeader(HDR_CONTENT_TYPE));
-		assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		if (!enableProtocolV2) {
+			assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		} else {
+			AccessEvent lsRefs = requests.get(requestNumber++);
+			assertEquals("POST", lsRefs.getMethod());
+			assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+			assertEquals(0, lsRefs.getParameters().size());
+			assertNotNull("has content-length",
+					lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+			assertNull("not chunked",
+					lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+			assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+			assertEquals(200, lsRefs.getStatus());
+			assertEquals("application/x-git-upload-pack-result",
+					lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+		}
 
-		AccessEvent service = requests.get(1);
+		AccessEvent service = requests.get(requestNumber);
 		assertEquals("POST", service.getMethod());
 		assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
 		assertEquals(0, service.getParameters().size());
@@ -602,6 +645,63 @@
 				.getResponseHeader(HDR_CONTENT_TYPE));
 	}
 
+	@Test
+	public void test_CloneWithCustomFactory() throws Exception {
+		HttpConnectionFactory globalFactory = HttpTransport
+				.getConnectionFactory();
+		HttpConnectionFactory failingConnectionFactory = new HttpConnectionFactory() {
+
+			@Override
+			public HttpConnection create(URL url) throws IOException {
+				throw new IOException("Should not be reached");
+			}
+
+			@Override
+			public HttpConnection create(URL url, Proxy proxy)
+					throws IOException {
+				throw new IOException("Should not be reached");
+			}
+		};
+		HttpTransport.setConnectionFactory(failingConnectionFactory);
+		try {
+			File tmp = createTempDirectory("cloneViaApi");
+			boolean[] localFactoryUsed = { false };
+			TransportConfigCallback callback = new TransportConfigCallback() {
+
+				@Override
+				public void configure(Transport transport) {
+					if (transport instanceof TransportHttp) {
+						((TransportHttp) transport).setHttpConnectionFactory(
+								new HttpConnectionFactory() {
+
+									@Override
+									public HttpConnection create(URL url)
+											throws IOException {
+										localFactoryUsed[0] = true;
+										return globalFactory.create(url);
+									}
+
+									@Override
+									public HttpConnection create(URL url,
+											Proxy proxy) throws IOException {
+										localFactoryUsed[0] = true;
+										return globalFactory.create(url, proxy);
+									}
+								});
+					}
+				}
+			};
+			try (Git git = Git.cloneRepository().setDirectory(tmp)
+					.setTransportConfigCallback(callback)
+					.setURI(remoteURI.toPrivateString()).call()) {
+				assertTrue("Should have used the local HttpConnectionFactory",
+						localFactoryUsed[0]);
+			}
+		} finally {
+			HttpTransport.setConnectionFactory(globalFactory);
+		}
+	}
+
 	private void initialClone_Redirect(int nofRedirects, int code)
 			throws Exception {
 		initialClone_Redirect(nofRedirects, code, false);
@@ -628,7 +728,8 @@
 		}
 
 		List<AccessEvent> requests = getRequests();
-		assertEquals(2 + nofRedirects, requests.size());
+		assertEquals((enableProtocolV2 ? 3 : 2) + nofRedirects,
+				requests.size());
 
 		int n = 0;
 		while (n < nofRedirects) {
@@ -644,7 +745,22 @@
 		assertEquals(200, info.getStatus());
 		assertEquals("application/x-git-upload-pack-advertisement",
 				info.getResponseHeader(HDR_CONTENT_TYPE));
-		assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		if (!enableProtocolV2) {
+			assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		} else {
+			AccessEvent lsRefs = requests.get(n++);
+			assertEquals("POST", lsRefs.getMethod());
+			assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+			assertEquals(0, lsRefs.getParameters().size());
+			assertNotNull("has content-length",
+					lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+			assertNull("not chunked",
+					lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+			assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+			assertEquals(200, lsRefs.getStatus());
+			assertEquals("application/x-git-upload-pack-result",
+					lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+		}
 
 		AccessEvent service = requests.get(n++);
 		assertEquals("POST", service.getMethod());
@@ -756,7 +872,7 @@
 		}
 
 		List<AccessEvent> requests = getRequests();
-		assertEquals(3, requests.size());
+		assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
 
 		AccessEvent info = requests.get(0);
 		assertEquals("GET", info.getMethod());
@@ -766,24 +882,27 @@
 		assertEquals(200, info.getStatus());
 		assertEquals("application/x-git-upload-pack-advertisement",
 				info.getResponseHeader(HDR_CONTENT_TYPE));
-		assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		if (!enableProtocolV2) {
+			assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		}
 
 		AccessEvent redirect = requests.get(1);
 		assertEquals("POST", redirect.getMethod());
 		assertEquals(301, redirect.getStatus());
 
-		AccessEvent service = requests.get(2);
-		assertEquals("POST", service.getMethod());
-		assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
-		assertEquals(0, service.getParameters().size());
-		assertNotNull("has content-length",
-				service.getRequestHeader(HDR_CONTENT_LENGTH));
-		assertNull("not chunked",
-				service.getRequestHeader(HDR_TRANSFER_ENCODING));
-
-		assertEquals(200, service.getStatus());
-		assertEquals("application/x-git-upload-pack-result",
-				service.getResponseHeader(HDR_CONTENT_TYPE));
+		for (int i = 2; i < requests.size(); i++) {
+			AccessEvent service = requests.get(i);
+			assertEquals("POST", service.getMethod());
+			assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
+			assertEquals(0, service.getParameters().size());
+			assertNotNull("has content-length",
+					service.getRequestHeader(HDR_CONTENT_LENGTH));
+			assertNull("not chunked",
+					service.getRequestHeader(HDR_TRANSFER_ENCODING));
+			assertEquals(200, service.getStatus());
+			assertEquals("application/x-git-upload-pack-result",
+					service.getResponseHeader(HDR_CONTENT_TYPE));
+		}
 	}
 
 	@Test
@@ -817,6 +936,35 @@
 		}
 	}
 
+	private void assertFetchRequests(List<AccessEvent> requests, int index) {
+		AccessEvent info = requests.get(index++);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(authURI, "info/refs"), info.getPath());
+		assertEquals(1, info.getParameters().size());
+		assertEquals("git-upload-pack", info.getParameter("service"));
+		assertEquals(200, info.getStatus());
+		assertEquals("application/x-git-upload-pack-advertisement",
+				info.getResponseHeader(HDR_CONTENT_TYPE));
+		if (!enableProtocolV2) {
+			assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		}
+
+		for (int i = index; i < requests.size(); i++) {
+			AccessEvent service = requests.get(i);
+			assertEquals("POST", service.getMethod());
+			assertEquals(join(authURI, "git-upload-pack"), service.getPath());
+			assertEquals(0, service.getParameters().size());
+			assertNotNull("has content-length",
+					service.getRequestHeader(HDR_CONTENT_LENGTH));
+			assertNull("not chunked",
+					service.getRequestHeader(HDR_TRANSFER_ENCODING));
+
+			assertEquals(200, service.getStatus());
+			assertEquals("application/x-git-upload-pack-result",
+					service.getResponseHeader(HDR_CONTENT_TYPE));
+		}
+	}
+
 	@Test
 	public void testInitialClone_WithAuthentication() throws Exception {
 		try (Repository dst = createBareRepository();
@@ -830,34 +978,167 @@
 		}
 
 		List<AccessEvent> requests = getRequests();
-		assertEquals(3, requests.size());
+		assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
 
 		AccessEvent info = requests.get(0);
 		assertEquals("GET", info.getMethod());
 		assertEquals(401, info.getStatus());
 
-		info = requests.get(1);
+		assertFetchRequests(requests, 1);
+	}
+
+	@Test
+	public void testInitialClone_WithPreAuthentication() throws Exception {
+		try (Repository dst = createBareRepository();
+				Transport t = Transport.open(dst, authURI)) {
+			assertFalse(dst.getObjectDatabase().has(A_txt));
+			((TransportHttp) t).setPreemptiveBasicAuthentication(
+					AppServer.username, AppServer.password);
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+			assertTrue(dst.getObjectDatabase().has(A_txt));
+			assertEquals(B, dst.exactRef(master).getObjectId());
+			fsck(dst, B);
+		}
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
+
+		assertFetchRequests(requests, 0);
+	}
+
+	@Test
+	public void testInitialClone_WithPreAuthenticationCleared()
+			throws Exception {
+		try (Repository dst = createBareRepository();
+				Transport t = Transport.open(dst, authURI)) {
+			assertFalse(dst.getObjectDatabase().has(A_txt));
+			((TransportHttp) t).setPreemptiveBasicAuthentication(
+					AppServer.username, AppServer.password);
+			((TransportHttp) t).setPreemptiveBasicAuthentication(null, null);
+			t.setCredentialsProvider(testCredentials);
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+			assertTrue(dst.getObjectDatabase().has(A_txt));
+			assertEquals(B, dst.exactRef(master).getObjectId());
+			fsck(dst, B);
+		}
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
+
+		AccessEvent info = requests.get(0);
 		assertEquals("GET", info.getMethod());
-		assertEquals(join(authURI, "info/refs"), info.getPath());
-		assertEquals(1, info.getParameters().size());
-		assertEquals("git-upload-pack", info.getParameter("service"));
-		assertEquals(200, info.getStatus());
-		assertEquals("application/x-git-upload-pack-advertisement",
-				info.getResponseHeader(HDR_CONTENT_TYPE));
-		assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		assertEquals(401, info.getStatus());
 
-		AccessEvent service = requests.get(2);
-		assertEquals("POST", service.getMethod());
-		assertEquals(join(authURI, "git-upload-pack"), service.getPath());
-		assertEquals(0, service.getParameters().size());
-		assertNotNull("has content-length",
-				service.getRequestHeader(HDR_CONTENT_LENGTH));
-		assertNull("not chunked",
-				service.getRequestHeader(HDR_TRANSFER_ENCODING));
+		assertFetchRequests(requests, 1);
+	}
 
-		assertEquals(200, service.getStatus());
-		assertEquals("application/x-git-upload-pack-result",
-				service.getResponseHeader(HDR_CONTENT_TYPE));
+	@Test
+	public void testInitialClone_PreAuthenticationTooLate() throws Exception {
+		try (Repository dst = createBareRepository();
+				Transport t = Transport.open(dst, authURI)) {
+			assertFalse(dst.getObjectDatabase().has(A_txt));
+			((TransportHttp) t).setPreemptiveBasicAuthentication(
+					AppServer.username, AppServer.password);
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+			assertTrue(dst.getObjectDatabase().has(A_txt));
+			assertEquals(B, dst.exactRef(master).getObjectId());
+			fsck(dst, B);
+			List<AccessEvent> requests = getRequests();
+			assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
+			assertFetchRequests(requests, 0);
+			assertThrows(IllegalStateException.class,
+					() -> ((TransportHttp) t).setPreemptiveBasicAuthentication(
+							AppServer.username, AppServer.password));
+			assertThrows(IllegalStateException.class, () -> ((TransportHttp) t)
+					.setPreemptiveBasicAuthentication(null, null));
+		}
+	}
+
+	@Test
+	public void testInitialClone_WithWrongPreAuthenticationAndCredentialProvider()
+			throws Exception {
+		try (Repository dst = createBareRepository();
+				Transport t = Transport.open(dst, authURI)) {
+			assertFalse(dst.getObjectDatabase().has(A_txt));
+			((TransportHttp) t).setPreemptiveBasicAuthentication(
+					AppServer.username, AppServer.password + 'x');
+			t.setCredentialsProvider(testCredentials);
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+			assertTrue(dst.getObjectDatabase().has(A_txt));
+			assertEquals(B, dst.exactRef(master).getObjectId());
+			fsck(dst, B);
+		}
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(401, info.getStatus());
+
+		assertFetchRequests(requests, 1);
+	}
+
+	@Test
+	public void testInitialClone_WithWrongPreAuthentication() throws Exception {
+		try (Repository dst = createBareRepository();
+				Transport t = Transport.open(dst, authURI)) {
+			assertFalse(dst.getObjectDatabase().has(A_txt));
+			((TransportHttp) t).setPreemptiveBasicAuthentication(
+					AppServer.username, AppServer.password + 'x');
+			TransportException e = assertThrows(TransportException.class,
+					() -> t.fetch(NullProgressMonitor.INSTANCE,
+							mirror(master)));
+			String msg = e.getMessage();
+			assertTrue("Unexpected exception message: " + msg,
+					msg.contains("no CredentialsProvider"));
+		}
+		List<AccessEvent> requests = getRequests();
+		assertEquals(1, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(401, info.getStatus());
+	}
+
+	@Test
+	public void testInitialClone_WithUserInfo() throws Exception {
+		URIish withUserInfo = authURI.setUser(AppServer.username)
+				.setPass(AppServer.password);
+		try (Repository dst = createBareRepository();
+				Transport t = Transport.open(dst, withUserInfo)) {
+			assertFalse(dst.getObjectDatabase().has(A_txt));
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+			assertTrue(dst.getObjectDatabase().has(A_txt));
+			assertEquals(B, dst.exactRef(master).getObjectId());
+			fsck(dst, B);
+		}
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
+
+		assertFetchRequests(requests, 0);
+	}
+
+	@Test
+	public void testInitialClone_PreAuthOverridesUserInfo() throws Exception {
+		URIish withUserInfo = authURI.setUser(AppServer.username)
+				.setPass(AppServer.password + 'x');
+		try (Repository dst = createBareRepository();
+				Transport t = Transport.open(dst, withUserInfo)) {
+			assertFalse(dst.getObjectDatabase().has(A_txt));
+			((TransportHttp) t).setPreemptiveBasicAuthentication(
+					AppServer.username, AppServer.password);
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+			assertTrue(dst.getObjectDatabase().has(A_txt));
+			assertEquals(B, dst.exactRef(master).getObjectId());
+			fsck(dst, B);
+		}
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
+
+		assertFetchRequests(requests, 0);
 	}
 
 	@Test
@@ -937,19 +1218,20 @@
 		}
 
 		List<AccessEvent> requests = getRequests();
-		assertEquals(4, requests.size());
+		assertEquals(enableProtocolV2 ? 5 : 4, requests.size());
 
-		AccessEvent redirect = requests.get(0);
+		int requestNumber = 0;
+		AccessEvent redirect = requests.get(requestNumber++);
 		assertEquals("GET", redirect.getMethod());
 		assertEquals(join(cloneFrom, "info/refs"), redirect.getPath());
 		assertEquals(301, redirect.getStatus());
 
-		AccessEvent info = requests.get(1);
+		AccessEvent info = requests.get(requestNumber++);
 		assertEquals("GET", info.getMethod());
 		assertEquals(join(authURI, "info/refs"), info.getPath());
 		assertEquals(401, info.getStatus());
 
-		info = requests.get(2);
+		info = requests.get(requestNumber++);
 		assertEquals("GET", info.getMethod());
 		assertEquals(join(authURI, "info/refs"), info.getPath());
 		assertEquals(1, info.getParameters().size());
@@ -957,9 +1239,24 @@
 		assertEquals(200, info.getStatus());
 		assertEquals("application/x-git-upload-pack-advertisement",
 				info.getResponseHeader(HDR_CONTENT_TYPE));
-		assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		if (!enableProtocolV2) {
+			assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		} else {
+			AccessEvent lsRefs = requests.get(requestNumber++);
+			assertEquals("POST", lsRefs.getMethod());
+			assertEquals(join(authURI, "git-upload-pack"), lsRefs.getPath());
+			assertEquals(0, lsRefs.getParameters().size());
+			assertNotNull("has content-length",
+					lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+			assertNull("not chunked",
+					lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+			assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+			assertEquals(200, lsRefs.getStatus());
+			assertEquals("application/x-git-upload-pack-result",
+					lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+		}
 
-		AccessEvent service = requests.get(3);
+		AccessEvent service = requests.get(requestNumber);
 		assertEquals("POST", service.getMethod());
 		assertEquals(join(authURI, "git-upload-pack"), service.getPath());
 		assertEquals(0, service.getParameters().size());
@@ -987,7 +1284,7 @@
 		}
 
 		List<AccessEvent> requests = getRequests();
-		assertEquals(3, requests.size());
+		assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
 
 		AccessEvent info = requests.get(0);
 		assertEquals("GET", info.getMethod());
@@ -997,25 +1294,30 @@
 		assertEquals(200, info.getStatus());
 		assertEquals("application/x-git-upload-pack-advertisement",
 				info.getResponseHeader(HDR_CONTENT_TYPE));
-		assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		if (!enableProtocolV2) {
+			assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+		}
 
 		AccessEvent service = requests.get(1);
 		assertEquals("POST", service.getMethod());
 		assertEquals(join(authOnPostURI, "git-upload-pack"), service.getPath());
 		assertEquals(401, service.getStatus());
 
-		service = requests.get(2);
-		assertEquals("POST", service.getMethod());
-		assertEquals(join(authOnPostURI, "git-upload-pack"), service.getPath());
-		assertEquals(0, service.getParameters().size());
-		assertNotNull("has content-length",
-				service.getRequestHeader(HDR_CONTENT_LENGTH));
-		assertNull("not chunked",
-				service.getRequestHeader(HDR_TRANSFER_ENCODING));
+		for (int i = 2; i < requests.size(); i++) {
+			service = requests.get(i);
+			assertEquals("POST", service.getMethod());
+			assertEquals(join(authOnPostURI, "git-upload-pack"),
+					service.getPath());
+			assertEquals(0, service.getParameters().size());
+			assertNotNull("has content-length",
+					service.getRequestHeader(HDR_CONTENT_LENGTH));
+			assertNull("not chunked",
+					service.getRequestHeader(HDR_TRANSFER_ENCODING));
 
-		assertEquals(200, service.getStatus());
-		assertEquals("application/x-git-upload-pack-result",
-				service.getResponseHeader(HDR_CONTENT_TYPE));
+			assertEquals(200, service.getStatus());
+			assertEquals("application/x-git-upload-pack-result",
+					service.getResponseHeader(HDR_CONTENT_TYPE));
+		}
 	}
 
 	@Test
@@ -1052,9 +1354,11 @@
 
 		List<AccessEvent> requests = getRequests();
 		requests.removeAll(cloneRequests);
-		assertEquals(2, requests.size());
 
-		AccessEvent info = requests.get(0);
+		assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
+
+		int requestNumber = 0;
+		AccessEvent info = requests.get(requestNumber++);
 		assertEquals("GET", info.getMethod());
 		assertEquals(join(remoteURI, "info/refs"), info.getPath());
 		assertEquals(1, info.getParameters().size());
@@ -1063,9 +1367,24 @@
 		assertEquals("application/x-git-upload-pack-advertisement",
 				info.getResponseHeader(HDR_CONTENT_TYPE));
 
+		if (enableProtocolV2) {
+			AccessEvent lsRefs = requests.get(requestNumber++);
+			assertEquals("POST", lsRefs.getMethod());
+			assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+			assertEquals(0, lsRefs.getParameters().size());
+			assertNotNull("has content-length",
+					lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+			assertNull("not chunked",
+					lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+			assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+			assertEquals(200, lsRefs.getStatus());
+			assertEquals("application/x-git-upload-pack-result",
+					lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+		}
+
 		// We should have needed one request to perform the fetch.
 		//
-		AccessEvent service = requests.get(1);
+		AccessEvent service = requests.get(requestNumber);
 		assertEquals("POST", service.getMethod());
 		assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
 		assertEquals(0, service.getParameters().size());
@@ -1116,9 +1435,10 @@
 
 		List<AccessEvent> requests = getRequests();
 		requests.removeAll(cloneRequests);
-		assertEquals(3, requests.size());
+		assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
 
-		AccessEvent info = requests.get(0);
+		int requestNumber = 0;
+		AccessEvent info = requests.get(requestNumber++);
 		assertEquals("GET", info.getMethod());
 		assertEquals(join(remoteURI, "info/refs"), info.getPath());
 		assertEquals(1, info.getParameters().size());
@@ -1127,10 +1447,25 @@
 		assertEquals("application/x-git-upload-pack-advertisement", info
 				.getResponseHeader(HDR_CONTENT_TYPE));
 
+		if (enableProtocolV2) {
+			AccessEvent lsRefs = requests.get(requestNumber++);
+			assertEquals("POST", lsRefs.getMethod());
+			assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+			assertEquals(0, lsRefs.getParameters().size());
+			assertNotNull("has content-length",
+					lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+			assertNull("not chunked",
+					lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+			assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+			assertEquals(200, lsRefs.getStatus());
+			assertEquals("application/x-git-upload-pack-result",
+					lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+		}
+
 		// We should have needed two requests to perform the fetch
 		// due to the high number of local unknown commits.
 		//
-		AccessEvent service = requests.get(1);
+		AccessEvent service = requests.get(requestNumber++);
 		assertEquals("POST", service.getMethod());
 		assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
 		assertEquals(0, service.getParameters().size());
@@ -1143,7 +1478,7 @@
 		assertEquals("application/x-git-upload-pack-result", service
 				.getResponseHeader(HDR_CONTENT_TYPE));
 
-		service = requests.get(2);
+		service = requests.get(requestNumber);
 		assertEquals("POST", service.getMethod());
 		assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
 		assertEquals(0, service.getParameters().size());
@@ -1158,6 +1493,64 @@
 	}
 
 	@Test
+	public void testFetch_MaxHavesCutoffAfterAckOnly() throws Exception {
+		// Bootstrap by doing the clone.
+		//
+		TestRepository dst = createTestRepository();
+		try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+		}
+		assertEquals(B, dst.getRepository().exactRef(master).getObjectId());
+
+		// Force enough into the local client that enumeration will
+		// need more than MAX_HAVES (256) haves to be sent. The server
+		// doesn't know any of these, so it will never ACK. The client
+		// should keep going.
+		//
+		// If it does, client and server will find a common commit, and
+		// the pack file will contain exactly the one commit object Z
+		// we create on the remote, which we can test for via the progress
+		// monitor, which should have something like
+		// "Receiving objects: 100% (1/1)". If the client sends a "done"
+		// too early, the server will send more objects, and we'll have
+		// a line like "Receiving objects: 100% (8/8)".
+		TestRepository.BranchBuilder b = dst.branch(master);
+		// The client will send 32 + 64 + 128 + 256 + 512 haves. Only the
+		// last one will be a common commit. If the cutoff kicks in too
+		// early (after 480), we'll get too many objects in the fetch.
+		for (int i = 0; i < 992; i++)
+			b.commit().tick(3600 /* 1 hour */).message("c" + i).create();
+
+		// Create a new commit on the remote.
+		//
+		RevCommit Z;
+		try (TestRepository<Repository> tr = new TestRepository<>(
+				remoteRepository)) {
+			b = tr.branch(master);
+			Z = b.commit().message("Z").create();
+		}
+
+		// Now incrementally update.
+		//
+		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+		Writer writer = new OutputStreamWriter(buffer, StandardCharsets.UTF_8);
+		TextProgressMonitor monitor = new TextProgressMonitor(writer);
+		try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
+			t.fetch(monitor, mirror(master));
+		}
+		assertEquals(Z, dst.getRepository().exactRef(master).getObjectId());
+
+		String progressMessages = new String(buffer.toByteArray(),
+				StandardCharsets.UTF_8);
+		Pattern expected = Pattern
+				.compile("Receiving objects:\\s+100% \\(1/1\\)\n");
+		if (!expected.matcher(progressMessages).find()) {
+			System.out.println(progressMessages);
+			fail("Expected only one object to be sent");
+		}
+	}
+
+	@Test
 	public void testInitialClone_BrokenServer() throws Exception {
 		try (Repository dst = createBareRepository();
 				Transport t = Transport.open(dst, brokenURI)) {
@@ -1211,7 +1604,8 @@
 					Collections.<ObjectId> emptySet());
 			fail("Server accepted want " + id.name());
 		} catch (TransportException err) {
-			assertEquals("want " + id.name() + " not valid", err.getMessage());
+			assertTrue(err.getMessage()
+					.contains("want " + id.name() + " not valid"));
 		}
 	}
 
@@ -1224,6 +1618,8 @@
 					new DfsRepositoryDescription(repoName));
 			final TestRepository<Repository> repo = new TestRepository<>(
 					badRefsRepo);
+			badRefsRepo.getConfig().setInt("protocol", null, "version",
+					enableProtocolV2 ? 2 : 0);
 
 			ServletContextHandler app = noRefServer.addContext("/git");
 			GitServlet gs = new GitServlet();
@@ -1253,7 +1649,8 @@
 						Collections.<ObjectId> emptySet());
 				fail("Successfully served ref with value " + c.getRef(master));
 			} catch (TransportException err) {
-				assertEquals("Internal server error", err.getMessage());
+				assertTrue("Unexpected exception message " + err.getMessage(),
+						err.getMessage().contains("Internal server error"));
 			}
 		} finally {
 			noRefServer.tearDown();
@@ -1429,5 +1826,4 @@
 		cfg.setBoolean("http", null, "receivepack", true);
 		cfg.save();
 	}
-
 }
diff --git a/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
index 311cd0b..b6c2cb5 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
@@ -22,16 +22,16 @@
  org.eclipse.jetty.util.log;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.util.security;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.util.ssl;version="[9.4.5,10.0.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.http.server;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.resolver;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.http.server;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.resolver;version="[5.11.2,5.12.0)",
  org.junit;version="[4.13,5.0.0)"
-Export-Package: org.eclipse.jgit.junit.http;version="5.10.1";
+Export-Package: org.eclipse.jgit.junit.http;version="5.11.2";
   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 de59edd..199e982 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit.http;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit.http;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit.http/pom.xml b/org.eclipse.jgit.junit.http/pom.xml
index 25e1c70..eb008b0 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.http</artifactId>
diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
index 4e27a3d..f9f8c85 100644
--- a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
+++ b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
@@ -23,8 +23,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 
 import org.eclipse.jetty.http.HttpVersion;
 import org.eclipse.jetty.security.AbstractLoginService;
@@ -259,7 +259,7 @@
 	static class TestMappedLoginService extends AbstractLoginService {
 		private String role;
 
-		protected final ConcurrentMap<String, UserPrincipal> users = new ConcurrentHashMap<>();
+		protected final Map<String, UserPrincipal> users = new ConcurrentHashMap<>();
 
 		TestMappedLoginService(String role) {
 			this.role = role;
diff --git a/org.eclipse.jgit.junit.ssh/.settings/edu.umd.cs.findbugs.core.prefs b/org.eclipse.jgit.junit.ssh/.settings/edu.umd.cs.findbugs.core.prefs
new file mode 100644
index 0000000..2792ea0
--- /dev/null
+++ b/org.eclipse.jgit.junit.ssh/.settings/edu.umd.cs.findbugs.core.prefs
@@ -0,0 +1,145 @@
+#SpotBugs User Preferences
+#Fri Dec 04 11:26:04 CET 2020
+detectorExplicitSerialization=ExplicitSerialization|true
+detectorMultithreadedInstanceAccess=MultithreadedInstanceAccess|true
+detectorConfusionBetweenInheritedAndOuterMethod=ConfusionBetweenInheritedAndOuterMethod|true
+detectorWrongMapIterator=WrongMapIterator|true
+detectorUnnecessaryMath=UnnecessaryMath|true
+detectorUselessSubclassMethod=UselessSubclassMethod|false
+filter_settings=Medium|BAD_PRACTICE,CORRECTNESS,EXPERIMENTAL,I18N,MALICIOUS_CODE,MT_CORRECTNESS,PERFORMANCE,SECURITY,STYLE|false|15
+detectorURLProblems=URLProblems|true
+detectorIteratorIdioms=IteratorIdioms|true
+detectorMutableEnum=MutableEnum|true
+detectorFindNonShortCircuit=FindNonShortCircuit|true
+detectorSynchronizeAndNullCheckField=SynchronizeAndNullCheckField|true
+detectorVolatileUsage=VolatileUsage|true
+detectorFindNakedNotify=FindNakedNotify|true
+detectorFindUninitializedGet=FindUninitializedGet|true
+detectorFindUseOfNonSerializableValue=FindUseOfNonSerializableValue|true
+detectorFindJSR166LockMonitorenter=FindJSR166LockMonitorenter|true
+detectorQuestionableBooleanAssignment=QuestionableBooleanAssignment|true
+detectorSwitchFallthrough=SwitchFallthrough|true
+detectorFindLocalSelfAssignment2=FindLocalSelfAssignment2|true
+detectorConfusedInheritance=ConfusedInheritance|true
+detectorSynchronizationOnSharedBuiltinConstant=SynchronizationOnSharedBuiltinConstant|true
+detectorMutableStaticFields=MutableStaticFields|true
+detectorInvalidJUnitTest=InvalidJUnitTest|true
+detectorInfiniteLoop=InfiniteLoop|true
+detectorFindRunInvocations=FindRunInvocations|true
+detectorBadSyntaxForRegularExpression=BadSyntaxForRegularExpression|true
+detectorXMLFactoryBypass=XMLFactoryBypass|true
+detectorFindOpenStream=FindOpenStream|true
+detectorCheckExpectedWarnings=CheckExpectedWarnings|false
+detectorHugeSharedStringConstants=HugeSharedStringConstants|true
+detectorLostLoggerDueToWeakReference=LostLoggerDueToWeakReference|true
+detectorStringConcatenation=StringConcatenation|true
+detectorLoadOfKnownNullValue=LoadOfKnownNullValue|true
+detectorFinalizerNullsFields=FinalizerNullsFields|true
+detectorFindFieldSelfAssignment=FindFieldSelfAssignment|true
+detectorInefficientToArray=InefficientToArray|false
+detectorDontCatchIllegalMonitorStateException=DontCatchIllegalMonitorStateException|true
+detectorInconsistentAnnotations=InconsistentAnnotations|true
+detectorBadlyOverriddenAdapter=BadlyOverriddenAdapter|true
+detectorInstantiateStaticClass=InstantiateStaticClass|true
+detectorCheckRelaxingNullnessAnnotation=CheckRelaxingNullnessAnnotation|true
+detectorMethodReturnCheck=MethodReturnCheck|true
+detectorEqualsOperandShouldHaveClassCompatibleWithThis=EqualsOperandShouldHaveClassCompatibleWithThis|true
+detectorFindDoubleCheck=FindDoubleCheck|true
+detectorFindBadForLoop=FindBadForLoop|true
+detectorDefaultEncodingDetector=DefaultEncodingDetector|true
+detectorFindInconsistentSync2=FindInconsistentSync2|true
+detectorFindSpinLoop=FindSpinLoop|true
+detectorFindMaskedFields=FindMaskedFields|true
+detectorBooleanReturnNull=BooleanReturnNull|true
+detectorFindUnsyncGet=FindUnsyncGet|true
+detectorCrossSiteScripting=CrossSiteScripting|true
+detectorDroppedException=DroppedException|true
+detectorFindDeadLocalStores=FindDeadLocalStores|true
+detectorCheckImmutableAnnotation=CheckImmutableAnnotation|true
+detectorInfiniteRecursiveLoop=InfiniteRecursiveLoop|true
+detectorFindRefComparison=FindRefComparison|true
+detectorFindRoughConstants=FindRoughConstants|true
+detectorMutableLock=MutableLock|true
+detectorFindNullDeref=FindNullDeref|true
+detectorFindReturnRef=FindReturnRef|true
+detectorSynchronizeOnClassLiteralNotGetClass=SynchronizeOnClassLiteralNotGetClass|true
+detectorFindUselessControlFlow=FindUselessControlFlow|true
+detectorOverridingEqualsNotSymmetrical=OverridingEqualsNotSymmetrical|true
+detectorIDivResultCastToDouble=IDivResultCastToDouble|true
+detectorReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass=ReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass|true
+detectorFindSelfComparison=FindSelfComparison|true
+detectorFindFloatEquality=FindFloatEquality|true
+detectorFindComparatorProblems=FindComparatorProblems|true
+detectorRepeatedConditionals=RepeatedConditionals|true
+filter_settings_neg=NOISE|
+detectorInefficientMemberAccess=InefficientMemberAccess|false
+detectorFindUncalledPrivateMethods=FindUncalledPrivateMethods|true
+detectorNumberConstructor=NumberConstructor|true
+detectorDontAssertInstanceofInTests=DontAssertInstanceofInTests|true
+detectorFindFinalizeInvocations=FindFinalizeInvocations|true
+detectorFindNullDerefsInvolvingNonShortCircuitEvaluation=FindNullDerefsInvolvingNonShortCircuitEvaluation|true
+detectorDontIgnoreResultOfPutIfAbsent=DontIgnoreResultOfPutIfAbsent|true
+detectorFindUnconditionalWait=FindUnconditionalWait|true
+detectorFindTwoLockWait=FindTwoLockWait|true
+detectorFindSleepWithLockHeld=FindSleepWithLockHeld|true
+detectorFindUnreleasedLock=FindUnreleasedLock|true
+detectorInefficientIndexOf=InefficientIndexOf|false
+detectorDoInsideDoPrivileged=DoInsideDoPrivileged|true
+detectorFindEmptySynchronizedBlock=FindEmptySynchronizedBlock|true
+detectorOverridingMethodsMustInvokeSuperDetector=OverridingMethodsMustInvokeSuperDetector|true
+detectorWaitInLoop=WaitInLoop|true
+detectorIntCast2LongAsInstant=IntCast2LongAsInstant|true
+detectorBadUseOfReturnValue=BadUseOfReturnValue|true
+detectorFindSqlInjection=FindSqlInjection|true
+detectorUnreadFields=UnreadFields|true
+detectorSynchronizingOnContentsOfFieldToProtectField=SynchronizingOnContentsOfFieldToProtectField|true
+detectorFindUselessObjects=FindUselessObjects|true
+detectorBadAppletConstructor=BadAppletConstructor|false
+detectorInheritanceUnsafeGetResource=InheritanceUnsafeGetResource|true
+detectorSerializableIdiom=SerializableIdiom|true
+detectorNaming=Naming|true
+detectorNoteUnconditionalParamDerefs=NoteUnconditionalParamDerefs|true
+detectorFormatStringChecker=FormatStringChecker|true
+detectorSuspiciousThreadInterrupted=SuspiciousThreadInterrupted|true
+detectorEmptyZipFileEntry=EmptyZipFileEntry|false
+detectorFindCircularDependencies=FindCircularDependencies|false
+detectorPreferZeroLengthArrays=PreferZeroLengthArrays|true
+detectorAtomicityProblem=AtomicityProblem|true
+detectorRuntimeExceptionCapture=RuntimeExceptionCapture|true
+detectorInitializationChain=InitializationChain|true
+detectorInitializeNonnullFieldsInConstructor=InitializeNonnullFieldsInConstructor|true
+detectorOptionalReturnNull=OptionalReturnNull|true
+detectorStartInConstructor=StartInConstructor|true
+detectorFindUnsatisfiedObligation=FindUnsatisfiedObligation|true
+detectorRedundantConditions=RedundantConditions|true
+effort=default
+detectorRedundantInterfaces=RedundantInterfaces|true
+detectorDuplicateBranches=DuplicateBranches|true
+detectorCheckTypeQualifiers=CheckTypeQualifiers|true
+detectorComparatorIdiom=ComparatorIdiom|true
+detectorFindBadCast2=FindBadCast2|true
+detectorFindMismatchedWaitOrNotify=FindMismatchedWaitOrNotify|true
+excludefilter0=findBugs/FindBugsExcludeFilter.xml|true
+detectorBadResultSetAccess=BadResultSetAccess|true
+detectorIncompatMask=IncompatMask|true
+detectorCovariantArrayAssignment=CovariantArrayAssignment|false
+detectorDumbMethodInvocations=DumbMethodInvocations|true
+run_at_full_build=false
+detectorStaticCalendarDetector=StaticCalendarDetector|true
+detectorUncallableMethodOfAnonymousClass=UncallableMethodOfAnonymousClass|true
+detectorVarArgsProblems=VarArgsProblems|true
+detectorInefficientInitializationInsideLoop=InefficientInitializationInsideLoop|false
+detectorCloneIdiom=CloneIdiom|true
+detectorFindHEmismatch=FindHEmismatch|true
+detectorAppendingToAnObjectOutputStream=AppendingToAnObjectOutputStream|true
+detectorFindSelfComparison2=FindSelfComparison2|true
+detectorLazyInit=LazyInit|true
+detectorFindUnrelatedTypesInGenericContainer=FindUnrelatedTypesInGenericContainer|true
+detectorDontUseEnum=DontUseEnum|true
+detectorFindPuzzlers=FindPuzzlers|true
+detectorCallToUnsupportedMethod=CallToUnsupportedMethod|false
+detectorSuperfluousInstanceOf=SuperfluousInstanceOf|true
+detectorReadReturnShouldBeChecked=ReadReturnShouldBeChecked|true
+detector_threshold=2
+detectorPublicSemaphores=PublicSemaphores|false
+detectorDumbMethods=DumbMethods|true
diff --git a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
index c5a3d06..13aa093 100644
--- a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
@@ -3,43 +3,46 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.junit.ssh
 Bundle-SymbolicName: org.eclipse.jgit.junit.ssh
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.apache.sshd.common;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.config.keys;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.file.virtualfs;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.helpers;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.io;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.kex;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.keyprovider;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.session;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.buffer;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.logging;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.security;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.threads;version="[2.4.0,2.5.0)",
- org.apache.sshd.server;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.auth;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.auth.gss;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.auth.keyboard;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.auth.password;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.command;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.session;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.shell;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.subsystem;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.subsystem.sftp;version="[2.4.0,2.5.0)",
- org.eclipse.jgit.annotations;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+Import-Package: org.apache.sshd.common;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.config.keys;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.file.virtualfs;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.helpers;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.io;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.kex;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.keyprovider;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.session;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.signature;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.buffer;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.logging;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.security;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.threads;version="[2.6.0,2.7.0)",
+ org.apache.sshd.core;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.auth;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.auth.gss;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.auth.keyboard;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.auth.password;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.command;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.session;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.shell;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.subsystem;version="[2.6.0,2.7.0)",
+ org.apache.sshd.sftp;version="[2.6.0,2.7.0)",
+ org.apache.sshd.sftp.server;version="[2.6.0,2.7.0)",
+ org.eclipse.jgit.annotations;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.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="5.10.1"
+Export-Package: org.eclipse.jgit.junit.ssh;version="5.11.2"
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 44d2bcc..b2d2d89 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit.ssh;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit.ssh;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit.ssh/findBugs/FindBugsExcludeFilter.xml b/org.eclipse.jgit.junit.ssh/findBugs/FindBugsExcludeFilter.xml
new file mode 100644
index 0000000..999cb71
--- /dev/null
+++ b/org.eclipse.jgit.junit.ssh/findBugs/FindBugsExcludeFilter.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<FindBugsFilter>
+     <!-- Silence returning null for Boolean return type -->
+     <Match>
+       <Class name="org.eclipse.jgit.junit.ssh.SshTestGitServer$FakeUserAuthGSS" />
+       <Method name="doAuth" />
+       <Bug pattern="NP_BOOLEAN_RETURN_NULL" />
+     </Match>
+</FindBugsFilter>
diff --git a/org.eclipse.jgit.junit.ssh/pom.xml b/org.eclipse.jgit.junit.ssh/pom.xml
index a0e35aa..a1fd542 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.ssh</artifactId>
diff --git a/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshBasicTestBase.java b/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshBasicTestBase.java
new file mode 100644
index 0000000..f9ca0b8
--- /dev/null
+++ b/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshBasicTestBase.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2020 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.junit.ssh;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.eclipse.jgit.api.Git;
+import org.junit.Test;
+
+/**
+ * Some minimal cloning and fetching tests. Concrete subclasses can implement
+ * the abstract operations from {@link SshTestHarness} to run with different SSH
+ * implementations.
+ */
+public abstract class SshBasicTestBase extends SshTestHarness {
+
+	protected File defaultCloneDir;
+
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		defaultCloneDir = new File(getTemporaryDirectory(), "cloned");
+	}
+
+	@Test
+	public void testSshCloneWithConfig() throws Exception {
+		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath());
+	}
+
+	@Test
+	public void testSshFetchWithConfig() throws Exception {
+		File localClone = cloneWith("ssh://localhost/doesntmatter",
+				defaultCloneDir, null, //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath());
+		// Do a commit in the upstream repo
+		try (Git git = new Git(db)) {
+			writeTrashFile("SomeOtherFile.txt", "Other commit");
+			git.add().addFilepattern("SomeOtherFile.txt").call();
+			git.commit().setMessage("New commit").call();
+		}
+		// Pull in the clone
+		try (Git git = Git.open(localClone)) {
+			File f = new File(git.getRepository().getWorkTree(),
+					"SomeOtherFile.txt");
+			assertFalse(f.exists());
+			git.pull().setRemote("origin").call();
+			assertTrue(f.exists());
+			assertEquals("Other commit", read(f));
+		}
+	}
+}
diff --git a/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestBase.java b/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestBase.java
index 3784741..6fa82f1 100644
--- a/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestBase.java
+++ b/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestBase.java
@@ -40,7 +40,7 @@
  * abstract operations from {@link SshTestHarness}. This gives a way to test
  * different ssh clients against a unified test suite.
  */
-public abstract class SshTestBase extends SshTestHarness {
+public abstract class SshTestBase extends SshBasicTestBase {
 
 	@DataPoints
 	public static String[] KEY_RESOURCES = { //
@@ -65,14 +65,6 @@
 			"id_ed25519_testpass", //
 			"id_ed25519_expensive_testpass" };
 
-	protected File defaultCloneDir;
-
-	@Override
-	public void setUp() throws Exception {
-		super.setUp();
-		defaultCloneDir = new File(getTemporaryDirectory(), "cloned");
-	}
-
 	@Test
 	public void testSshWithoutConfig() throws Exception {
 		assertThrows(TransportException.class,
@@ -133,16 +125,6 @@
 	}
 
 	@Test
-	public void testSshWithConfig() throws Exception {
-		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
-				"Host localhost", //
-				"HostName localhost", //
-				"Port " + testPort, //
-				"User " + TEST_USER, //
-				"IdentityFile " + privateKey1.getAbsolutePath());
-	}
-
-	@Test
 	public void testSshWithConfigEncryptedUnusedKey() throws Exception {
 		// Copy the encrypted test key from the bundle.
 		File encryptedKey = new File(sshDir, "id_dsa");
diff --git a/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java b/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java
index ab8e0c1..4fe98f8 100644
--- a/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java
+++ b/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java
@@ -9,6 +9,9 @@
  */
 package org.eclipse.jgit.junit.ssh;
 
+import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENTIFICATION_LINES;
+import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENT_LINES_SEPARATOR;
+
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -21,26 +24,28 @@
 import java.security.PublicKey;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.PropertyResolver;
-import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
 import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
-import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.security.SecurityUtils;
 import org.apache.sshd.common.util.threads.CloseableExecutorService;
 import org.apache.sshd.common.util.threads.ThreadUtils;
 import org.apache.sshd.server.ServerAuthenticationManager;
-import org.apache.sshd.server.ServerFactoryManager;
+import org.apache.sshd.server.ServerBuilder;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.auth.UserAuth;
 import org.apache.sshd.server.auth.UserAuthFactory;
@@ -52,8 +57,9 @@
 import org.apache.sshd.server.session.ServerSession;
 import org.apache.sshd.server.shell.UnknownCommand;
 import org.apache.sshd.server.subsystem.SubsystemFactory;
-import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.sftp.server.SftpSubsystemFactory;
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.RemoteConfig;
@@ -161,7 +167,9 @@
 		this.testUser = testUser;
 		setTestUserPublicKey(testKey);
 		this.repository = repository;
-		server = SshServer.setUpDefaultServer();
+		ServerBuilder builder = ServerBuilder.builder()
+				.signatureFactories(getSignatureFactories());
+		server = builder.build();
 		hostKeys.add(hostKey);
 		server.setKeyPairProvider((session) -> hostKeys);
 
@@ -186,6 +194,37 @@
 		});
 	}
 
+	/**
+	 * Apache MINA sshd 2.6.0 has removed DSA, DSA_CERT and RSA_CERT. We have to
+	 * set it up explicitly to still allow users to connect with DSA keys.
+	 *
+	 * @return a list of supported signature factories
+	 */
+	@SuppressWarnings("deprecation")
+	private static List<NamedFactory<Signature>> getSignatureFactories() {
+		// @formatter:off
+		return Arrays.asList(
+                BuiltinSignatures.nistp256_cert,
+                BuiltinSignatures.nistp384_cert,
+                BuiltinSignatures.nistp521_cert,
+                BuiltinSignatures.ed25519_cert,
+                BuiltinSignatures.rsaSHA512_cert,
+                BuiltinSignatures.rsaSHA256_cert,
+                BuiltinSignatures.rsa_cert,
+                BuiltinSignatures.nistp256,
+                BuiltinSignatures.nistp384,
+                BuiltinSignatures.nistp521,
+                BuiltinSignatures.ed25519,
+                BuiltinSignatures.sk_ecdsa_sha2_nistp256,
+                BuiltinSignatures.sk_ssh_ed25519,
+                BuiltinSignatures.rsaSHA512,
+                BuiltinSignatures.rsaSHA256,
+                BuiltinSignatures.rsa,
+                BuiltinSignatures.dsa_cert,
+                BuiltinSignatures.dsa);
+		// @formatter:on
+	}
+
 	private static PublicKey readPublicKey(Path key)
 			throws IOException, GeneralSecurityException {
 		return AuthorizedKeyEntry.readAuthorizedKeys(key).get(0)
@@ -202,7 +241,7 @@
 
 	private static class FakeUserAuthGSS extends UserAuthGSS {
 		@Override
-		protected Boolean doAuth(Buffer buffer, boolean initial)
+		protected @Nullable Boolean doAuth(Buffer buffer, boolean initial)
 				throws Exception {
 			// We always reply that we did do this, but then we fail at the
 			// first token message. That way we can test that the client-side
@@ -277,14 +316,8 @@
 	@NonNull
 	protected List<SubsystemFactory> configureSubsystems() {
 		// SFTP.
-		server.setFileSystemFactory(new VirtualFileSystemFactory() {
-
-			@Override
-			protected Path computeRootDir(Session session) throws IOException {
-				return SshTestGitServer.this.repository.getDirectory()
-						.getParentFile().getAbsoluteFile().toPath();
-			}
-		});
+		server.setFileSystemFactory(new VirtualFileSystemFactory(repository
+				.getDirectory().getParentFile().getAbsoluteFile().toPath()));
 		return Collections
 				.singletonList((new SftpSubsystemFactory.Builder()).build());
 	}
@@ -433,9 +466,8 @@
 	 */
 	public void setPreamble(String... lines) {
 		if (lines != null && lines.length > 0) {
-			PropertyResolverUtils.updateProperty(this.server,
-					ServerFactoryManager.SERVER_EXTRA_IDENTIFICATION_LINES,
-					String.join("|", lines));
+			SERVER_EXTRA_IDENTIFICATION_LINES.set(server, String.join(
+					String.valueOf(SERVER_EXTRA_IDENT_LINES_SEPARATOR), lines));
 		}
 	}
 
diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
index 75baf2c..f2f2e16 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.annotations;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.dircache;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.pack;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.merge;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="5.10.1",
- org.eclipse.jgit.treewalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk.filter;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util.io;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util.time;version="[5.10.1,5.11.0)",
+Import-Package: org.eclipse.jgit.annotations;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.dircache;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.merge;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="5.11.2",
+ org.eclipse.jgit.treewalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk.filter;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util.io;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util.time;version="[5.11.2,5.12.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="5.10.1";
+Export-Package: org.eclipse.jgit.junit;version="5.11.2";
   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="5.10.1";uses:="org.eclipse.jgit.util.time"
+ org.eclipse.jgit.junit.time;version="5.11.2";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 7a2c896..b358c38 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit/pom.xml b/org.eclipse.jgit.junit/pom.xml
index ae026fc..eb65596 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit</artifactId>
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java
index b982787..4a4dc92 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/SeparateClassloaderTestRunner.java
@@ -9,10 +9,10 @@
  */
 package org.eclipse.jgit.junit;
 
-import static java.lang.ClassLoader.getSystemClassLoader;
-
+import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.Paths;
 
 import org.junit.runners.BlockJUnit4ClassRunner;
 import org.junit.runners.model.InitializationError;
@@ -40,7 +40,13 @@
 	private static Class<?> loadNewClass(Class<?> klass)
 			throws InitializationError {
 		try {
-			URL[] urls = ((URLClassLoader) getSystemClassLoader()).getURLs();
+			String pathSeparator = System.getProperty("path.separator");
+			String[] classPathEntries = System.getProperty("java.class.path")
+					.split(pathSeparator);
+			URL[] urls = new URL[classPathEntries.length];
+			for (int i = 0; i < classPathEntries.length; i++) {
+				urls[i] = Paths.get(classPathEntries[i]).toUri().toURL();
+			}
 			ClassLoader testClassLoader = new URLClassLoader(urls) {
 
 				@Override
@@ -54,7 +60,7 @@
 				}
 			};
 			return Class.forName(klass.getName(), true, testClassLoader);
-		} catch (ClassNotFoundException e) {
+		} catch (ClassNotFoundException | MalformedURLException e) {
 			throw new InitializationError(e);
 		}
 	}
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
index a5b3b1f..0232156 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
@@ -43,8 +43,10 @@
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.internal.storage.file.ObjectDirectory;
+import org.eclipse.jgit.internal.storage.file.Pack;
 import org.eclipse.jgit.internal.storage.file.PackFile;
 import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -773,7 +775,7 @@
 			rw.writeInfoRefs();
 
 			final StringBuilder w = new StringBuilder();
-			for (PackFile p : fr.getObjectDatabase().getPacks()) {
+			for (Pack p : fr.getObjectDatabase().getPacks()) {
 				w.append("P ");
 				w.append(p.getPackFile().getName());
 				w.append('\n');
@@ -906,23 +908,22 @@
 			ObjectDirectory odb = (ObjectDirectory) db.getObjectDatabase();
 			NullProgressMonitor m = NullProgressMonitor.INSTANCE;
 
-			final File pack, idx;
+			final PackFile pack, idx;
 			try (PackWriter pw = new PackWriter(db)) {
 				Set<ObjectId> all = new HashSet<>();
 				for (Ref r : db.getRefDatabase().getRefs())
 					all.add(r.getObjectId());
 				pw.preparePack(m, all, PackWriter.NONE);
 
-				final ObjectId name = pw.computeName();
-
-				pack = nameFor(odb, name, ".pack");
+				pack = new PackFile(odb.getPackDirectory(), pw.computeName(),
+						PackExt.PACK);
 				try (OutputStream out =
 						new BufferedOutputStream(new FileOutputStream(pack))) {
 					pw.writePack(m, m, out);
 				}
 				pack.setReadOnly();
 
-				idx = nameFor(odb, name, ".idx");
+				idx = pack.create(PackExt.INDEX);
 				try (OutputStream out =
 						new BufferedOutputStream(new FileOutputStream(idx))) {
 					pw.writeIndex(out);
@@ -954,17 +955,12 @@
 	}
 
 	private static void prunePacked(ObjectDirectory odb) throws IOException {
-		for (PackFile p : odb.getPacks()) {
+		for (Pack p : odb.getPacks()) {
 			for (MutableEntry e : p)
 				FileUtils.delete(odb.fileFor(e.toObjectId()));
 		}
 	}
 
-	private static File nameFor(ObjectDirectory odb, ObjectId name, String t) {
-		File packdir = odb.getPackDirectory();
-		return new File(packdir, "pack-" + name.name() + t);
-	}
-
 	private void writeFile(File p, byte[] bin) throws IOException,
 			ObjectWritingException {
 		final LockFile lck = new LockFile(p);
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 4f443e0..d757e45 100644
--- a/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
@@ -3,17 +3,17 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.server.test
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server.test
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: javax.servlet;version="[3.1.0,4.0.0)",
  javax.servlet.http;version="[3.1.0,4.0.0)",
  org.apache.http;version="[4.3.0,5.0.0)",
- org.apache.http.client;version="[4.3.0,5.0.0)",
- org.apache.http.client.methods;version="[4.3.0,5.0.0)",
+ org.apache.http.client;version="[4.4.0,5.0.0)",
+ org.apache.http.client.methods;version="[4.4.0,5.0.0)",
  org.apache.http.entity;version="[4.3.0,5.0.0)",
- org.apache.http.impl.client;version="[4.3.0,5.0.0)",
+ org.apache.http.impl.client;version="[4.4.0,5.0.0)",
  org.eclipse.jetty.continuation;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.http;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.io;version="[9.4.5,10.0.0)",
@@ -28,24 +28,24 @@
  org.eclipse.jetty.util.log;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.util.security;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.util.thread;version="[9.4.5,10.0.0)",
- org.eclipse.jgit.api;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit.http;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.server;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.server.fs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.test;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk.filter;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit.http;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.server;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.test;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk.filter;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
  org.hamcrest.core;version="[1.1.0,2.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 193cd8d..4d6a76a 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 b5e6135..0545f2d 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.lfs.server;version="5.10.1";
+Export-Package: org.eclipse.jgit.lfs.server;version="5.11.2";
   uses:="javax.servlet.http,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.fs;version="5.10.1";
+ org.eclipse.jgit.lfs.server.fs;version="5.11.2";
   uses:="javax.servlet,
    javax.servlet.http,
    org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.internal;version="5.10.1";x-internal:=true,
- org.eclipse.jgit.lfs.server.s3;version="5.10.1";
+ org.eclipse.jgit.lfs.server.internal;version="5.11.2";x-internal:=true,
+ org.eclipse.jgit.lfs.server.s3;version="5.11.2";
   uses:="org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib"
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
@@ -24,16 +24,15 @@
  javax.servlet.annotation;version="[3.1.0,4.0.0)",
  javax.servlet.http;version="[3.1.0,4.0.0)",
  org.apache.http;version="[4.3.0,5.0.0)",
- org.apache.http.client;version="[4.3.0,5.0.0)",
- org.eclipse.jgit.annotations;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.internal;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.http;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.http.apache;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.annotations;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.internal;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.http;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.http.apache;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.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 d0f1020..7641f4a 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.lfs.server;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.lfs.server;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.lfs.server/findBugs/FindBugsExcludeFilter.xml b/org.eclipse.jgit.lfs.server/findBugs/FindBugsExcludeFilter.xml
new file mode 100644
index 0000000..c5f3f80
--- /dev/null
+++ b/org.eclipse.jgit.lfs.server/findBugs/FindBugsExcludeFilter.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<FindBugsFilter>
+<!-- Field is written by gson, seems like spotbugs doesn't recognize this -->
+     <Match>
+       <Class name="org.eclipse.jgit.lfs.server.LfsObject" />
+       <Bug pattern="UWF_UNWRITTEN_FIELD" />
+     </Match>
+</FindBugsFilter>
diff --git a/org.eclipse.jgit.lfs.server/pom.xml b/org.eclipse.jgit.lfs.server/pom.xml
index 008b4f2..f908bf5 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 c9877bd..df90846 100644
--- a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
@@ -3,22 +3,24 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.test
 Bundle-SymbolicName: org.eclipse.jgit.lfs.test
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.internal.storage.dfs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk.filter;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+Import-Package: org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.attributes;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk.filter;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
  org.hamcrest.core;version="[1.1.0,2.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="5.10.1";x-friends:="org.eclipse.jgit.lfs.server.test"
+Export-Package: org.eclipse.jgit.lfs.test;version="5.11.2";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 70d756d..eef3ac8 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.test</artifactId>
diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java
new file mode 100644
index 0000000..8964310
--- /dev/null
+++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lfs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.attributes.FilterCommandRegistry;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lfs.lib.Constants;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class LfsGitTest extends RepositoryTestCase {
+
+	private static final String SMUDGE_NAME = org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX
+			+ Constants.ATTR_FILTER_DRIVER_PREFIX
+			+ org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_SMUDGE;
+
+	private static final String CLEAN_NAME = org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX
+			+ Constants.ATTR_FILTER_DRIVER_PREFIX
+			+ org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_CLEAN;
+
+	@BeforeClass
+	public static void installLfs() {
+		FilterCommandRegistry.register(SMUDGE_NAME, SmudgeFilter.FACTORY);
+		FilterCommandRegistry.register(CLEAN_NAME, CleanFilter.FACTORY);
+	}
+
+	@AfterClass
+	public static void removeLfs() {
+		FilterCommandRegistry.unregister(SMUDGE_NAME);
+		FilterCommandRegistry.unregister(CLEAN_NAME);
+	}
+
+	private Git git;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		git = new Git(db);
+		// commit something
+		writeTrashFile("Test.txt", "Hello world");
+		git.add().addFilepattern("Test.txt").call();
+		git.commit().setMessage("Initial commit").call();
+		// prepare the config for LFS
+		StoredConfig config = git.getRepository().getConfig();
+		config.setString("filter", "lfs", "clean", CLEAN_NAME);
+		config.setString("filter", "lfs", "smudge", SMUDGE_NAME);
+		config.save();
+	}
+
+	@Test
+	public void checkoutNonLfsPointer() throws Exception {
+		String content = "size_t\nsome_function(void* ptr);\n";
+		File smallFile = writeTrashFile("Test.txt", content);
+		StringBuilder largeContent = new StringBuilder(
+				LfsPointer.SIZE_THRESHOLD * 4);
+		while (largeContent.length() < LfsPointer.SIZE_THRESHOLD * 4) {
+			largeContent.append(content);
+		}
+		File largeFile = writeTrashFile("large.txt", largeContent.toString());
+		fsTick(largeFile);
+		git.add().addFilepattern("Test.txt").addFilepattern("large.txt").call();
+		git.commit().setMessage("Text files").call();
+		writeTrashFile(".gitattributes", "*.txt filter=lfs");
+		git.add().addFilepattern(".gitattributes").call();
+		git.commit().setMessage("attributes").call();
+		assertTrue(smallFile.delete());
+		assertTrue(largeFile.delete());
+		// This reset will run the two text files through the smudge filter
+		git.reset().setMode(ResetType.HARD).call();
+		assertTrue(smallFile.exists());
+		assertTrue(largeFile.exists());
+		checkFile(smallFile, content);
+		checkFile(largeFile, largeContent.toString());
+		// Modify the large file
+		largeContent.append(content);
+		writeTrashFile("large.txt", largeContent.toString());
+		// This should convert largeFile to an LFS pointer
+		git.add().addFilepattern("large.txt").call();
+		git.commit().setMessage("Large modified").call();
+		String lfsPtr = "version https://git-lfs.github.com/spec/v1\n"
+				+ "oid sha256:d041ab19bd7edd899b3c0450d0f61819f96672f0b22d26c9753abc62e1261614\n"
+				+ "size 858\n";
+		assertEquals("[.gitattributes, mode:100644, content:*.txt filter=lfs]"
+				+ "[Test.txt, mode:100644, content:" + content + ']'
+						+ "[large.txt, mode:100644, content:" + lfsPtr + ']',
+				indexState(CONTENT));
+		// Verify the file has been saved
+		File savedFile = new File(db.getDirectory(), "lfs");
+		savedFile = new File(savedFile, "objects");
+		savedFile = new File(savedFile, "d0");
+		savedFile = new File(savedFile, "41");
+		savedFile = new File(savedFile,
+				"d041ab19bd7edd899b3c0450d0f61819f96672f0b22d26c9753abc62e1261614");
+		String saved = new String(Files.readAllBytes(savedFile.toPath()),
+				StandardCharsets.UTF_8);
+		assertEquals(saved, largeContent.toString());
+
+		assertTrue(smallFile.delete());
+		assertTrue(largeFile.delete());
+		git.reset().setMode(ResetType.HARD).call();
+		assertTrue(smallFile.exists());
+		assertTrue(largeFile.exists());
+		checkFile(smallFile, content);
+		checkFile(largeFile, largeContent.toString());
+		assertEquals("[.gitattributes, mode:100644, content:*.txt filter=lfs]"
+				+ "[Test.txt, mode:100644, content:" + content + ']'
+						+ "[large.txt, mode:100644, content:" + lfsPtr + ']',
+				indexState(CONTENT));
+		git.add().addFilepattern("Test.txt").call();
+		git.commit().setMessage("Small committed again").call();
+		String lfsPtrSmall = "version https://git-lfs.github.com/spec/v1\n"
+				+ "oid sha256:9110463275fb0e2f0e9fdeaf84e598e62915666161145cf08927079119cc7814\n"
+				+ "size 33\n";
+		assertEquals("[.gitattributes, mode:100644, content:*.txt filter=lfs]"
+				+ "[Test.txt, mode:100644, content:" + lfsPtrSmall + ']'
+						+ "[large.txt, mode:100644, content:" + lfsPtr + ']',
+				indexState(CONTENT));
+
+		assertTrue(git.status().call().isClean());
+	}
+}
diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/lib/LFSPointerTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/lib/LFSPointerTest.java
index 7ee898f..da78b28 100644
--- a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/lib/LFSPointerTest.java
+++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/lib/LFSPointerTest.java
@@ -12,7 +12,13 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 
@@ -23,17 +29,275 @@
  * Test LfsPointer file abstraction
  */
 public class LFSPointerTest {
+
+	private static final String TEST_SHA256 = "27e15b72937fc8f558da24ac3d50ec20302a4cf21e33b87ae8e4ce90e89c4b10";
+
 	@Test
 	public void testEncoding() throws IOException {
-		final String s = "27e15b72937fc8f558da24ac3d50ec20302a4cf21e33b87ae8e4ce90e89c4b10";
-		AnyLongObjectId id = LongObjectId.fromString(s);
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
 		LfsPointer ptr = new LfsPointer(id, 4);
 		try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
 			ptr.encode(baos);
 			assertEquals(
 					"version https://git-lfs.github.com/spec/v1\noid sha256:"
-							+ s + "\nsize 4\n",
+							+ TEST_SHA256 + "\nsize 4\n",
 					baos.toString(UTF_8.name()));
 		}
 	}
+
+	@Test
+	public void testReadValidLfsPointer() throws Exception {
+		String ptr = "version https://git-lfs.github.com/spec/v1\n"
+				+ "oid sha256:" + TEST_SHA256 + '\n'
+				+ "size 4\n";
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				ptr.getBytes(UTF_8))) {
+			assertEquals(lfs, LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testReadValidLfsPointerUnordered() throws Exception {
+		// This is actually not allowed per the spec, but JGit accepts it
+		// anyway.
+		String ptr = "version https://git-lfs.github.com/spec/v1\n"
+				+ "size 4\n"
+				+ "oid sha256:" + TEST_SHA256 + '\n';
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				ptr.getBytes(UTF_8))) {
+			assertEquals(lfs, LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testReadValidLfsPointerVersionNotFirst() throws Exception {
+		// This is actually not allowed per the spec, but JGit accepts it
+		// anyway.
+		String ptr = "oid sha256:" + TEST_SHA256 + '\n'
+				+ "size 4\n"
+				+ "version https://git-lfs.github.com/spec/v1\n";
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				ptr.getBytes(UTF_8))) {
+			assertEquals(lfs, LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testReadInvalidLfsPointer() throws Exception {
+		String cSource = "size_t someFunction(void *ptr); // Fake C source\n";
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				cSource.getBytes(UTF_8))) {
+			assertNull("Is not a LFS pointer", LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testReadInvalidLfsPointer2() throws Exception {
+		String cSource = "size_t\nsomeFunction(void *ptr);\n// Fake C source\n";
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				cSource.getBytes(UTF_8))) {
+			assertNull("Is not a LFS pointer", LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testReadInValidLfsPointerVersionWrong() throws Exception {
+		String ptr = "version https://git-lfs.example.org/spec/v1\n"
+				+ "oid sha256:" + TEST_SHA256 + '\n'
+				+ "size 4\n";
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				ptr.getBytes(UTF_8))) {
+			assertNull("Is not a LFS pointer", LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testReadInValidLfsPointerVersionTwice() throws Exception {
+		String ptr = "version https://git-lfs.github.com/spec/v1\n"
+				+ "version https://git-lfs.github.com/spec/v1\n"
+				+ "oid sha256:" + TEST_SHA256 + '\n'
+				+ "size 4\n";
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				ptr.getBytes(UTF_8))) {
+			assertNull("Is not a LFS pointer", LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testReadInValidLfsPointerVersionTwice2() throws Exception {
+		String ptr = "version https://git-lfs.github.com/spec/v1\n"
+				+ "oid sha256:" + TEST_SHA256 + '\n'
+				+ "version https://git-lfs.github.com/spec/v1\n"
+				+ "size 4\n";
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				ptr.getBytes(UTF_8))) {
+			assertNull("Is not a LFS pointer", LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testReadInValidLfsPointerOidTwice() throws Exception {
+		String ptr = "version https://git-lfs.github.com/spec/v1\n"
+				+ "oid sha256:" + TEST_SHA256 + '\n'
+				+ "oid sha256:" + TEST_SHA256 + '\n'
+				+ "size 4\n";
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				ptr.getBytes(UTF_8))) {
+			assertNull("Is not a LFS pointer", LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testReadInValidLfsPointerSizeTwice() throws Exception {
+		String ptr = "version https://git-lfs.github.com/spec/v1\n"
+				+ "size 4\n"
+				+ "size 4\n"
+				+ "oid sha256:" + TEST_SHA256 + '\n';
+		try (ByteArrayInputStream in = new ByteArrayInputStream(
+				ptr.getBytes(UTF_8))) {
+			assertNull("Is not a LFS pointer", LfsPointer.parseLfsPointer(in));
+		}
+	}
+
+	@Test
+	public void testRoundtrip() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer ptr = new LfsPointer(id, 4);
+		try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+			ptr.encode(baos);
+			try (ByteArrayInputStream in = new ByteArrayInputStream(
+					baos.toByteArray())) {
+				assertEquals(ptr, LfsPointer.parseLfsPointer(in));
+			}
+		}
+	}
+
+	@Test
+	public void testEquals() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		AnyLongObjectId id2 = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs2 = new LfsPointer(id2, 4);
+		assertTrue(lfs.equals(lfs2));
+		assertTrue(lfs2.equals(lfs));
+	}
+
+	@Test
+	public void testEqualsNull() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		assertFalse(lfs.equals(null));
+	}
+
+	@Test
+	public void testEqualsSame() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		assertTrue(lfs.equals(lfs));
+	}
+
+	@Test
+	public void testEqualsOther() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		assertFalse(lfs.equals(new Object()));
+	}
+
+	@Test
+	public void testNotEqualsOid() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		AnyLongObjectId id2 = LongObjectId
+				.fromString(TEST_SHA256.replace('7', '5'));
+		LfsPointer lfs2 = new LfsPointer(id2, 4);
+		assertFalse(lfs.equals(lfs2));
+		assertFalse(lfs2.equals(lfs));
+	}
+
+	@Test
+	public void testNotEqualsSize() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		AnyLongObjectId id2 = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs2 = new LfsPointer(id2, 5);
+		assertFalse(lfs.equals(lfs2));
+		assertFalse(lfs2.equals(lfs));
+	}
+
+	@Test
+	public void testCompareToEquals() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		AnyLongObjectId id2 = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs2 = new LfsPointer(id2, 4);
+		assertEquals(0, lfs.compareTo(lfs2));
+		assertEquals(0, lfs2.compareTo(lfs));
+	}
+
+	@Test
+	@SuppressWarnings("SelfComparison")
+	public void testCompareToSame() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		assertEquals(0, lfs.compareTo(lfs));
+	}
+
+	@Test
+	public void testCompareToNotEqualsOid() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		AnyLongObjectId id2 = LongObjectId
+				.fromString(TEST_SHA256.replace('7', '5'));
+		LfsPointer lfs2 = new LfsPointer(id2, 4);
+		assertNotEquals(0, lfs.compareTo(lfs2));
+		assertNotEquals(0, lfs2.compareTo(lfs));
+	}
+
+	@Test
+	public void testCompareToNotEqualsSize() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		AnyLongObjectId id2 = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs2 = new LfsPointer(id2, 5);
+		assertNotEquals(0, lfs.compareTo(lfs2));
+		assertNotEquals(0, lfs2.compareTo(lfs));
+	}
+
+	@Test
+	public void testCompareToNull() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		assertThrows(NullPointerException.class, () -> lfs.compareTo(null));
+	}
+
+	@Test
+	public void testHashcodeEquals() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		AnyLongObjectId id2 = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs2 = new LfsPointer(id2, 4);
+		assertEquals(lfs.hashCode(), lfs2.hashCode());
+	}
+
+	@Test
+	public void testHashcodeSame() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		assertEquals(lfs.hashCode(), lfs.hashCode());
+	}
+
+	@Test
+	public void testHashcodeNotEquals() throws Exception {
+		AnyLongObjectId id = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs = new LfsPointer(id, 4);
+		AnyLongObjectId id2 = LongObjectId.fromString(TEST_SHA256);
+		LfsPointer lfs2 = new LfsPointer(id2, 5);
+		assertNotEquals(lfs.hashCode(), lfs2.hashCode());
+	}
 }
diff --git a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
index 1b2a90c..876384f 100644
--- a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
@@ -3,33 +3,31 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs
 Bundle-SymbolicName: org.eclipse.jgit.lfs
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.lfs;version="5.10.1",
- org.eclipse.jgit.lfs.errors;version="5.10.1",
- org.eclipse.jgit.lfs.internal;version="5.10.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="5.10.1"
+Export-Package: org.eclipse.jgit.lfs;version="5.11.2",
+ org.eclipse.jgit.lfs.errors;version="5.11.2",
+ org.eclipse.jgit.lfs.internal;version="5.11.2";x-friends:="org.eclipse.jgit.lfs.test,org.eclipse.jgit.lfs.server.fs,org.eclipse.jgit.lfs.server",
+ org.eclipse.jgit.lfs.lib;version="5.11.2"
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: com.google.gson;version="[2.8.2,3.0.0)",
- com.google.gson.stream;version="[2.8.2,3.0.0)",
- org.apache.http.impl.client;version="[4.2.6,5.0.0)",
- org.apache.http.impl.conn;version="[4.2.6,5.0.0)",
- org.eclipse.jgit.annotations;version="[5.10.1,5.11.0)";resolution:=optional,
- org.eclipse.jgit.api.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.attributes;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.diff;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.hooks;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.pack;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.http;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk.filter;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util.io;version="[5.10.1,5.11.0)"
+Import-Package: com.google.gson;version="[2.8.0,3.0.0)",
+ com.google.gson.stream;version="[2.8.0,3.0.0)",
+ org.eclipse.jgit.annotations;version="[5.11.2,5.12.0)";resolution:=optional,
+ org.eclipse.jgit.api.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.attributes;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.diff;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.hooks;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.pack;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.http;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk.filter;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util.io;version="[5.11.2,5.12.0)"
diff --git a/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF
index 4d9bc2e..db9bcc6 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.lfs;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.lfs;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.lfs/pom.xml b/org.eclipse.jgit.lfs/pom.xml
index b307916..d1f2ce4 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs</artifactId>
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsBlobFilter.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsBlobFilter.java
index 52c3001..032a19b 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsBlobFilter.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsBlobFilter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com> and others
+ * Copyright (C) 2017, 2021 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
@@ -45,7 +45,7 @@
 	 */
 	public static ObjectLoader smudgeLfsBlob(Repository db, ObjectLoader loader)
 			throws IOException {
-		if (loader.getSize() > LfsPointer.SIZE_THRESHOLD) {
+		if (loader.getSize() > LfsPointer.FULL_SIZE_THRESHOLD) {
 			return loader;
 		}
 
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPointer.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPointer.java
index 4e2d8a9..0a8a3fa 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPointer.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPointer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016, Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2016, 2021 Christian Halstrick <christian.halstrick@sap.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -11,7 +11,9 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import java.io.BufferedInputStream;
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -19,11 +21,13 @@
 import java.io.PrintStream;
 import java.io.UnsupportedEncodingException;
 import java.util.Locale;
+import java.util.Objects;
 
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
 import org.eclipse.jgit.lfs.lib.Constants;
 import org.eclipse.jgit.lfs.lib.LongObjectId;
+import org.eclipse.jgit.util.IO;
 
 /**
  * Represents an LFS pointer file
@@ -56,9 +60,15 @@
 	public static final String HASH_FUNCTION_NAME = Constants.LONG_HASH_FUNCTION
 			.toLowerCase(Locale.ROOT).replace("-", ""); //$NON-NLS-1$ //$NON-NLS-2$
 
-	private AnyLongObjectId oid;
+	/**
+	 * {@link #SIZE_THRESHOLD} is too low; with lfs extensions a LFS pointer can
+	 * be larger. But 8kB should be more than enough.
+	 */
+	static final int FULL_SIZE_THRESHOLD = 8 * 1024;
 
-	private long size;
+	private final AnyLongObjectId oid;
+
+	private final long size;
 
 	/**
 	 * <p>Constructor for LfsPointer.</p>
@@ -114,34 +124,113 @@
 
 	/**
 	 * Try to parse the data provided by an InputStream to the format defined by
-	 * {@link #VERSION}
+	 * {@link #VERSION}. If the given stream supports mark and reset as
+	 * indicated by {@link InputStream#markSupported()}, its input position will
+	 * be reset if the stream content is not actually a LFS pointer (i.e., when
+	 * {@code null} is returned). If the stream content is an invalid LFS
+	 * pointer or the given stream does not support mark/reset, the input
+	 * position may not be reset.
 	 *
 	 * @param in
 	 *            the {@link java.io.InputStream} from where to read the data
-	 * @return an {@link org.eclipse.jgit.lfs.LfsPointer} or <code>null</code>
-	 *         if the stream was not parseable as LfsPointer
+	 * @return an {@link org.eclipse.jgit.lfs.LfsPointer} or {@code null} if the
+	 *         stream was not parseable as LfsPointer
 	 * @throws java.io.IOException
 	 */
 	@Nullable
 	public static LfsPointer parseLfsPointer(InputStream in)
 			throws IOException {
+		if (in.markSupported()) {
+			return parse(in);
+		}
+		// Fallback; note that while parse() resets its input stream, that won't
+		// reset "in".
+		return parse(new BufferedInputStream(in));
+	}
+
+	@Nullable
+	private static LfsPointer parse(InputStream in)
+			throws IOException {
+		if (!in.markSupported()) {
+			// No translation; internal error
+			throw new IllegalArgumentException(
+					"LFS pointer parsing needs InputStream.markSupported() == true"); //$NON-NLS-1$
+		}
+		// Try reading only a short block first.
+		in.mark(SIZE_THRESHOLD);
+		byte[] preamble = new byte[SIZE_THRESHOLD];
+		int length = IO.readFully(in, preamble, 0);
+		if (length < preamble.length || in.read() < 0) {
+			// We have the whole file. Try to parse a pointer from it.
+			try (BufferedReader r = new BufferedReader(new InputStreamReader(
+					new ByteArrayInputStream(preamble, 0, length), UTF_8))) {
+				LfsPointer ptr = parse(r);
+				if (ptr == null) {
+					in.reset();
+				}
+				return ptr;
+			}
+		}
+		// Longer than SIZE_THRESHOLD: expect "version" to be the first line.
+		boolean hasVersion = checkVersion(preamble);
+		in.reset();
+		if (!hasVersion) {
+			return null;
+		}
+		in.mark(FULL_SIZE_THRESHOLD);
+		byte[] fullPointer = new byte[FULL_SIZE_THRESHOLD];
+		length = IO.readFully(in, fullPointer, 0);
+		if (length == fullPointer.length && in.read() >= 0) {
+			in.reset();
+			return null; // Too long.
+		}
+		try (BufferedReader r = new BufferedReader(new InputStreamReader(
+				new ByteArrayInputStream(fullPointer, 0, length), UTF_8))) {
+			LfsPointer ptr = parse(r);
+			if (ptr == null) {
+				in.reset();
+			}
+			return ptr;
+		}
+	}
+
+	private static LfsPointer parse(BufferedReader r) throws IOException {
 		boolean versionLine = false;
 		LongObjectId id = null;
 		long sz = -1;
-
-		try (BufferedReader br = new BufferedReader(
-				new InputStreamReader(in, UTF_8))) {
-			for (String s = br.readLine(); s != null; s = br.readLine()) {
-				if (s.startsWith("#") || s.length() == 0) { //$NON-NLS-1$
-					continue;
-				} else if (s.startsWith("version") && s.length() > 8 //$NON-NLS-1$
-						&& (s.substring(8).trim().equals(VERSION) ||
-								s.substring(8).trim().equals(VERSION_LEGACY))) {
-					versionLine = true;
-				} else if (s.startsWith("oid sha256:")) { //$NON-NLS-1$
-					id = LongObjectId.fromString(s.substring(11).trim());
-				} else if (s.startsWith("size") && s.length() > 5) { //$NON-NLS-1$
-					sz = Long.parseLong(s.substring(5).trim());
+		// This parsing is a bit too general if we go by the spec at
+		// https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
+		// Comment lines are not mentioned in the spec, the "version" line
+		// MUST be the first, and keys are ordered alphabetically.
+		for (String s = r.readLine(); s != null; s = r.readLine()) {
+			if (s.startsWith("#") || s.length() == 0) { //$NON-NLS-1$
+				continue;
+			} else if (s.startsWith("version")) { //$NON-NLS-1$
+				if (versionLine || !checkVersionLine(s)) {
+					return null; // Not a LFS pointer
+				}
+				versionLine = true;
+			} else {
+				try {
+					if (s.startsWith("oid sha256:")) { //$NON-NLS-1$
+						if (id != null) {
+							return null; // Not a LFS pointer
+						}
+						id = LongObjectId.fromString(s.substring(11).trim());
+					} else if (s.startsWith("size")) { //$NON-NLS-1$
+						if (sz > 0 || s.length() < 5 || s.charAt(4) != ' ') {
+							return null; // Not a LFS pointer
+						}
+						sz = Long.parseLong(s.substring(5).trim());
+					}
+				} catch (RuntimeException e) {
+					// We could not parse the line. If we have a version
+					// already, this is a corrupt LFS pointer. Otherwise it
+					// is just not an LFS pointer.
+					if (versionLine) {
+						throw e;
+					}
+					return null;
 				}
 			}
 			if (versionLine && id != null && sz > -1) {
@@ -151,6 +240,30 @@
 		return null;
 	}
 
+	private static boolean checkVersion(byte[] data) {
+		// According to the spec at
+		// https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
+		// it MUST always be the first line.
+		try (BufferedReader r = new BufferedReader(
+				new InputStreamReader(new ByteArrayInputStream(data), UTF_8))) {
+			String s = r.readLine();
+			if (s != null && s.startsWith("version")) { //$NON-NLS-1$
+				return checkVersionLine(s);
+			}
+		} catch (IOException e) {
+			// Doesn't occur, we're reading from a byte array!
+		}
+		return false;
+	}
+
+	private static boolean checkVersionLine(String s) {
+		if (s.length() < 8 || s.charAt(7) != ' ') {
+			return false; // Not a valid LFS pointer version line
+		}
+		String rest = s.substring(8).trim();
+		return VERSION.equals(rest) || VERSION_LEGACY.equals(rest);
+	}
+
 	/** {@inheritDoc} */
 	@Override
 	public String toString() {
@@ -170,5 +283,22 @@
 
 		return Long.compare(getSize(), o.getSize());
 	}
-}
 
+	@Override
+	public int hashCode() {
+		return Objects.hash(getOid()) * 31 + Long.hashCode(getSize());
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || getClass() != obj.getClass()) {
+			return false;
+		}
+		LfsPointer other = (LfsPointer) obj;
+		return Objects.equals(getOid(), other.getOid())
+				&& getSize() == other.getSize();
+	}
+}
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java
index 2f80d5b..3411887 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016, Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2016, 2021 Christian Halstrick <christian.halstrick@sap.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -11,6 +11,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -87,20 +88,31 @@
 	 */
 	public SmudgeFilter(Repository db, InputStream in, OutputStream out)
 			throws IOException {
+		this(in.markSupported() ? in : new BufferedInputStream(in), out, db);
+	}
+
+	private SmudgeFilter(InputStream in, OutputStream out, Repository db)
+			throws IOException {
 		super(in, out);
+		InputStream from = in;
 		try {
-			Lfs lfs = new Lfs(db);
-			LfsPointer res = LfsPointer.parseLfsPointer(in);
+			LfsPointer res = LfsPointer.parseLfsPointer(from);
 			if (res != null) {
 				AnyLongObjectId oid = res.getOid();
+				Lfs lfs = new Lfs(db);
 				Path mediaFile = lfs.getMediaFile(oid);
 				if (!Files.exists(mediaFile)) {
 					downloadLfsResource(lfs, db, res);
 				}
 				this.in = Files.newInputStream(mediaFile);
+			} else {
+				// Not swapped; stream was reset, don't close!
+				from = null;
 			}
 		} finally {
-			in.close(); // make sure the swapped stream is closed properly.
+			if (from != null) {
+				from.close(); // Close the swapped-out stream
+			}
 		}
 	}
 
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java
index 7a0ed45..e221913 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java
@@ -18,7 +18,9 @@
 import java.net.ProxySelector;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
 import java.util.LinkedList;
 import java.util.Map;
 import java.util.TreeMap;
@@ -258,8 +260,8 @@
 	private static final class AuthCache {
 		private static final long AUTH_CACHE_EAGER_TIMEOUT = 500;
 
-		private static final SimpleDateFormat ISO_FORMAT = new SimpleDateFormat(
-				"yyyy-MM-dd'T'HH:mm:ss.SSSX"); //$NON-NLS-1$
+		private static final DateTimeFormatter ISO_FORMAT = DateTimeFormatter
+				.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"); //$NON-NLS-1$
 
 		/**
 		 * Creates a cache entry for an authentication response.
@@ -278,8 +280,10 @@
 							- AUTH_CACHE_EAGER_TIMEOUT;
 				} else if (action.expiresAt != null
 						&& !action.expiresAt.isEmpty()) {
-					this.validUntil = ISO_FORMAT.parse(action.expiresAt)
-							.getTime() - AUTH_CACHE_EAGER_TIMEOUT;
+					this.validUntil = LocalDateTime
+							.parse(action.expiresAt, ISO_FORMAT)
+							.atZone(ZoneOffset.UTC).toInstant().toEpochMilli()
+							- AUTH_CACHE_EAGER_TIMEOUT;
 				} else {
 					this.validUntil = System.currentTimeMillis();
 				}
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/LfsPointerFilter.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/LfsPointerFilter.java
index d84eebd..99bae49 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/LfsPointerFilter.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/LfsPointerFilter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015, 2017, Dariusz Luksza <dariusz@luksza.org> and others
+ * Copyright (C) 2015, 2021 Dariusz Luksza <dariusz@luksza.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
@@ -58,6 +58,8 @@
 		try (ObjectStream stream = object.openStream()) {
 			pointer = LfsPointer.parseLfsPointer(stream);
 			return pointer != null;
+		} catch (RuntimeException e) {
+			return false;
 		}
 	}
 
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 522cc7d..cc8756e 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="5.10.1.qualifier"
+      version="5.11.2.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 aa9486a..ef6a8a6 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
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 3908d3a..85dec4c 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="5.10.1.qualifier"
+      version="5.11.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="5.10.1" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="5.11.2" 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 4ae91c5..eebe2c4 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 a47f8c7..beff00e 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="5.10.1.qualifier"
+      version="5.11.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="5.10.1" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="5.11.2" 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 965c916..2584dab 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 45d4845..ce6e241 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="5.10.1.qualifier"
+      version="5.11.2.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="5.10.1" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="5.11.2" 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 cb8477e..70ed137 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 e055c0a..a7f0dd9 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="5.10.1.qualifier"
+      version="5.11.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="5.10.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="5.11.2" 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 9303934..74d9992 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 6b23287..d009c0a 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="5.10.1.qualifier"
+      version="5.11.2.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="5.10.1" match="equivalent"/>
-      <import feature="org.eclipse.jgit.lfs" version="5.10.1" match="equivalent"/>
-      <import feature="org.eclipse.jgit.ssh.apache" version="5.10.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="5.11.2" match="equivalent"/>
+      <import feature="org.eclipse.jgit.lfs" version="5.11.2" match="equivalent"/>
+      <import feature="org.eclipse.jgit.ssh.apache" version="5.11.2" 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 0f8a36d..2c1a283 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 749416a..48497e5 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 5149be9..4b0ff95 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="5.10.1.qualifier"
+      version="5.11.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="5.10.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="5.11.2" 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 f6cd493..0ca001c 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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>5.10.1-SNAPSHOT</version>
+      <version>5.11.2-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 1fdea7f..3c9004b 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="5.10.1.qualifier"
+      version="5.11.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="5.10.1" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="5.11.2" 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 8f281ff..f563cff 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
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 ec6421e..1bc9234 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="5.10.1.qualifier"
+      version="5.11.2.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="5.10.1" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="5.11.2" 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 3f5fdef..526e18c 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
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 679072d..43c0a83 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10.target
index d7e1585..715986b 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.10" sequenceNumber="1606854603">
+<target name="jgit-4.10" sequenceNumber="1615333029">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.10.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10.tpd
index d84f801..ed443a6 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.10.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.10" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/2018-12/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.11.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.11.target
index 42ef49c..1926712 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.11.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.11.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.11" sequenceNumber="1606854603">
+<target name="jgit-4.11" sequenceNumber="1615333055">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.11.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.11.tpd
index df221e6..013d621 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.11.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.11.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.11" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/2019-03/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.12.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.12.target
index 17634cf..4449dc3 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.12.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.12.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.12" sequenceNumber="1606854603">
+<target name="jgit-4.12" sequenceNumber="1615333029">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.12.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.12.tpd
index 56fd714..99008ab 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.12.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.12.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.12" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/2019-06/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.13.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.13.target
index 04b536e..01a10e7 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.13.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.13.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.13" sequenceNumber="1606854603">
+<target name="jgit-4.13" sequenceNumber="1615333029">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.13.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.13.tpd
index 46069c3..d0db92c 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.13.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.13.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.13" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/2019-09/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.14.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.14.target
index c1722ff..b56f9a1 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.14.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.14.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.14" sequenceNumber="1606854603">
+<target name="jgit-4.14" sequenceNumber="1615333029">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.14.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.14.tpd
index f517ab2..e0a730e 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.14.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.14.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.14" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/2019-12/201912181000/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.15.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.15.target
index 7f5f5a1..f3820a7 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.15.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.15.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.15" sequenceNumber="1606854603">
+<target name="jgit-4.15" sequenceNumber="1615333029">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.15.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.15.tpd
index 1e4a0ee..773a9a9 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.15.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.15.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.15" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/2020-03/202003181000/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.16.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.16.target
index bba090a..6a9f582 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.16.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.16.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.16" sequenceNumber="1606854603">
+<target name="jgit-4.16" sequenceNumber="1615333030">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.16.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.16.tpd
index 2435c48..8b4de8b 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.16.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.16.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.16" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/2020-06/" {
 	org.eclipse.osgi lazy
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 d7c1728..b7481e0 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,30 +1,32 @@
 <?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="1606854603">
+<target name="jgit-4.17" sequenceNumber="1615333030">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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 4327415..b2585be 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-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.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 4ce2019..6d851a2 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,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.18-staging" sequenceNumber="1608217757">
+<target name="jgit-4.18" sequenceNumber="1615333029">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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 b457322..6d16256 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-staging" with source configurePhase
+target "jgit-4.18" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.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-staging.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19-staging.target
new file mode 100644
index 0000000..1a0505d
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19-staging.target
@@ -0,0 +1,96 @@
+<?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="1615333029">
+  <locations>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
+    </location>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
+      <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="javaewah" version="1.1.7.v20200107-0831"/>
+      <unit id="javaewah.source" version="1.1.7.v20200107-0831"/>
+      <unit id="javax.servlet" version="3.1.0.v201410161800"/>
+      <unit id="javax.servlet.source" version="3.1.0.v201410161800"/>
+      <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.v20181102-1323"/>
+      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20181102-1323"/>
+      <unit id="org.apache.ant" version="1.10.9.v20201106-1946"/>
+      <unit id="org.apache.ant.source" version="1.10.9.v20201106-1946"/>
+      <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.19.0.v20200106-2343"/>
+      <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
+      <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
+      <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
+      <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
+      <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
+      <unit id="org.bouncycastle.bcpg.source" version="1.65.0.v20200527-1955"/>
+      <unit id="org.bouncycastle.bcpkix" version="1.65.0.v20200527-1955"/>
+      <unit id="org.bouncycastle.bcpkix.source" version="1.65.0.v20200527-1955"/>
+      <unit id="org.bouncycastle.bcprov" version="1.65.1.v20200529-1514"/>
+      <unit id="org.bouncycastle.bcprov.source" version="1.65.1.v20200529-1514"/>
+      <unit id="org.hamcrest" version="1.1.0.v20090501071000"/>
+      <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.0.v20200204-1500"/>
+      <unit id="org.junit.source" version="4.13.0.v20200204-1500"/>
+      <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.log4j12" version="1.7.30.v20201108-2042"/>
+      <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
+      <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
+      <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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/2021-03/"/>
+    </location>
+  </locations>
+</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19-staging.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19-staging.tpd
new file mode 100644
index 0000000..7ed5377
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19-staging.tpd
@@ -0,0 +1,8 @@
+target "jgit-4.19-staging" with source configurePhase
+
+include "projects/jetty-9.4.x.tpd"
+include "orbit/R20210223232630-2021-03.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.6.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
index 061dcf2..249be4c 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.6" sequenceNumber="1606854620">
+<target name="jgit-4.6" sequenceNumber="1615333044">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.6.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.tpd
index c7fbf02..6e7cd8b 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.6" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/neon/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
index b96c54d..72c44d7 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.7" sequenceNumber="1606854608">
+<target name="jgit-4.7" sequenceNumber="1615333034">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.7.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.tpd
index 585e04b..5a58b00 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.7" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/oxygen/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.target
index 3ddc0bc..10e3dea 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.8" sequenceNumber="1606854603">
+<target name="jgit-4.8" sequenceNumber="1615333030">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.8.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.tpd
index 694fc67..3114877 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.8.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.8" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/photon/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.target
index f119fc1..55b3c60 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.target
@@ -1,30 +1,32 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.9" sequenceNumber="1606854603">
+<target name="jgit-4.9" sequenceNumber="1615333029">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util" version="9.4.30.v20200611"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.4.30.v20200611"/>
-      <repository id="jetty-9.4.30" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax" version="9.4.36.v20210114"/>
+      <unit id="org.eclipse.jetty.util.ajax.source" version="9.4.36.v20210114"/>
+      <repository id="jetty-9.4.36" location="https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.google.gson" version="2.8.2.v20180104-1110"/>
-      <unit id="com.google.gson.source" version="2.8.2.v20180104-1110"/>
+      <unit id="com.google.gson" version="2.8.6.v20201231-1626"/>
+      <unit id="com.google.gson.source" version="2.8.6.v20201231-1626"/>
       <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"/>
@@ -47,16 +49,16 @@
       <unit id="org.apache.commons.compress.source" version="1.19.0.v20200106-2343"/>
       <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.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.10.v20200830-2311"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.12.v20200108-1212"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.12.v20200108-1212"/>
+      <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.14.v20210128-2225"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/>
       <unit id="org.apache.log4j" version="1.2.15.v201012070815"/>
       <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/>
-      <unit id="org.apache.sshd.osgi" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.osgi.source" version="2.4.0.v20200318-1614"/>
-      <unit id="org.apache.sshd.sftp" version="2.4.0.v20200319-1547"/>
-      <unit id="org.apache.sshd.sftp.source" version="2.4.0.v20200319-1547"/>
+      <unit id="org.apache.sshd.osgi" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.osgi.source" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp" version="2.6.0.v20210201-2003"/>
+      <unit id="org.apache.sshd.sftp.source" version="2.6.0.v20210201-2003"/>
       <unit id="org.assertj" version="3.14.0.v20200120-1926"/>
       <unit id="org.assertj.source" version="3.14.0.v20200120-1926"/>
       <unit id="org.bouncycastle.bcpg" version="1.65.0.v20200527-1955"/>
@@ -84,7 +86,7 @@
       <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/>
       <unit id="org.tukaani.xz" version="1.8.0.v20180207-1613"/>
       <unit id="org.tukaani.xz.source" version="1.8.0.v20180207-1613"/>
-      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository"/>
+      <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/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.9.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.tpd
index ae5390c..132a0b0 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.9.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.9" with source configurePhase
 
 include "projects/jetty-9.4.x.tpd"
-include "orbit/R20201130205003-2020-12.tpd"
+include "orbit/R20210223232630-2021-03.tpd"
 
 location "https://download.eclipse.org/releases/2018-09/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20210223232630-2021-03.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20210223232630-2021-03.tpd
new file mode 100644
index 0000000..605a43b
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20210223232630-2021-03.tpd
@@ -0,0 +1,66 @@
+target "R20210223232630-2021-03" with source configurePhase
+// see https://download.eclipse.org/tools/orbit/downloads/
+
+location "https://download.eclipse.org/tools/orbit/downloads/drops/R20210223232630/repository" {
+	com.google.gson [2.8.6.v20201231-1626,2.8.6.v20201231-1626]
+	com.google.gson.source [2.8.6.v20201231-1626,2.8.6.v20201231-1626]
+	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.1.v201205102305,1.1.1.v201205102305]
+	com.jcraft.jzlib.source [1.1.1.v201205102305,1.1.1.v201205102305]
+	javaewah [1.1.7.v20200107-0831,1.1.7.v20200107-0831]
+	javaewah.source [1.1.7.v20200107-0831,1.1.7.v20200107-0831]
+	javax.servlet [3.1.0.v201410161800,3.1.0.v201410161800]
+	javax.servlet.source [3.1.0.v201410161800,3.1.0.v201410161800]
+	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.v20181102-1323,0.3.0.v20181102-1323]
+	net.i2p.crypto.eddsa.source [0.3.0.v20181102-1323,0.3.0.v20181102-1323]
+	org.apache.ant [1.10.9.v20201106-1946,1.10.9.v20201106-1946]
+	org.apache.ant.source [1.10.9.v20201106-1946,1.10.9.v20201106-1946]
+	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.19.0.v20200106-2343,1.19.0.v20200106-2343]
+	org.apache.commons.compress.source [1.19.0.v20200106-2343,1.19.0.v20200106-2343]
+	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.14.v20210128-2225,4.4.14.v20210128-2225]
+	org.apache.httpcomponents.httpcore.source [4.4.14.v20210128-2225,4.4.14.v20210128-2225]
+	org.apache.log4j [1.2.15.v201012070815,1.2.15.v201012070815]
+	org.apache.log4j.source [1.2.15.v201012070815,1.2.15.v201012070815]
+	org.apache.sshd.osgi [2.6.0.v20210201-2003,2.6.0.v20210201-2003]
+	org.apache.sshd.osgi.source [2.6.0.v20210201-2003,2.6.0.v20210201-2003]
+	org.apache.sshd.sftp [2.6.0.v20210201-2003,2.6.0.v20210201-2003]
+	org.apache.sshd.sftp.source [2.6.0.v20210201-2003,2.6.0.v20210201-2003]
+	org.assertj [3.14.0.v20200120-1926,3.14.0.v20200120-1926]
+	org.assertj.source [3.14.0.v20200120-1926,3.14.0.v20200120-1926]
+	org.bouncycastle.bcpg [1.65.0.v20200527-1955,1.65.0.v20200527-1955]
+	org.bouncycastle.bcpg.source [1.65.0.v20200527-1955,1.65.0.v20200527-1955]
+	org.bouncycastle.bcpkix [1.65.0.v20200527-1955,1.65.0.v20200527-1955]
+	org.bouncycastle.bcpkix.source [1.65.0.v20200527-1955,1.65.0.v20200527-1955]
+	org.bouncycastle.bcprov [1.65.1.v20200529-1514,1.65.1.v20200529-1514]
+	org.bouncycastle.bcprov.source [1.65.1.v20200529-1514,1.65.1.v20200529-1514]
+	org.hamcrest [1.1.0.v20090501071000,1.1.0.v20090501071000]
+	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.0.v20200204-1500,4.13.0.v20200204-1500]
+	org.junit.source [4.13.0.v20200204-1500,4.13.0.v20200204-1500]
+	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.log4j12 [1.7.30.v20201108-2042,1.7.30.v20201108-2042]
+	org.slf4j.binding.log4j12.source [1.7.30.v20201108-2042,1.7.30.v20201108-2042]
+	org.tukaani.xz [1.8.0.v20180207-1613,1.8.0.v20180207-1613]
+	org.tukaani.xz.source [1.8.0.v20180207-1613,1.8.0.v20180207-1613]
+}
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 a0b4931..70f7cea 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml
@@ -16,7 +16,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.target</artifactId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.4.x.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.4.x.tpd
index 70c426c..4eec8aa 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.4.x.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.4.x.tpd
@@ -1,20 +1,22 @@
 target "jetty-9.4.x" with source configurePhase
 
-location jetty-9.4.30 "https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.30.v20200611/" {
-	org.eclipse.jetty.client [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.client.source [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.continuation [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.continuation.source [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.http [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.http.source [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.io [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.io.source [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.security [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.security.source [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.server [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.server.source [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.servlet [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.servlet.source [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.util [9.4.30.v20200611,9.4.30.v20200611]
-	org.eclipse.jetty.util.source [9.4.30.v20200611,9.4.30.v20200611]
+location jetty-9.4.36 "https://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.36.v20210114/" {
+	org.eclipse.jetty.client [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.client.source [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.continuation [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.continuation.source [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.http [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.http.source [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.io [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.io.source [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.security [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.security.source [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.server [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.server.source [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.servlet [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.servlet.source [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.util [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.util.source [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.util.ajax [9.4.36.v20210114,9.4.36.v20210114]
+	org.eclipse.jetty.util.ajax.source [9.4.36.v20210114,9.4.36.v20210114]
 }
diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml
index 3d261de..6ab3254 100644
--- a/org.eclipse.jgit.packaging/pom.xml
+++ b/org.eclipse.jgit.packaging/pom.xml
@@ -16,7 +16,7 @@
 
   <groupId>org.eclipse.jgit</groupId>
   <artifactId>jgit.tycho.parent</artifactId>
-  <version>5.10.1-SNAPSHOT</version>
+  <version>5.11.2-SNAPSHOT</version>
   <packaging>pom</packaging>
 
   <name>JGit Tycho Parent</name>
@@ -165,7 +165,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-enforcer-plugin</artifactId>
-        <version>3.0.0-M1</version>
+        <version>3.0.0-M3</version>
         <executions>
           <execution>
             <id>enforce-maven</id>
@@ -175,7 +175,7 @@
             <configuration>
               <rules>
                 <requireMavenVersion>
-                  <version>3.5.2</version>
+                  <version>3.6.3</version>
                 </requireMavenVersion>
               </rules>
             </configuration>
@@ -294,12 +294,12 @@
         <plugin>
           <groupId>org.eclipse.cbi.maven.plugins</groupId>
           <artifactId>eclipse-jarsigner-plugin</artifactId>
-          <version>1.1.5</version>
+          <version>1.1.7</version>
         </plugin>
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>build-helper-maven-plugin</artifactId>
-          <version>3.0.0</version>
+          <version>3.2.0</version>
         </plugin>
         <plugin>
           <artifactId>maven-clean-plugin</artifactId>
@@ -318,7 +318,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-site-plugin</artifactId>
-          <version>3.8.2</version>
+          <version>3.9.1</version>
         </plugin>
       </plugins>
     </pluginManagement>
diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
index 891d217..ae67125 100644
--- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
@@ -3,28 +3,28 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.pgm.test
 Bundle-SymbolicName: org.eclipse.jgit.pgm.test
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.api;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.diff;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.dircache;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="5.10.1",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.merge;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.pgm;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.pgm.internal;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.pgm.opt;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util.io;version="[5.10.1,5.11.0)",
+Import-Package: org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.diff;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.dircache;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="5.11.2",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.merge;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.pgm;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.pgm.internal;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.pgm.opt;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util.io;version="[5.11.2,5.12.0)",
  org.hamcrest.core;bundle-version="[1.1.0,2.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.pgm.test/pom.xml b/org.eclipse.jgit.pgm.test/pom.xml
index ab17f1d..ca908a4 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm.test</artifactId>
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CloneTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CloneTest.java
index 2f09b7f..4cbd61c 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CloneTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CloneTest.java
@@ -11,7 +11,9 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
@@ -25,6 +27,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.RefSpec;
@@ -64,6 +67,45 @@
 		assertEquals("expected 1 branch", 1, branches.size());
 	}
 
+	@Test
+	public void testCloneInitialBranch() throws Exception {
+		createInitialCommit();
+
+		File gitDir = db.getDirectory();
+		String sourceURI = gitDir.toURI().toString();
+		File target = createTempDirectory("target");
+		String cmd = "git clone --branch master " + sourceURI + " "
+				+ shellQuote(target.getPath());
+		String[] result = execute(cmd);
+		assertArrayEquals(new String[] {
+				"Cloning into '" + target.getPath() + "'...", "", "" }, result);
+
+		Git git2 = Git.open(target);
+		List<Ref> branches = git2.branchList().call();
+		assertEquals("expected 1 branch", 1, branches.size());
+
+		Repository db2 = git2.getRepository();
+		ObjectId head = db2.resolve("HEAD");
+		assertNotNull(head);
+		assertNotEquals(ObjectId.zeroId(), head);
+		ObjectId master = db2.resolve("master");
+		assertEquals(head, master);
+	}
+
+	@Test
+	public void testCloneInitialBranchMissing() throws Exception {
+		createInitialCommit();
+
+		File gitDir = db.getDirectory();
+		String sourceURI = gitDir.toURI().toString();
+		File target = createTempDirectory("target");
+		String cmd = "git clone --branch foo " + sourceURI + " "
+				+ shellQuote(target.getPath());
+		Die e = assertThrows(Die.class, () -> execute(cmd));
+		assertEquals("Remote branch 'foo' not found in upstream origin",
+				e.getMessage());
+	}
+
 	private RevCommit createInitialCommit() throws Exception {
 		JGitTestUtil.writeTrashFile(db, "hello.txt", "world");
 		git.add().addFilepattern("hello.txt").call();
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/InitTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/InitTest.java
index 84474e3..88789d3 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/InitTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/InitTest.java
@@ -11,11 +11,14 @@
 package org.eclipse.jgit.pgm;
 
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
 
 import java.io.File;
 
+import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -54,4 +57,22 @@
 		assertArrayEquals(expecteds, result);
 	}
 
+	@Test
+	public void testInitDirectoryInitialBranch() throws Exception {
+		File workDirectory = tempFolder.getRoot();
+		File gitDirectory = new File(workDirectory, Constants.DOT_GIT);
+
+		String[] result = execute(
+				"git init -b main '" + workDirectory.getCanonicalPath() + "'");
+
+		String[] expecteds = new String[] {
+				"Initialized empty Git repository in "
+						+ gitDirectory.getCanonicalPath(),
+				"" };
+		assertArrayEquals(expecteds, result);
+
+		try (Repository repo = new FileRepository(gitDirectory)) {
+			assertEquals("refs/heads/main", repo.getFullBranch());
+		}
+	}
 }
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index 419d754..9200d3b 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -3,58 +3,57 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.pgm
 Bundle-SymbolicName: org.eclipse.jgit.pgm
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: javax.servlet;version="[3.1.0,4.0.0)",
+ org.apache.commons.logging;version="[1.2,2.0)",
  org.eclipse.jetty.server;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.server.handler;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.servlet;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.util;version="[9.4.5,10.0.0)",
  org.eclipse.jetty.util.component;version="[9.4.5,10.0.0)",
- org.eclipse.jgit.api;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.archive;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.awtui;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.blame;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.diff;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.dircache;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.gitrepo;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.ketch;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.io;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.pack;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.reftree;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.server;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.server.fs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs.server.s3;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.merge;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.notes;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revplot;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk.filter;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.pack;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.http.apache;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.resolver;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.sshd;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk.filter;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util.io;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.archive;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.awtui;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.blame;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.diff;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.dircache;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.gitrepo;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.io;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.server;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs.server.s3;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.merge;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.notes;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revplot;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk.filter;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.pack;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.http.apache;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.resolver;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.sshd;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk.filter;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util.io;version="[5.11.2,5.12.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="5.10.1";
+Export-Package: org.eclipse.jgit.console;version="5.11.2";
  uses:="org.eclipse.jgit.transport,
   org.eclipse.jgit.util",
- org.eclipse.jgit.pgm;version="5.10.1";
+ org.eclipse.jgit.pgm;version="5.11.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.util.io,
    org.eclipse.jgit.awtui,
@@ -66,14 +65,14 @@
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.api,
    javax.swing",
- org.eclipse.jgit.pgm.debug;version="5.10.1";
+ org.eclipse.jgit.pgm.debug;version="5.11.2";
   uses:="org.eclipse.jgit.util.io,
    org.eclipse.jgit.pgm,
    org.eclipse.jetty.servlet",
- org.eclipse.jgit.pgm.internal;version="5.10.1";
+ org.eclipse.jgit.pgm.internal;version="5.11.2";
   x-friends:="org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.test",
- org.eclipse.jgit.pgm.opt;version="5.10.1";
+ org.eclipse.jgit.pgm.opt;version="5.11.2";
   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 2464093..df01339 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="5.11.2.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 062b964..e645255 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
@@ -47,7 +47,6 @@
 org.eclipse.jgit.pgm.debug.ReadDirCache
 org.eclipse.jgit.pgm.debug.ReadReftable
 org.eclipse.jgit.pgm.debug.RebuildCommitGraph
-org.eclipse.jgit.pgm.debug.RebuildRefTree
 org.eclipse.jgit.pgm.debug.ShowCacheTree
 org.eclipse.jgit.pgm.debug.ShowCommands
 org.eclipse.jgit.pgm.debug.ShowDirCache
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index b00ade3..c07e146 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 6112a27..83846ee 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
@@ -77,14 +77,15 @@
 invalidRecurseSubmodulesMode=Invalid recurse submodules mode: {0}
 invalidUntrackedFilesMode=Invalid untracked files mode ''{0}''
 jgitVersion=jgit version {0}
-lineFormat={0}
-listeningOn=Listening on {0}
 lfsNoAccessKey=No accessKey in {0}
 lfsNoSecretKey=No secretKey in {0}
 lfsProtocolUrl=LFS protocol URL: {0}
 lfsStoreDirectory=LFS objects stored in: {0}
 lfsStoreUrl=LFS store URL: {0}
 lfsUnknownStoreType="Unknown LFS store type: {0}"
+lineFormat={0}
+listeningOn=Listening on {0}
+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:
 mergeFailed=Automatic merge failed; fix conflicts and then commit the result
@@ -118,7 +119,6 @@
 metaVar_filepattern=filepattern
 metaVar_gitDir=GIT_DIR
 metaVar_hostName=HOSTNAME
-metaVar_ketchServerType=SERVERTYPE
 metaVar_lfsStorage=STORAGE
 metaVar_linesOfContext=lines
 metaVar_message=message
@@ -143,6 +143,7 @@
 metaVar_s3StorageClass=STORAGE-CLASS
 metaVar_seconds=SECONDS
 metaVar_service=SERVICE
+metaVar_tagLocalUser=<GPG key ID>
 metaVar_treeish=tree-ish
 metaVar_uriish=uri-ish
 metaVar_url=URL
@@ -246,7 +247,6 @@
 usage_Gc=Cleanup unnecessary files and optimize the local repository
 usage_Glog=View commit history as a graph
 usage_IndexPack=Build pack index file for an existing packed archive
-usage_ketchServerType=Ketch server type
 usage_LFSDirectory=Directory to store large objects
 usage_LFSPort=Server http port
 usage_LFSRunStore=Store (fs | s3), store lfs objects in file system or Amazon S3
@@ -266,8 +266,6 @@
 usage_PrunePreserved=Remove the preserved subdirectory containing previously preserved old pack files before repacking, and before preserving more old pack files
 usage_ReadDirCache= Read the DirCache 100 times
 usage_RebuildCommitGraph=Recreate a repository from another one's commit graph
-usage_RebuildRefTree=Copy references into a RefTree
-usage_RebuildRefTreeEnable=set extensions.refStorage = reftree
 usage_Remote=Manage set of tracked repositories
 usage_RepositoryToReadFrom=Repository to read from
 usage_RepositoryToReceiveInto=Repository to receive into
@@ -414,6 +412,7 @@
 usage_showRefNamesMatchingCommits=Show ref names matching commits
 usage_showPatch=display patch
 usage_showNotes=Add this ref to the list of note branches from which notes are displayed
+usage_showSignature=Verify signatures of signed commits in the log
 usage_showTimeInMilliseconds=Show mtime in milliseconds
 usage_squash=Squash commits as if a real merge happened, but do not make a commit or move the HEAD.
 usage_srcPrefix=show the source prefix instead of "a/"
@@ -421,13 +420,19 @@
 usage_symbolicVersionForTheProject=Symbolic version for the project
 usage_tags=fetch all tags
 usage_notags=do not fetch tags
+usage_tagAnnotated=create an annotated tag, unsigned unless -s or -u are given, or config tag.gpgSign is true
 usage_tagDelete=delete tag
-usage_tagMessage=tag message
+usage_tagLocalUser=create a signed annotated tag using the specified GPG key ID
+usage_tagMessage=create an annotated tag with the given message, unsigned unless -s or -u are given, or config tag.gpgSign is true, or tar.forceSignAnnotated is true and -a is not given
+usage_tagSign=create a signed annotated tag
+usage_tagNoSign=suppress signing the tag
+usage_tagVerify=Verify the GPG signature
 usage_untrackedFilesMode=show untracked files
 usage_updateRef=reference to update
 usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository
 usage_useNameInsteadOfOriginToTrackUpstream=use <name> instead of 'origin' to track upstream
 usage_checkoutBranchAfterClone=check out named branch instead of remote's HEAD
+usage_initialBranch=initial branch of the newly created repository (default 'master', can be configured via config option init.defaultBranch)
 usage_viewCommitHistory=View commit history
 usage_orphan=Create a new orphan branch. The first commit made on this new branch will have no parents and it will be the root of a new history totally disconnected from other branches and commits.
 usernameFor=Username for {0}:
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java
index 8f80d6d..f28915d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java
@@ -18,6 +18,7 @@
 import org.eclipse.jgit.api.CloneCommand;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.InvalidRemoteException;
+import org.eclipse.jgit.api.errors.TransportException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.TextProgressMonitor;
@@ -50,6 +51,9 @@
 	@Option(name = "--recurse-submodules", usage = "usage_recurseSubmodules")
 	private boolean cloneSubmodules;
 
+	@Option(name = "--timeout", metaVar = "metaVar_seconds", usage = "usage_abortConnectionIfNoActivity")
+	int timeout = -1;
+
 	@Argument(index = 0, required = true, metaVar = "metaVar_uriish")
 	private String sourceUri;
 
@@ -90,9 +94,8 @@
 
 		CloneCommand command = Git.cloneRepository();
 		command.setURI(sourceUri).setRemote(remoteName).setBare(isBare)
-				.setMirror(isMirror)
-				.setNoCheckout(noCheckout).setBranch(branch)
-				.setCloneSubmodules(cloneSubmodules);
+				.setMirror(isMirror).setNoCheckout(noCheckout).setBranch(branch)
+				.setCloneSubmodules(cloneSubmodules).setTimeout(timeout);
 
 		command.setGitDir(gitdir == null ? null : new File(gitdir));
 		command.setDirectory(localNameF);
@@ -108,6 +111,8 @@
 			db = command.call().getRepository();
 			if (msgs && db.resolve(Constants.HEAD) == null)
 				outw.println(CLIText.get().clonedEmptyRepository);
+		} catch (TransportException e) {
+			throw die(e.getMessage(), e);
 		} catch (InvalidRemoteException e) {
 			throw die(MessageFormat.format(CLIText.get().doesNotExist,
 					sourceUri), e);
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java
index bf91025..f987f2c 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java
@@ -13,19 +13,12 @@
 import java.io.File;
 import java.io.IOException;
 import java.net.InetSocketAddress;
-import java.net.URISyntaxException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executors;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.ketch.KetchLeader;
-import org.eclipse.jgit.internal.ketch.KetchLeaderCache;
-import org.eclipse.jgit.internal.ketch.KetchPreReceive;
-import org.eclipse.jgit.internal.ketch.KetchSystem;
-import org.eclipse.jgit.internal.ketch.KetchText;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -33,10 +26,7 @@
 import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.DaemonClient;
 import org.eclipse.jgit.transport.DaemonService;
-import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.resolver.FileResolver;
-import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
-import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.SystemReader;
 import org.kohsuke.args4j.Argument;
@@ -71,13 +61,6 @@
 	@Option(name = "--export-all", usage = "usage_exportWithoutGitDaemonExportOk")
 	boolean exportAll;
 
-	@Option(name = "--ketch", metaVar = "metaVar_ketchServerType", usage = "usage_ketchServerType")
-	KetchServerType ketchServerType;
-
-	enum KetchServerType {
-		LEADER;
-	}
-
 	@Argument(required = true, metaVar = "metaVar_directory", usage = "usage_directoriesToExport")
 	List<File> directory = new ArrayList<>();
 
@@ -102,9 +85,9 @@
 			}
 			cfg = new FileBasedConfig(configFile, FS.DETECTED);
 		}
-		cfg.load();
-		new WindowCacheConfig().fromConfig(cfg).install();
-		packConfig.fromConfig(cfg);
+			cfg.load();
+			new WindowCacheConfig().fromConfig(cfg).install();
+			packConfig.fromConfig(cfg);
 
 		int threads = packConfig.getThreads();
 		if (threads <= 0)
@@ -137,9 +120,6 @@
 			service(d, n).setOverridable(true);
 		for (String n : forbidOverride)
 			service(d, n).setOverridable(false);
-		if (ketchServerType == KetchServerType.LEADER) {
-			startKetchLeader(d);
-		}
 		d.start();
 		outw.println(MessageFormat.format(CLIText.get().listeningOn, d.getAddress()));
 	}
@@ -162,24 +142,4 @@
 			throw die(MessageFormat.format(CLIText.get().serviceNotSupported, n));
 		return svc;
 	}
-
-	private void startKetchLeader(org.eclipse.jgit.transport.Daemon daemon) {
-		KetchSystem system = new KetchSystem();
-		final KetchLeaderCache leaders = new KetchLeaderCache(system);
-		final ReceivePackFactory<DaemonClient> factory;
-
-		factory = daemon.getReceivePackFactory();
-		daemon.setReceivePackFactory((DaemonClient req, Repository repo) -> {
-			ReceivePack rp = factory.create(req, repo);
-			KetchLeader leader;
-			try {
-				leader = leaders.get(repo);
-			} catch (URISyntaxException err) {
-				throw new ServiceNotEnabledException(
-						KetchText.get().invalidFollowerUri, err);
-			}
-			rp.setPreReceiveHook(new KetchPreReceive(leader));
-			return rp;
-		});
-	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Init.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Init.java
index 7f59ef4..7a0d96d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Init.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Init.java
@@ -24,6 +24,7 @@
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.util.StringUtils;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -32,6 +33,10 @@
 	@Option(name = "--bare", usage = "usage_CreateABareRepository")
 	private boolean bare;
 
+	@Option(name = "--initial-branch", aliases = { "-b" },
+			metaVar = "metaVar_branchName", usage = "usage_initialBranch")
+	private String branch;
+
 	@Argument(index = 0, metaVar = "metaVar_directory")
 	private String directory;
 
@@ -54,6 +59,9 @@
 		}
 		Repository repository;
 		try {
+			if (!StringUtils.isEmptyOrNull(branch)) {
+				command.setInitialBranch(branch);
+			}
 			repository = command.call().getRepository();
 			outw.println(MessageFormat.format(
 					CLIText.get().initializedEmptyGitRepositoryIn,
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
index 55efd23..353b64b 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2010, Google Inc.
- * Copyright (C) 2006-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2006, 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2008, 2021, 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
@@ -31,12 +31,17 @@
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.pgm.internal.VerificationUtils;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.util.GitDateFormatter;
@@ -68,6 +73,9 @@
 		additionalNoteRefs.add(notesRef);
 	}
 
+	@Option(name = "--show-signature", usage = "usage_showSignature")
+	private boolean showSignature;
+
 	@Option(name = "--date", usage = "usage_date")
 	void dateFormat(String date) {
 		if (date.toLowerCase(Locale.ROOT).equals(date))
@@ -147,6 +155,10 @@
 	// END -- Options shared with Diff
 
 
+	private GpgSignatureVerifier verifier;
+
+	private GpgConfig config;
+
 	Log() {
 		dateFormatter = new GitDateFormatter(Format.DEFAULT);
 	}
@@ -161,6 +173,7 @@
 	/** {@inheritDoc} */
 	@Override
 	protected void run() {
+		config = new GpgConfig(db.getConfig());
 		diffFmt.setRepository(db);
 		try {
 			diffFmt.setPathFilter(pathFilter);
@@ -197,6 +210,9 @@
 			throw die(e.getMessage(), e);
 		} finally {
 			diffFmt.close();
+			if (verifier != null) {
+				verifier.clear();
+			}
 		}
 	}
 
@@ -229,6 +245,9 @@
 		}
 		outw.println();
 
+		if (showSignature) {
+			showSignature(c);
+		}
 		final PersonIdent author = c.getAuthorIdent();
 		outw.println(MessageFormat.format(CLIText.get().authorInfo, author.getName(), author.getEmailAddress()));
 		outw.println(MessageFormat.format(CLIText.get().dateInfo,
@@ -252,6 +271,27 @@
 		outw.flush();
 	}
 
+	private void showSignature(RevCommit c) throws IOException {
+		if (c.getRawGpgSignature() == null) {
+			return;
+		}
+		if (verifier == null) {
+			GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory
+					.getDefault();
+			if (factory == null) {
+				throw die(CLIText.get().logNoSignatureVerifier, null);
+			}
+			verifier = factory.getVerifier();
+		}
+		SignatureVerification verification = verifier.verifySignature(c,
+				config);
+		if (verification == null) {
+			return;
+		}
+		VerificationUtils.writeVerification(outw, verification,
+				verifier.getName(), c.getCommitterIdent());
+	}
+
 	/**
 	 * @param c
 	 * @return <code>true</code> if at least one note was printed,
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsRemote.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsRemote.java
index 055b48a..83446cc 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsRemote.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsRemote.java
@@ -79,7 +79,7 @@
 
 	private void show(Ref ref, String name)
 			throws IOException {
-		outw.print("ref: ");
+		outw.print("ref: "); //$NON-NLS-1$
 		outw.print(ref.getName());
 		outw.print('\t');
 		outw.print(name);
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
index 5f9551e..3beab60 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
@@ -29,10 +29,15 @@
 import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
 import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.pgm.internal.VerificationUtils;
 import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -41,6 +46,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.RawParseUtils;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -58,6 +64,9 @@
 	@Option(name = "--", metaVar = "metaVar_path", handler = PathTreeFilterHandler.class)
 	protected TreeFilter pathFilter = TreeFilter.ALL;
 
+	@Option(name = "--show-signature", usage = "usage_showSignature")
+	private boolean showSignature;
+
 	// BEGIN -- Options shared with Diff
 	@Option(name = "-p", usage = "usage_showPatch")
 	boolean showPatch;
@@ -219,13 +228,20 @@
 		}
 
 		outw.println();
-		final String[] lines = tag.getFullMessage().split("\n"); //$NON-NLS-1$
-		for (String s : lines) {
-			outw.print("    "); //$NON-NLS-1$
-			outw.print(s);
-			outw.println();
+		String fullMessage = tag.getFullMessage();
+		if (!fullMessage.isEmpty()) {
+			String[] lines = tag.getFullMessage().split("\n"); //$NON-NLS-1$
+			for (String s : lines) {
+				outw.println(s);
+			}
 		}
-
+		byte[] rawSignature = tag.getRawGpgSignature();
+		if (rawSignature != null) {
+			String[] lines = RawParseUtils.decode(rawSignature).split("\n"); //$NON-NLS-1$
+			for (String s : lines) {
+				outw.println(s);
+			}
+		}
 		outw.println();
 	}
 
@@ -253,6 +269,10 @@
 		c.getId().copyTo(outbuffer, outw);
 		outw.println();
 
+		if (showSignature) {
+			showSignature(c);
+		}
+
 		final PersonIdent author = c.getAuthorIdent();
 		outw.println(MessageFormat.format(CLIText.get().authorInfo,
 				author.getName(), author.getEmailAddress()));
@@ -291,4 +311,28 @@
 		}
 		outw.println();
 	}
+
+	private void showSignature(RevCommit c) throws IOException {
+		if (c.getRawGpgSignature() == null) {
+			return;
+		}
+		GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory
+				.getDefault();
+		if (factory == null) {
+			throw die(CLIText.get().logNoSignatureVerifier, null);
+		}
+		GpgSignatureVerifier verifier = factory.getVerifier();
+		GpgConfig config = new GpgConfig(db.getConfig());
+		try {
+			SignatureVerification verification = verifier.verifySignature(c,
+					config);
+			if (verification == null) {
+				return;
+			}
+			VerificationUtils.writeVerification(outw, verification,
+					verifier.getName(), c.getCommitterIdent());
+		} finally {
+			verifier.clear();
+		}
+	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
index b408b78..e2cd31d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
@@ -4,7 +4,7 @@
  * Copyright (C) 2008, Charles O'Farrell <charleso@charleso.org>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg.lists@dewire.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2021 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
@@ -22,26 +22,59 @@
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.ListTagCommand;
 import org.eclipse.jgit.api.TagCommand;
+import org.eclipse.jgit.api.VerificationResult;
+import org.eclipse.jgit.api.VerifySignatureCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.pgm.internal.CLIText;
+import org.eclipse.jgit.pgm.internal.VerificationUtils;
+import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
 @Command(common = true, usage = "usage_CreateATag")
 class Tag extends TextBuiltin {
-	@Option(name = "-f", usage = "usage_forceReplacingAnExistingTag")
+
+	@Option(name = "--force", aliases = { "-f" }, forbids = { "--delete",
+			"--verify" }, usage = "usage_forceReplacingAnExistingTag")
 	private boolean force;
 
-	@Option(name = "-d", usage = "usage_tagDelete")
+	@Option(name = "--delete", aliases = { "-d" }, forbids = {
+			"--verify" }, usage = "usage_tagDelete")
 	private boolean delete;
 
-	@Option(name = "-m", metaVar = "metaVar_message", usage = "usage_tagMessage")
-	private String message = ""; //$NON-NLS-1$
+	@Option(name = "--annotate", aliases = {
+			"-a" }, forbids = { "--delete",
+					"--verify" }, usage = "usage_tagAnnotated")
+	private boolean annotated;
+
+	@Option(name = "-m", forbids = { "--delete",
+			"--verify" }, metaVar = "metaVar_message", usage = "usage_tagMessage")
+	private String message;
+
+	@Option(name = "--sign", aliases = { "-s" }, forbids = {
+			"--no-sign", "--delete", "--verify" }, usage = "usage_tagSign")
+	private boolean sign;
+
+	@Option(name = "--no-sign", usage = "usage_tagNoSign", forbids = {
+			"--sign", "--delete", "--verify" })
+	private boolean noSign;
+
+	@Option(name = "--local-user", aliases = {
+			"-u" }, forbids = { "--delete",
+					"--verify" }, metaVar = "metaVar_tagLocalUser", usage = "usage_tagLocalUser")
+	private String gpgKeyId;
+
+	@Option(name = "--verify", aliases = { "-v" }, forbids = { "--delete",
+			"--force", "--annotate", "-m", "--sign", "--no-sign",
+			"--local-user" }, usage = "usage_tagVerify")
+	private boolean verify;
 
 	@Argument(index = 0, metaVar = "metaVar_name")
 	private String tagName;
@@ -54,7 +87,25 @@
 	protected void run() {
 		try (Git git = new Git(db)) {
 			if (tagName != null) {
-				if (delete) {
+				if (verify) {
+					VerifySignatureCommand verifySig = git.verifySignature()
+							.setMode(VerifySignatureCommand.VerifyMode.TAGS)
+							.addName(tagName);
+
+					VerificationResult verification = verifySig.call()
+							.get(tagName);
+					if (verification == null) {
+						showUnsigned(git, tagName);
+					} else {
+						Throwable error = verification.getException();
+						if (error != null) {
+							throw die(error.getMessage(), error);
+						}
+						writeVerification(verifySig.getVerifier().getName(),
+								(RevTag) verification.getObject(),
+								verification.getVerification());
+					}
+				} else if (delete) {
 					List<String> deletedTags = git.tagDelete().setTags(tagName)
 							.call();
 					if (deletedTags.isEmpty()) {
@@ -70,6 +121,18 @@
 							command.setObjectId(walk.parseAny(object));
 						}
 					}
+					if (noSign) {
+						command.setSigned(false);
+					} else if (sign) {
+						command.setSigned(true);
+					}
+					if (annotated) {
+						command.setAnnotated(true);
+					} else if (message == null && !sign && gpgKeyId == null) {
+						// None of -a, -m, -s, -u given
+						command.setAnnotated(false);
+					}
+					command.setSigningKey(gpgKeyId);
 					try {
 						command.call();
 					} catch (RefAlreadyExistsException e) {
@@ -88,4 +151,36 @@
 			throw die(e.getMessage(), e);
 		}
 	}
+
+	private void showUnsigned(Git git, String wantedTag) throws IOException {
+		ObjectId id = git.getRepository().resolve(wantedTag);
+		if (id != null && !ObjectId.zeroId().equals(id)) {
+			try (RevWalk walk = new RevWalk(git.getRepository())) {
+				showTag(walk.parseTag(id));
+			}
+		} else {
+			throw die(
+					MessageFormat.format(CLIText.get().tagNotFound, wantedTag));
+		}
+	}
+
+	private void showTag(RevTag tag) throws IOException {
+		outw.println("object " + tag.getObject().name()); //$NON-NLS-1$
+		outw.println("type " + Constants.typeString(tag.getObject().getType())); //$NON-NLS-1$
+		outw.println("tag " + tag.getTagName()); //$NON-NLS-1$
+		outw.println("tagger " + tag.getTaggerIdent().toExternalString()); //$NON-NLS-1$
+		outw.println();
+		outw.print(tag.getFullMessage());
+	}
+
+	private void writeVerification(String name, RevTag tag,
+			SignatureVerification verification) throws IOException {
+		showTag(tag);
+		if (verification == null) {
+			outw.println();
+			return;
+		}
+		VerificationUtils.writeVerification(outw, verification, name,
+				tag.getTaggerIdent());
+	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
index 0b02dd1..f70e72d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
@@ -63,7 +63,7 @@
 	private boolean help;
 
 	@Option(name = "--ssh", usage = "usage_sshDriver")
-	private SshDriver sshDriver = SshDriver.JSCH;
+	private SshDriver sshDriver = SshDriver.APACHE;
 
 	/**
 	 * Input stream, typically this is standard input.
@@ -220,7 +220,7 @@
 			SshdSessionFactory factory = new SshdSessionFactory(
 					new JGitKeyCache(), new DefaultProxyDataFactory());
 			Runtime.getRuntime()
-					.addShutdownHook(new Thread(() -> factory.close()));
+					.addShutdownHook(new Thread(factory::close));
 			SshSessionFactory.setInstance(factory);
 			break;
 		}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/BenchmarkReftable.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/BenchmarkReftable.java
index 630fac5..f23f4cf 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/BenchmarkReftable.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/BenchmarkReftable.java
@@ -23,7 +23,9 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.internal.storage.file.FileReftableStack;
 import org.eclipse.jgit.internal.storage.io.BlockSource;
@@ -47,6 +49,7 @@
 		SEEK_COLD, SEEK_HOT,
 		BY_ID_COLD, BY_ID_HOT,
 		WRITE_STACK,
+		GET_REFS_EXCLUDING_REF
 	}
 
 	@Option(name = "--tries")
@@ -91,7 +94,11 @@
 		case WRITE_STACK:
 			writeStack();
 			break;
-		}
+		case GET_REFS_EXCLUDING_REF :
+			getRefsExcludingWithSeekPast(ref);
+			getRefsExcludingWithFilter(ref);
+			break;
+	}
 	}
 
 	private void printf(String fmt, Object... args) throws IOException {
@@ -315,4 +322,49 @@
 		printf("%12s %10d usec  %9.1f usec/run  %5d runs", "reftable",
 				tot / 1000, (((double) tot) / tries) / 1000, tries);
 	}
+
+	@SuppressWarnings({"nls", "boxing"})
+	private void getRefsExcludingWithFilter(String prefix) throws Exception {
+		long startTime = System.nanoTime();
+		List<Ref> allRefs = new ArrayList<>();
+		try (FileInputStream in = new FileInputStream(reftablePath);
+				BlockSource src = BlockSource.from(in);
+				ReftableReader reader = new ReftableReader(src)) {
+			try (RefCursor rc = reader.allRefs()) {
+				while (rc.next()) {
+					allRefs.add(rc.getRef());
+				}
+			}
+		}
+		int total = allRefs.size();
+		allRefs = allRefs.stream().filter(r -> r.getName().startsWith(prefix)).collect(Collectors.toList());
+		int notStartWithPrefix = allRefs.size();
+		int startWithPrefix = total - notStartWithPrefix;
+		long totalTime = System.nanoTime() - startTime;
+		printf("total time the action took using filter: %10d usec", totalTime / 1000);
+		printf("number of refs that start with prefix: %d", startWithPrefix);
+		printf("number of refs that don't start with prefix: %d", notStartWithPrefix);
+	}
+
+	@SuppressWarnings({"nls", "boxing"})
+	private void getRefsExcludingWithSeekPast(String prefix) throws Exception {
+		long start = System.nanoTime();
+		try (FileInputStream in = new FileInputStream(reftablePath);
+				BlockSource src = BlockSource.from(in);
+				ReftableReader reader = new ReftableReader(src)) {
+			try (RefCursor rc = reader.allRefs()) {
+				while (rc.next()) {
+					if (rc.getRef().getName().startsWith(prefix)) {
+						break;
+					}
+				}
+				rc.seekPastPrefix(prefix);
+				while (rc.next()) {
+					rc.getRef();
+				}
+			}
+		}
+		long tot = System.nanoTime() - start;
+		printf("total time the action took using seek: %10d usec", tot / 1000);
+	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java
deleted file mode 100644
index 38951ba..0000000
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2015, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.pgm.debug;
-
-import static org.eclipse.jgit.lib.Constants.HEAD;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jgit.internal.storage.reftree.RefTree;
-import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ConfigConstants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.pgm.Command;
-import org.eclipse.jgit.pgm.TextBuiltin;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.kohsuke.args4j.Option;
-
-@Command(usage = "usage_RebuildRefTree")
-class RebuildRefTree extends TextBuiltin {
-	@Option(name = "--enable", usage = "usage_RebuildRefTreeEnable")
-	boolean enable;
-
-	private String txnNamespace;
-	private String txnCommitted;
-
-	/** {@inheritDoc} */
-	@Override
-	protected void run() throws Exception {
-		try (ObjectReader reader = db.newObjectReader();
-				RevWalk rw = new RevWalk(reader);
-				ObjectInserter inserter = db.newObjectInserter()) {
-			RefDatabase refDb = db.getRefDatabase();
-			if (refDb instanceof RefTreeDatabase) {
-				RefTreeDatabase d = (RefTreeDatabase) refDb;
-				refDb = d.getBootstrap();
-				txnNamespace = d.getTxnNamespace();
-				txnCommitted = d.getTxnCommitted();
-			} else {
-				RefTreeDatabase d = new RefTreeDatabase(db, refDb);
-				txnNamespace = d.getTxnNamespace();
-				txnCommitted = d.getTxnCommitted();
-			}
-
-			errw.format("Rebuilding %s from %s", //$NON-NLS-1$
-					txnCommitted, refDb.getClass().getSimpleName());
-			errw.println();
-			errw.flush();
-
-			CommitBuilder b = new CommitBuilder();
-			Ref ref = refDb.exactRef(txnCommitted);
-			RefUpdate update = refDb.newUpdate(txnCommitted, true);
-			ObjectId oldTreeId;
-
-			if (ref != null && ref.getObjectId() != null) {
-				ObjectId oldId = ref.getObjectId();
-				update.setExpectedOldObjectId(oldId);
-				b.setParentId(oldId);
-				oldTreeId = rw.parseCommit(oldId).getTree();
-			} else {
-				update.setExpectedOldObjectId(ObjectId.zeroId());
-				oldTreeId = ObjectId.zeroId();
-			}
-
-			RefTree tree = rebuild(refDb);
-			b.setTreeId(tree.writeTree(inserter));
-			b.setAuthor(new PersonIdent(db));
-			b.setCommitter(b.getAuthor());
-			if (b.getTreeId().equals(oldTreeId)) {
-				return;
-			}
-
-			update.setNewObjectId(inserter.insert(b));
-			inserter.flush();
-
-			RefUpdate.Result result = update.update(rw);
-			switch (result) {
-			case NEW:
-			case FAST_FORWARD:
-				break;
-			default:
-				throw die(String.format("%s: %s", update.getName(), result)); //$NON-NLS-1$
-			}
-
-			if (enable && !(db.getRefDatabase() instanceof RefTreeDatabase)) {
-				StoredConfig cfg = db.getConfig();
-				cfg.setInt(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 1);
-				cfg.setString(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
-						ConfigConstants.CONFIG_KEY_REFSTORAGE,
-						ConfigConstants.CONFIG_REFSTORAGE_REFTREE);
-				cfg.save();
-				errw.println("Enabled reftree."); //$NON-NLS-1$
-				errw.flush();
-			}
-		}
-	}
-
-	private RefTree rebuild(RefDatabase refdb) throws IOException {
-		RefTree tree = RefTree.newEmptyTree();
-		List<org.eclipse.jgit.internal.storage.reftree.Command> cmds
-			= new ArrayList<>();
-
-		Ref head = refdb.exactRef(HEAD);
-		if (head != null) {
-			cmds.add(new org.eclipse.jgit.internal.storage.reftree.Command(
-					null,
-					head));
-		}
-
-		for (Ref r : refdb.getRefs()) {
-			if (r.getName().equals(txnCommitted) || r.getName().equals(HEAD)
-					|| r.getName().startsWith(txnNamespace)) {
-				continue;
-			}
-			cmds.add(new org.eclipse.jgit.internal.storage.reftree.Command(
-					null,
-					db.getRefDatabase().peel(r)));
-		}
-		tree.apply(cmds);
-		return tree;
-	}
-}
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 c68019e..991b3ba 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
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2010, 2013 Sasa Zivkov <sasa.zivkov@sap.com>
- * Copyright (C) 2013, Obeo and others
+ * Copyright (C) 2013, 2021 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
@@ -163,6 +163,7 @@
 	/***/ public String lfsUnknownStoreType;
 	/***/ public String lineFormat;
 	/***/ public String listeningOn;
+	/***/ public String logNoSignatureVerifier;
 	/***/ public String mergeCheckoutConflict;
 	/***/ public String mergeConflict;
 	/***/ public String mergeFailed;
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/VerificationUtils.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/VerificationUtils.java
new file mode 100644
index 0000000..c1f8a86
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/VerificationUtils.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.pgm.internal;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.util.GitDateFormatter;
+import org.eclipse.jgit.util.SignatureUtils;
+import org.eclipse.jgit.util.io.ThrowingPrintWriter;
+
+/**
+ * Utilities for signature verification.
+ */
+public final class VerificationUtils {
+
+	private VerificationUtils() {
+		// No instantiation
+	}
+
+	/**
+	 * Writes information about a signature verification to the given writer.
+	 *
+	 * @param out
+	 *            to write to
+	 * @param verification
+	 *            to show
+	 * @param name
+	 *            of the verifier used
+	 * @param creator
+	 *            of the object verified; used for time zone information
+	 * @throws IOException
+	 *             if writing fails
+	 */
+	public static void writeVerification(ThrowingPrintWriter out,
+			SignatureVerification verification, String name,
+			PersonIdent creator) throws IOException {
+		String[] text = SignatureUtils
+				.toString(verification, creator,
+						new GitDateFormatter(GitDateFormatter.Format.LOCALE))
+				.split("\n"); //$NON-NLS-1$
+		for (String line : text) {
+			out.print(name);
+			out.print(": "); //$NON-NLS-1$
+			out.println(line);
+		}
+	}
+}
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 c7ea7b6..0d83eb3 100644
--- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
@@ -3,30 +3,32 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.apache.test
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.test
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.apache.sshd.client.config.hosts;version="[2.4.0,2.5.0)",
- org.apache.sshd.common;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.auth;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.config.keys;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.helpers;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.keyprovider;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.session;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.net;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.security;version="[2.4.0,2.5.0)",
- org.apache.sshd.server;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.forward;version="[2.4.0,2.5.0)",
- org.eclipse.jgit.api;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.transport.sshd.proxy;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit.ssh;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.sshd;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+Import-Package: org.apache.sshd.client.config.hosts;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.auth;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.config.keys;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.helpers;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.keyprovider;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.session;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.signature;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.net;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.security;version="[2.6.0,2.7.0)",
+ org.apache.sshd.core;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.forward;version="[2.6.0,2.7.0)",
+ org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.transport.sshd.proxy;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit.ssh;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.sshd;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)"
diff --git a/org.eclipse.jgit.ssh.apache.test/build.properties b/org.eclipse.jgit.ssh.apache.test/build.properties
index 9ffa0ca..406c5a7 100644
--- a/org.eclipse.jgit.ssh.apache.test/build.properties
+++ b/org.eclipse.jgit.ssh.apache.test/build.properties
@@ -3,3 +3,5 @@
 bin.includes = META-INF/,\
                .,\
                plugin.properties
+additional.bundles = org.apache.log4j,\
+                     org.slf4j.binding.log4j12
diff --git a/org.eclipse.jgit.ssh.apache.test/pom.xml b/org.eclipse.jgit.ssh.apache.test/pom.xml
index 824724e..67ac7c1 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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/ApacheSshProtocol2Test.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshProtocol2Test.java
new file mode 100644
index 0000000..0ad96b9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshProtocol2Test.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2020 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.sshd;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+
+import org.eclipse.jgit.junit.ssh.SshBasicTestBase;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.util.FS;
+
+public class ApacheSshProtocol2Test extends SshBasicTestBase {
+
+	@Override
+	protected SshSessionFactory createSessionFactory() {
+		SshdSessionFactory result = new SshdSessionFactory(new JGitKeyCache(),
+				null);
+		// The home directory is mocked at this point!
+		result.setHomeDirectory(FS.DETECTED.userHome());
+		result.setSshDirectory(sshDir);
+		return result;
+	}
+
+	@Override
+	protected void installConfig(String... config) {
+		File configFile = new File(sshDir, Constants.CONFIG);
+		if (config != null) {
+			try {
+				Files.write(configFile.toPath(), Arrays.asList(config));
+			} catch (IOException e) {
+				throw new UncheckedIOException(e);
+			}
+		}
+	}
+
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		StoredConfig config = ((Repository) db).getConfig();
+		config.setInt("protocol", null, "version", 2);
+		config.save();
+	}
+}
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 3427da6..c56d230 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
@@ -9,6 +9,7 @@
  */
 package org.eclipse.jgit.transport.sshd;
 
+import static org.apache.sshd.core.CoreModuleProperties.MAX_CONCURRENT_SESSIONS;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -33,7 +34,6 @@
 
 import org.apache.sshd.client.config.hosts.KnownHostEntry;
 import org.apache.sshd.client.config.hosts.KnownHostHashValue;
-import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntry;
@@ -41,14 +41,15 @@
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.ServerAuthenticationManager;
-import org.apache.sshd.server.ServerFactoryManager;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.forward.StaticDecisionForwardingFilter;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.TransportException;
 import org.eclipse.jgit.junit.ssh.SshTestBase;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.transport.RemoteSession;
 import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 import org.junit.experimental.theories.Theories;
@@ -216,8 +217,8 @@
 	 */
 	@Test
 	public void testCloneAndFetchWithSessionLimit() throws Exception {
-		PropertyResolverUtils.updateProperty(server.getPropertyResolver(),
-				ServerFactoryManager.MAX_CONCURRENT_SESSIONS, 2);
+		MAX_CONCURRENT_SESSIONS
+				.set(server.getPropertyResolver(), Integer.valueOf(2));
 		File localClone = cloneWith("ssh://localhost/doesntmatter",
 				defaultCloneDir, null, //
 				"Host localhost", //
@@ -233,6 +234,61 @@
 	}
 
 	/**
+	 * Creates a simple SSH server without git setup.
+	 *
+	 * @param user
+	 *            to accept
+	 * @param userKey
+	 *            public key of that user at this server
+	 * @return the {@link SshServer}, not yet started
+	 * @throws Exception
+	 */
+	private SshServer createServer(String user, File userKey) throws Exception {
+		SshServer srv = SshServer.setUpDefaultServer();
+		// Give the server its own host key
+		KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+		generator.initialize(2048);
+		KeyPair proxyHostKey = generator.generateKeyPair();
+		srv.setKeyPairProvider(
+				session -> Collections.singletonList(proxyHostKey));
+		// Allow (only) publickey authentication
+		srv.setUserAuthFactories(Collections.singletonList(
+				ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY));
+		// Install the user's public key
+		PublicKey userProxyKey = AuthorizedKeyEntry
+				.readAuthorizedKeys(userKey.toPath()).get(0)
+				.resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
+		srv.setPublickeyAuthenticator(
+				(userName, publicKey, session) -> user.equals(userName)
+						&& KeyUtils.compareKeys(userProxyKey, publicKey));
+		return srv;
+	}
+
+	/**
+	 * Writes the server's host key to our knownhosts file.
+	 *
+	 * @param srv to register
+	 * @throws Exception
+	 */
+	private void registerServer(SshServer srv) throws Exception {
+		// Add the proxy's host key to knownhosts
+		try (BufferedWriter writer = Files.newBufferedWriter(
+				knownHosts.toPath(), StandardCharsets.US_ASCII,
+				StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
+			writer.append('\n');
+			KnownHostHashValue.appendHostPattern(writer, "localhost",
+					srv.getPort());
+			writer.append(',');
+			KnownHostHashValue.appendHostPattern(writer, "127.0.0.1",
+					srv.getPort());
+			writer.append(' ');
+			PublicKeyEntry.appendPublicKeyEntry(writer,
+					srv.getKeyPairProvider().loadKeys(null).iterator().next().getPublic());
+			writer.append('\n');
+		}
+	}
+
+	/**
 	 * Creates a simple proxy server. Accepts only publickey authentication from
 	 * the given user with the given key, allows all forwardings. Adds the
 	 * proxy's host key to {@link #knownHosts}.
@@ -248,23 +304,7 @@
 	 */
 	private SshServer createProxy(String user, File userKey,
 			SshdSocketAddress[] report) throws Exception {
-		SshServer proxy = SshServer.setUpDefaultServer();
-		// Give the server its own host key
-		KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
-		generator.initialize(2048);
-		KeyPair proxyHostKey = generator.generateKeyPair();
-		proxy.setKeyPairProvider(
-				session -> Collections.singletonList(proxyHostKey));
-		// Allow (only) publickey authentication
-		proxy.setUserAuthFactories(Collections.singletonList(
-				ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY));
-		// Install the user's public key
-		PublicKey userProxyKey = AuthorizedKeyEntry
-				.readAuthorizedKeys(userKey.toPath()).get(0)
-				.resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
-		proxy.setPublickeyAuthenticator(
-				(userName, publicKey, session) -> user.equals(userName)
-						&& KeyUtils.compareKeys(userProxyKey, publicKey));
+		SshServer proxy = createServer(user, userKey);
 		// Allow forwarding
 		proxy.setForwardingFilter(new StaticDecisionForwardingFilter(true) {
 
@@ -276,21 +316,7 @@
 			}
 		});
 		proxy.start();
-		// Add the proxy's host key to knownhosts
-		try (BufferedWriter writer = Files.newBufferedWriter(
-				knownHosts.toPath(), StandardCharsets.US_ASCII,
-				StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
-			writer.append('\n');
-			KnownHostHashValue.appendHostPattern(writer, "localhost",
-					proxy.getPort());
-			writer.append(',');
-			KnownHostHashValue.appendHostPattern(writer, "127.0.0.1",
-					proxy.getPort());
-			writer.append(' ');
-			PublicKeyEntry.appendPublicKeyEntry(writer,
-					proxyHostKey.getPublic());
-			writer.append('\n');
-		}
+		registerServer(proxy);
 		return proxy;
 	}
 
@@ -607,4 +633,73 @@
 			}
 		}
 	}
+
+	/**
+	 * Tests that one can log in to an old server that doesn't handle
+	 * rsa-sha2-512 if one puts ssh-rsa first in the client's list of public key
+	 * signature algorithms.
+	 *
+	 * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">bug
+	 *      572056</a>
+	 * @throws Exception
+	 *             on failure
+	 */
+	@Test
+	public void testConnectAuthSshRsaPubkeyAcceptedAlgorithms()
+			throws Exception {
+		try (SshServer oldServer = createServer(TEST_USER, publicKey1)) {
+			oldServer.setSignatureFactoriesNames("ssh-rsa");
+			oldServer.start();
+			registerServer(oldServer);
+			installConfig("Host server", //
+					"HostName localhost", //
+					"Port " + oldServer.getPort(), //
+					"User " + TEST_USER, //
+					"IdentityFile " + privateKey1.getAbsolutePath(), //
+					"PubkeyAcceptedAlgorithms ^ssh-rsa");
+			RemoteSession session = getSessionFactory().getSession(
+					new URIish("ssh://server/doesntmatter"), null, FS.DETECTED,
+					10000);
+			assertNotNull(session);
+			session.disconnect();
+		}
+	}
+
+	/**
+	 * Tests that one can log in to an old server that knows only the ssh-rsa
+	 * signature algorithm. The client has by default the list of signature
+	 * algorithms for RSA as "rsa-sha2-512,rsa-sha2-256,ssh-rsa". It should try
+	 * all three with the single key configured, and finally succeed.
+	 * <p>
+	 * The re-ordering mechanism (see
+	 * {@link #testConnectAuthSshRsaPubkeyAcceptedAlgorithms()}) is still
+	 * important; servers may impose a penalty (back-off delay) for subsequent
+	 * attempts with signature algorithms unknown to the server. So a user
+	 * connecting to such a server and noticing delays may still want to put
+	 * ssh-rsa first in the list for that host.
+	 * </p>
+	 *
+	 * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">bug
+	 *      572056</a>
+	 * @throws Exception
+	 *             on failure
+	 */
+	@Test
+	public void testConnectAuthSshRsa() throws Exception {
+		try (SshServer oldServer = createServer(TEST_USER, publicKey1)) {
+			oldServer.setSignatureFactoriesNames("ssh-rsa");
+			oldServer.start();
+			registerServer(oldServer);
+			installConfig("Host server", //
+					"HostName localhost", //
+					"Port " + oldServer.getPort(), //
+					"User " + TEST_USER, //
+					"IdentityFile " + privateKey1.getAbsolutePath());
+			RemoteSession session = getSessionFactory().getSession(
+					new URIish("ssh://server/doesntmatter"), null, FS.DETECTED,
+					10000);
+			assertNotNull(session);
+			session.disconnect();
+		}
+	}
 }
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
index 914f684..8fce3c5 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Export-Package: org.eclipse.jgit.internal.transport.sshd;version="5.10.1";x-internal:=true;
+Export-Package: org.eclipse.jgit.internal.transport.sshd;version="5.11.2";x-internal:=true;
   uses:="org.apache.sshd.client,
    org.apache.sshd.client.auth,
    org.apache.sshd.client.auth.keyboard,
@@ -23,9 +23,9 @@
    org.apache.sshd.common.signature,
    org.apache.sshd.common.util.buffer,
    org.eclipse.jgit.transport",
- org.eclipse.jgit.internal.transport.sshd.auth;version="5.10.1";x-internal:=true,
- org.eclipse.jgit.internal.transport.sshd.proxy;version="5.10.1";x-friends:="org.eclipse.jgit.ssh.apache.test",
- org.eclipse.jgit.transport.sshd;version="5.10.1";
+ org.eclipse.jgit.internal.transport.sshd.auth;version="5.11.2";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.proxy;version="5.11.2";x-friends:="org.eclipse.jgit.ssh.apache.test",
+ org.eclipse.jgit.transport.sshd;version="5.11.2";
   uses:="org.eclipse.jgit.transport,
    org.apache.sshd.client.config.hosts,
    org.apache.sshd.common.keyprovider,
@@ -33,55 +33,57 @@
    org.apache.sshd.client.session,
    org.apache.sshd.client.keyverifier"
 Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)",
- org.apache.sshd.agent;version="[2.4.0,2.5.0)",
- org.apache.sshd.client;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.auth;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.auth.keyboard;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.auth.password;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.auth.pubkey;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.channel;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.config.hosts;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.config.keys;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.future;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.keyverifier;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.session;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.session.forward;version="[2.4.0,2.5.0)",
- org.apache.sshd.client.subsystem.sftp;version="[2.4.0,2.5.0)",
- org.apache.sshd.common;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.auth;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.channel;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.compression;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.config.keys;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.config.keys.loader;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.config.keys.loader.openssh.kdf;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.digest;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.forward;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.future;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.helpers;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.io;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.kex;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.keyprovider;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.mac;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.random;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.session;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.session.helpers;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.signature;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.subsystem.sftp;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.buffer;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.closeable;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.io;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.io.resource;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.logging;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.net;version="[2.4.0,2.5.0)",
- org.apache.sshd.common.util.security;version="[2.4.0,2.5.0)",
- org.apache.sshd.server.auth;version="[2.4.0,2.5.0)",
- org.eclipse.jgit.annotations;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.fnmatch;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+ org.apache.sshd.agent;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.auth;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.auth.keyboard;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.auth.password;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.auth.pubkey;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.channel;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.config.hosts;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.config.keys;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.future;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.keyverifier;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.session;version="[2.6.0,2.7.0)",
+ org.apache.sshd.client.session.forward;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.auth;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.channel;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.compression;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.config.keys;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.config.keys.loader;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.config.keys.loader.openssh.kdf;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.digest;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.forward;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.future;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.helpers;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.io;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.kex;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.keyprovider;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.mac;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.random;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.session;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.session.helpers;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.signature;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.buffer;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.closeable;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.io;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.io.resource;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.logging;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.net;version="[2.6.0,2.7.0)",
+ org.apache.sshd.common.util.security;version="[2.6.0,2.7.0)",
+ org.apache.sshd.core;version="[2.6.0,2.7.0)",
+ org.apache.sshd.server.auth;version="[2.6.0,2.7.0)",
+ org.apache.sshd.sftp;version="[2.6.0,2.7.0)",
+ org.apache.sshd.sftp.client;version="[2.6.0,2.7.0)",
+ org.apache.sshd.sftp.common;version="[2.6.0,2.7.0)",
+ org.eclipse.jgit.annotations;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.fnmatch;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.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 c956f02..4a196fe 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.apache/pom.xml b/org.eclipse.jgit.ssh.apache/pom.xml
index 1d93fb0..aca0dd4 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
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 f810fd4..16b5738 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
@@ -5,8 +5,7 @@
 configInvalidPattern=Invalid pattern in ssh config key {0}: {1}
 configInvalidPositive=Ssh config entry {0} must be a strictly positive number but is ''{1}''
 configInvalidProxyJump=Ssh config, host ''{0}'': Cannot parse ProxyJump ''{1}''
-configNoKnownHostKeyAlgorithms=No implementations for any of the algorithms ''{0}'' given in HostKeyAlgorithms in the ssh config; using the default.
-configNoRemainingHostKeyAlgorithms=Ssh config removed all host key algorithms: HostKeyAlgorithms ''{0}''
+configNoKnownAlgorithms=Ssh config ''{0}'' ''{1}'' resulted in empty list (none known, or all known removed); using default.
 configProxyJumpNotSsh=Non-ssh URI in ProxyJump ssh config
 configProxyJumpWithPath=ProxyJump ssh config: jump host specification must not have a path
 ftpCloseFailed=Closing the SFTP channel failed
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
index 0d6f302..8183a92 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
@@ -10,6 +10,7 @@
 package org.eclipse.jgit.internal.transport.sshd;
 
 import static java.text.MessageFormat.format;
+import static org.apache.sshd.core.CoreModuleProperties.MAX_IDENTIFICATION_SIZE;
 
 import java.io.IOException;
 import java.io.StreamCorruptedException;
@@ -20,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -29,19 +31,14 @@
 
 import org.apache.sshd.client.ClientFactoryManager;
 import org.apache.sshd.client.config.hosts.HostConfigEntry;
-import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSessionImpl;
-import org.apache.sshd.client.session.ClientUserAuthService;
 import org.apache.sshd.common.AttributeRepository;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.PropertyResolver;
-import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.io.IoWriteFuture;
-import org.apache.sshd.common.kex.KexState;
 import org.apache.sshd.common.util.Readable;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.eclipse.jgit.errors.InvalidPatternException;
@@ -49,6 +46,7 @@
 import org.eclipse.jgit.internal.transport.sshd.proxy.StatefulProxyConnector;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.transport.SshConstants;
+import org.eclipse.jgit.util.StringUtils;
 
 /**
  * A {@link org.apache.sshd.client.session.ClientSession ClientSession} that can
@@ -66,7 +64,8 @@
 	 * protocol version exchange. 64kb is what OpenSSH < 8.0 read; OpenSSH 8.0
 	 * changed it to 8Mb, but that seems excessive for the purpose stated in RFC
 	 * 4253. The Apache MINA sshd default in
-	 * {@link FactoryManager#DEFAULT_MAX_IDENTIFICATION_SIZE} is 16kb.
+	 * {@link org.apache.sshd.core.CoreModuleProperties#MAX_IDENTIFICATION_SIZE}
+	 * is 16kb.
 	 */
 	private static final int DEFAULT_MAX_IDENTIFICATION_SIZE = 64 * 1024;
 
@@ -77,17 +76,6 @@
 	private volatile StatefulProxyConnector proxyHandler;
 
 	/**
-	 * Work-around for bug 565394 / SSHD-1050; remove when using sshd 2.6.0.
-	 */
-	private volatile AuthFuture authFuture;
-
-	/** Records exceptions before there is an authFuture. */
-	private List<Throwable> earlyErrors = new ArrayList<>();
-
-	/** Guards setting an earlyError and the authFuture together. */
-	private final Object errorLock = new Object();
-
-	/**
 	 * @param manager
 	 * @param session
 	 * @throws Exception
@@ -97,125 +85,6 @@
 		super(manager, session);
 	}
 
-	// BEGIN Work-around for bug 565394 / SSHD-1050
-	// Remove when using sshd 2.6.0.
-
-	@Override
-	public AuthFuture auth() throws IOException {
-		if (getUsername() == null) {
-			throw new IllegalStateException(
-					SshdText.get().sessionWithoutUsername);
-		}
-		ClientUserAuthService authService = getUserAuthService();
-		String serviceName = nextServiceName();
-		List<Throwable> errors = null;
-		AuthFuture future;
-		// Guard both getting early errors and setting authFuture
-		synchronized (errorLock) {
-			future = authService.auth(serviceName);
-			if (future == null) {
-				// Internal error; no translation.
-				throw new IllegalStateException(
-						"No auth future generated by service '" //$NON-NLS-1$
-								+ serviceName + '\'');
-			}
-			errors = earlyErrors;
-			earlyErrors = null;
-			authFuture = future;
-		}
-		if (errors != null && !errors.isEmpty()) {
-			Iterator<Throwable> iter = errors.iterator();
-			Throwable first = iter.next();
-			iter.forEachRemaining(t -> {
-				if (t != first && t != null) {
-					first.addSuppressed(t);
-				}
-			});
-			// Mark the future as having had an exception; just to be on the
-			// safe side. Actually, there shouldn't be anyone waiting on this
-			// future yet.
-			future.setException(first);
-			if (log.isDebugEnabled()) {
-				log.debug("auth({}) early exception type={}: {}", //$NON-NLS-1$
-						this, first.getClass().getSimpleName(),
-						first.getMessage());
-			}
-			if (first instanceof SshException) {
-				throw new SshException(
-						((SshException) first).getDisconnectCode(),
-						first.getMessage(), first);
-			}
-			throw new IOException(first.getMessage(), first);
-		}
-		return future;
-	}
-
-	@Override
-	protected void signalAuthFailure(AuthFuture future, Throwable t) {
-		signalAuthFailure(t);
-	}
-
-	private void signalAuthFailure(Throwable t) {
-		AuthFuture future = authFuture;
-		if (future == null) {
-			synchronized (errorLock) {
-				if (earlyErrors != null) {
-					earlyErrors.add(t);
-				}
-				future = authFuture;
-			}
-		}
-		if (future != null) {
-			future.setException(t);
-		}
-		if (log.isDebugEnabled()) {
-			boolean signalled = future != null && t == future.getException();
-			log.debug("signalAuthFailure({}) type={}, signalled={}: {}", this, //$NON-NLS-1$
-					t.getClass().getSimpleName(), Boolean.valueOf(signalled),
-					t.getMessage());
-		}
-	}
-
-	@Override
-	public void exceptionCaught(Throwable t) {
-		signalAuthFailure(t);
-		super.exceptionCaught(t);
-	}
-
-	@Override
-	protected void preClose() {
-		signalAuthFailure(
-				new SshException(SshdText.get().authenticationOnClosedSession));
-		super.preClose();
-	}
-
-	@Override
-	protected void handleDisconnect(int code, String msg, String lang,
-			Buffer buffer) throws Exception {
-		signalAuthFailure(new SshException(code, msg));
-		super.handleDisconnect(code, msg, lang, buffer);
-	}
-
-	@Override
-	protected <C extends Collection<ClientSessionEvent>> C updateCurrentSessionState(
-			C newState) {
-		if (closeFuture.isClosed()) {
-			newState.add(ClientSessionEvent.CLOSED);
-		}
-		if (isAuthenticated()) { // authFuture.isSuccess()
-			newState.add(ClientSessionEvent.AUTHED);
-		}
-		if (KexState.DONE.equals(getKexState())) {
-			AuthFuture future = authFuture;
-			if (future == null || future.isFailure()) {
-				newState.add(ClientSessionEvent.WAIT_AUTH);
-			}
-		}
-		return newState;
-	}
-
-	// END Work-around for bug 565394 / SSHD-1050
-
 	/**
 	 * Retrieves the {@link HostConfigEntry} this session was created for.
 	 *
@@ -332,66 +201,25 @@
 	}
 
 	@Override
-	protected void checkKeys() throws SshException {
-		ServerKeyVerifier serverKeyVerifier = getServerKeyVerifier();
-		// The super implementation always uses
-		// getIoSession().getRemoteAddress(). In case of a proxy connection,
-		// that would be the address of the proxy!
-		SocketAddress remoteAddress = getConnectAddress();
-		PublicKey serverKey = getKex().getServerKey();
-		if (!serverKeyVerifier.verifyServerKey(this, remoteAddress,
-				serverKey)) {
-			throw new SshException(
-					org.apache.sshd.common.SshConstants.SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE,
-					SshdText.get().kexServerKeyInvalid);
-		}
-	}
-
-	@Override
 	protected String resolveAvailableSignaturesProposal(
 			FactoryManager manager) {
-		Set<String> defaultSignatures = new LinkedHashSet<>();
-		defaultSignatures.addAll(getSignatureFactoriesNames());
+		List<String> defaultSignatures = getSignatureFactoriesNames();
 		HostConfigEntry config = resolveAttribute(
 				JGitSshClient.HOST_CONFIG_ENTRY);
-		String hostKeyAlgorithms = config
+		String algorithms = config
 				.getProperty(SshConstants.HOST_KEY_ALGORITHMS);
-		if (hostKeyAlgorithms != null && !hostKeyAlgorithms.isEmpty()) {
-			char first = hostKeyAlgorithms.charAt(0);
-			switch (first) {
-			case '+':
-				// Additions make not much sense -- it's either in
-				// defaultSignatures already, or we have no implementation for
-				// it. No point in proposing it.
-				return String.join(",", defaultSignatures); //$NON-NLS-1$
-			case '-':
-				// This takes wildcard patterns!
-				removeFromList(defaultSignatures,
-						SshConstants.HOST_KEY_ALGORITHMS,
-						hostKeyAlgorithms.substring(1));
-				if (defaultSignatures.isEmpty()) {
-					// Too bad: user config error. Warn here, and then fail
-					// later.
-					log.warn(format(
-							SshdText.get().configNoRemainingHostKeyAlgorithms,
-							hostKeyAlgorithms));
+		if (!StringUtils.isEmptyOrNull(algorithms)) {
+			List<String> result = modifyAlgorithmList(defaultSignatures,
+					algorithms, SshConstants.HOST_KEY_ALGORITHMS);
+			if (!result.isEmpty()) {
+				if (log.isDebugEnabled()) {
+					log.debug(SshConstants.HOST_KEY_ALGORITHMS + ' ' + result);
 				}
-				return String.join(",", defaultSignatures); //$NON-NLS-1$
-			default:
-				// Default is overridden -- only accept the ones for which we do
-				// have an implementation.
-				List<String> newNames = filteredList(defaultSignatures,
-						hostKeyAlgorithms);
-				if (newNames.isEmpty()) {
-					log.warn(format(
-							SshdText.get().configNoKnownHostKeyAlgorithms,
-							hostKeyAlgorithms));
-					// Use the default instead.
-				} else {
-					return String.join(",", newNames); //$NON-NLS-1$
-				}
-				break;
+				return String.join(",", result); //$NON-NLS-1$
 			}
+			log.warn(format(SshdText.get().configNoKnownAlgorithms,
+					SshConstants.HOST_KEY_ALGORITHMS,
+					algorithms));
 		}
 		// No HostKeyAlgorithms; using default -- change order to put existing
 		// keys first.
@@ -411,11 +239,67 @@
 				}
 			}
 			reordered.addAll(defaultSignatures);
+			if (log.isDebugEnabled()) {
+				log.debug(SshConstants.HOST_KEY_ALGORITHMS + ' ' + reordered);
+			}
 			return String.join(",", reordered); //$NON-NLS-1$
 		}
+		if (log.isDebugEnabled()) {
+			log.debug(
+					SshConstants.HOST_KEY_ALGORITHMS + ' ' + defaultSignatures);
+		}
 		return String.join(",", defaultSignatures); //$NON-NLS-1$
 	}
 
+	/**
+	 * Modifies a given algorithm list according to a list from the ssh config,
+	 * including remove ('-') and reordering ('^') operators. Addition ('+') is
+	 * not handled since we have no way of adding dynamically implementations,
+	 * and the defaultList is supposed to contain all known implementations
+	 * already.
+	 *
+	 * @param defaultList
+	 *            to modify
+	 * @param fromConfig
+	 *            telling how to modify the {@code defaultList}, must not be
+	 *            {@code null} or empty
+	 * @param overrideKey
+	 *            ssh config key; used for logging
+	 * @return the modified list or {@code null} if {@code overrideKey} is not
+	 *         set
+	 */
+	public List<String> modifyAlgorithmList(List<String> defaultList,
+			String fromConfig, String overrideKey) {
+		Set<String> defaults = new LinkedHashSet<>();
+		defaults.addAll(defaultList);
+		switch (fromConfig.charAt(0)) {
+		case '+':
+			// Additions make not much sense -- it's either in
+			// defaultList already, or we have no implementation for
+			// it. No point in proposing it.
+			return defaultList;
+		case '-':
+			// This takes wildcard patterns!
+			removeFromList(defaults, overrideKey, fromConfig.substring(1));
+			return new ArrayList<>(defaults);
+		case '^':
+			// Specified entries go to the front of the default list
+			List<String> allSignatures = filteredList(defaults,
+					fromConfig.substring(1));
+			Set<String> atFront = new HashSet<>(allSignatures);
+			for (String sig : defaults) {
+				if (!atFront.contains(sig)) {
+					allSignatures.add(sig);
+				}
+			}
+			return allSignatures;
+		default:
+			// Default is overridden -- only accept the ones for which we do
+			// have an implementation.
+			return filteredList(defaults, fromConfig);
+		}
+	}
+
 	private void removeFromList(Set<String> current, String key,
 			String patterns) {
 		for (String toRemove : patterns.split("\\s*,\\s*")) { //$NON-NLS-1$
@@ -477,9 +361,15 @@
 			throw new IllegalStateException(
 					"doReadIdentification of client called with server=true"); //$NON-NLS-1$
 		}
-		int maxIdentSize = PropertyResolverUtils.getIntProperty(this,
-				FactoryManager.MAX_IDENTIFICATION_SIZE,
-				DEFAULT_MAX_IDENTIFICATION_SIZE);
+		Integer maxIdentLength = MAX_IDENTIFICATION_SIZE.get(this).orElse(null);
+		int maxIdentSize;
+		if (maxIdentLength == null || maxIdentLength
+				.intValue() < DEFAULT_MAX_IDENTIFICATION_SIZE) {
+			maxIdentSize = DEFAULT_MAX_IDENTIFICATION_SIZE;
+			MAX_IDENTIFICATION_SIZE.set(this, Integer.valueOf(maxIdentSize));
+		} else {
+			maxIdentSize = maxIdentLength.intValue();
+		}
 		int current = buffer.rpos();
 		int end = current + buffer.available();
 		if (current >= end) {
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 4abd6e9..ff8caaa 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
@@ -9,7 +9,8 @@
  */
 package org.eclipse.jgit.internal.transport.sshd;
 
-import org.apache.sshd.client.ClientAuthenticationManager;
+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;
@@ -29,9 +30,7 @@
 	public void init(ClientSession session, String service) throws Exception {
 		super.init(session, service);
 		maxAttempts = Math.max(1,
-				session.getIntProperty(
-						ClientAuthenticationManager.PASSWORD_PROMPTS,
-						ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS));
+				PASSWORD_PROMPTS.getRequired(session).intValue());
 		attempts = 0;
 	}
 
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthFactory.java
new file mode 100644
index 0000000..0e3e24d
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.transport.sshd;
+
+import java.io.IOException;
+
+import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
+import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
+import org.apache.sshd.client.session.ClientSession;
+
+/**
+ * A customized authentication factory for public key user authentication.
+ */
+public class JGitPublicKeyAuthFactory extends UserAuthPublicKeyFactory {
+
+	/** The singleton {@link JGitPublicKeyAuthFactory}. */
+	public static final JGitPublicKeyAuthFactory FACTORY = new JGitPublicKeyAuthFactory();
+
+	private JGitPublicKeyAuthFactory() {
+		super();
+	}
+
+	@Override
+	public UserAuthPublicKey createUserAuth(ClientSession session)
+			throws IOException {
+		return new JGitPublicKeyAuthentication(getSignatureFactories());
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java
new file mode 100644
index 0000000..297b456
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.transport.sshd;
+
+import java.io.IOException;
+import java.security.PublicKey;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.RuntimeSshException;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.signature.SignatureFactoriesHolder;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * Custom {@link UserAuthPublicKey} implementation fixing SSHD-1105: if there
+ * are several signature algorithms applicable for a public key type, we must
+ * try them all, in the correct order.
+ *
+ * @see <a href="https://issues.apache.org/jira/browse/SSHD-1105">SSHD-1105</a>
+ * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">Bug
+ *      572056</a>
+ */
+public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
+
+	private final List<String> algorithms = new LinkedList<>();
+
+	JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
+		super(factories);
+	}
+
+	@Override
+	protected boolean sendAuthDataRequest(ClientSession session, String service)
+			throws Exception {
+		if (current == null) {
+			algorithms.clear();
+		}
+		String currentAlgorithm = null;
+		if (current != null && !algorithms.isEmpty()) {
+			currentAlgorithm = algorithms.remove(0);
+		}
+		if (currentAlgorithm == null) {
+			try {
+				if (keys == null || !keys.hasNext()) {
+					if (log.isDebugEnabled()) {
+						log.debug(
+								"sendAuthDataRequest({})[{}] no more keys to send", //$NON-NLS-1$
+								session, service);
+					}
+					return false;
+				}
+				current = keys.next();
+				algorithms.clear();
+			} catch (Error e) { // Copied from superclass
+				warn("sendAuthDataRequest({})[{}] failed ({}) to get next key: {}", //$NON-NLS-1$
+						session, service, e.getClass().getSimpleName(),
+						e.getMessage(), e);
+				throw new RuntimeSshException(e);
+			}
+		}
+		PublicKey key;
+		try {
+			key = current.getPublicKey();
+		} catch (Error e) { // Copied from superclass
+			warn("sendAuthDataRequest({})[{}] failed ({}) to retrieve public key: {}", //$NON-NLS-1$
+					session, service, e.getClass().getSimpleName(),
+					e.getMessage(), e);
+			throw new RuntimeSshException(e);
+		}
+		if (currentAlgorithm == null) {
+			String keyType = KeyUtils.getKeyType(key);
+			Set<String> aliases = new HashSet<>(
+					KeyUtils.getAllEquivalentKeyTypes(keyType));
+			aliases.add(keyType);
+			List<NamedFactory<Signature>> existingFactories;
+			if (current instanceof SignatureFactoriesHolder) {
+				existingFactories = ((SignatureFactoriesHolder) current)
+						.getSignatureFactories();
+			} else {
+				existingFactories = getSignatureFactories();
+			}
+			if (existingFactories != null) {
+				// Select the factories by name and in order
+				existingFactories.forEach(f -> {
+					if (aliases.contains(f.getName())) {
+						algorithms.add(f.getName());
+					}
+				});
+			}
+			currentAlgorithm = algorithms.isEmpty() ? keyType
+					: algorithms.remove(0);
+		}
+		String name = getName();
+		if (log.isDebugEnabled()) {
+			log.debug(
+					"sendAuthDataRequest({})[{}] send SSH_MSG_USERAUTH_REQUEST request {} type={} - fingerprint={}", //$NON-NLS-1$
+					session, service, name, currentAlgorithm,
+					KeyUtils.getFingerPrint(key));
+		}
+
+		Buffer buffer = session
+				.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
+		buffer.putString(session.getUsername());
+		buffer.putString(service);
+		buffer.putString(name);
+		buffer.putBoolean(false);
+		buffer.putString(currentAlgorithm);
+		buffer.putPublicKey(key);
+		session.writePacket(buffer);
+		return true;
+	}
+
+	@Override
+	protected void releaseKeys() throws IOException {
+		algorithms.clear();
+		current = null;
+		super.releaseKeys();
+	}
+}
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 beaaeca..071e197 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
@@ -10,6 +10,8 @@
 package org.eclipse.jgit.internal.transport.sshd;
 
 import static java.text.MessageFormat.format;
+import static org.apache.sshd.core.CoreModuleProperties.PASSWORD_PROMPTS;
+import static org.apache.sshd.core.CoreModuleProperties.PREFERRED_AUTHS;
 import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;
 
 import java.io.IOException;
@@ -32,7 +34,6 @@
 import java.util.Objects;
 import java.util.stream.Collectors;
 
-import org.apache.sshd.client.ClientAuthenticationManager;
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.config.hosts.HostConfigEntry;
 import org.apache.sshd.client.future.ConnectFuture;
@@ -169,12 +170,15 @@
 		Map<AttributeKey<?>, Object> data = new HashMap<>();
 		data.put(HOST_CONFIG_ENTRY, hostConfig);
 		data.put(ORIGINAL_REMOTE_ADDRESS, originalAddress);
+		data.put(TARGET_SERVER, new SshdSocketAddress(originalAddress));
 		String preferredAuths = hostConfig.getProperty(
 				SshConstants.PREFERRED_AUTHENTICATIONS,
 				resolveAttribute(PREFERRED_AUTHENTICATIONS));
 		if (!StringUtils.isEmptyOrNull(preferredAuths)) {
 			data.put(SessionAttributes.PROPERTIES,
-					Collections.singletonMap(PREFERRED_AUTHS, preferredAuths));
+					Collections.singletonMap(
+							PREFERRED_AUTHS.getName(),
+							preferredAuths));
 		}
 		return new SessionAttributes(
 				AttributeRepository.ofAttributesMap(data),
@@ -263,12 +267,29 @@
 		session.setUsername(username);
 		session.setConnectAddress(address);
 		session.setHostConfigEntry(hostConfig);
+		// Set signature algorithms for public key authentication
+		String pubkeyAlgos = hostConfig
+				.getProperty(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
+		if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) {
+			List<String> signatures = getSignatureFactoriesNames();
+			signatures = session.modifyAlgorithmList(signatures, pubkeyAlgos,
+					SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
+			if (!signatures.isEmpty()) {
+				if (log.isDebugEnabled()) {
+					log.debug(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS + ' '
+							+ signatures);
+				}
+				session.setSignatureFactoriesNames(signatures);
+			} else {
+				log.warn(format(SshdText.get().configNoKnownAlgorithms,
+						SshConstants.PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos));
+			}
+		}
 		if (session.getCredentialsProvider() == null) {
 			session.setCredentialsProvider(getCredentialsProvider());
 		}
 		int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig);
-		session.getProperties().put(PASSWORD_PROMPTS,
-				Integer.valueOf(numberOfPasswordPrompts));
+		PASSWORD_PROMPTS.set(session, Integer.valueOf(numberOfPasswordPrompts));
 		List<Path> identities = hostConfig.getIdentities().stream()
 				.map(s -> {
 					try {
@@ -311,7 +332,7 @@
 			log.warn(format(SshdText.get().configInvalidPositive,
 					SshConstants.NUMBER_OF_PASSWORD_PROMPTS, prompts));
 		}
-		return ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS;
+		return PASSWORD_PROMPTS.getRequiredDefault().intValue();
 	}
 
 	/**
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java
index 97e0fcc..6b0d9fb 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java
@@ -46,7 +46,7 @@
 
 	@Override
 	public HostConfigEntry resolveEffectiveHost(String host, int port,
-			SocketAddress localAddress, String username,
+			SocketAddress localAddress, String username, String proxyJump,
 			AttributeRepository attributes) throws IOException {
 		SshConfigStore.HostConfig entry = configFile == null
 				? SshConfigStore.EMPTY_CONFIG
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java
index 5bc1115..47e09b7 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabase.java
@@ -346,11 +346,14 @@
 			throws IOException {
 		KnownHostEntry hostEntry = entry.getHostEntry();
 		String oldLine = hostEntry.getConfigLine();
+		if (oldLine == null) {
+			return;
+		}
 		String newLine = updateHostKeyLine(oldLine, serverKey);
 		if (newLine == null || newLine.isEmpty()) {
 			return;
 		}
-		if (oldLine == null || oldLine.isEmpty() || newLine.equals(oldLine)) {
+		if (oldLine.isEmpty() || newLine.equals(oldLine)) {
 			// Shouldn't happen.
 			return;
 		}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
index 078e411..2cd0669 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
@@ -9,6 +9,8 @@
  */
 package org.eclipse.jgit.internal.transport.sshd;
 
+import static org.apache.sshd.core.CoreModuleProperties.PASSWORD_PROMPTS;
+
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
@@ -18,7 +20,6 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Supplier;
 
-import org.apache.sshd.client.ClientAuthenticationManager;
 import org.apache.sshd.common.AttributeRepository.AttributeKey;
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
@@ -62,15 +63,8 @@
 		if (state == null) {
 			state = new PerSessionState();
 			state.delegate = factory.get();
-			Integer maxNumberOfAttempts = context
-					.getInteger(ClientAuthenticationManager.PASSWORD_PROMPTS);
-			if (maxNumberOfAttempts != null
-					&& maxNumberOfAttempts.intValue() > 0) {
-				state.delegate.setAttempts(maxNumberOfAttempts.intValue());
-			} else {
-				state.delegate.setAttempts(
-						ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS);
-			}
+			state.delegate.setAttempts(
+					PASSWORD_PROMPTS.getRequiredDefault().intValue());
 			context.setAttribute(STATE, state);
 		}
 		return state;
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 13bb3eb..4c4ff59 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
@@ -25,8 +25,7 @@
 	/***/ public String configInvalidPattern;
 	/***/ public String configInvalidPositive;
 	/***/ public String configInvalidProxyJump;
-	/***/ public String configNoKnownHostKeyAlgorithms;
-	/***/ public String configNoRemainingHostKeyAlgorithms;
+	/***/ public String configNoKnownAlgorithms;
 	/***/ public String configProxyJumpNotSsh;
 	/***/ public String configProxyJumpWithPath;
 	/***/ public String ftpCloseFailed;
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpClientConnector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpClientConnector.java
index 8ac752b..e5d1e80 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpClientConnector.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpClientConnector.java
@@ -135,7 +135,7 @@
 		byte[] data = eol(msg).toString().getBytes(US_ASCII);
 		Buffer buffer = new ByteArrayBuffer(data.length, false);
 		buffer.putRawBytes(data);
-		session.writePacket(buffer).verify(getTimeout());
+		session.writeBuffer(buffer).verify(getTimeout());
 	}
 
 	private StringBuilder connect() {
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/Socks5ClientConnector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/Socks5ClientConnector.java
index 78b8d45..8844efa 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/Socks5ClientConnector.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/Socks5ClientConnector.java
@@ -235,7 +235,7 @@
 		buffer.putByte((byte) authenticationProposals.length);
 		buffer.putRawBytes(authenticationProposals);
 		state = ProtocolState.INIT;
-		session.writePacket(buffer).verify(getTimeout());
+		session.writeBuffer(buffer).verify(getTimeout());
 	}
 
 	private byte[] getAuthenticationProposals() {
@@ -298,7 +298,7 @@
 		buffer.putByte((byte) ((port >> 8) & 0xFF));
 		buffer.putByte((byte) (port & 0xFF));
 		state = ProtocolState.CONNECTING;
-		session.writePacket(buffer).verify(getTimeout());
+		session.writeBuffer(buffer).verify(getTimeout());
 	}
 
 	private void doPasswordAuth(IoSession session) throws Exception {
@@ -335,7 +335,7 @@
 						"No data for proxy authentication with " //$NON-NLS-1$
 								+ proxyAddress);
 			}
-			session.writePacket(buffer).verify(getTimeout());
+			session.writeBuffer(buffer).verify(getTimeout());
 		} finally {
 			if (buffer != null) {
 				buffer.clear(true);
@@ -350,7 +350,7 @@
 			authenticator.process();
 			buffer = authenticator.getToken();
 			if (buffer != null) {
-				session.writePacket(buffer).verify(getTimeout());
+				session.writeBuffer(buffer).verify(getTimeout());
 			}
 		} finally {
 			if (buffer != null) {
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 0fb0610..33b234b 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
@@ -11,6 +11,7 @@
 
 import static java.text.MessageFormat.format;
 import static org.apache.sshd.common.SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE;
+import static org.apache.sshd.sftp.SftpModuleProperties.SFTP_CHANNEL_OPEN_TIMEOUT;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -24,6 +25,7 @@
 import java.util.EnumSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
@@ -37,23 +39,23 @@
 import org.apache.sshd.client.future.ConnectFuture;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.session.forward.PortForwardingTracker;
-import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
-import org.apache.sshd.client.subsystem.sftp.SftpClient.CopyMode;
-import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
 import org.apache.sshd.common.AttributeRepository;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.future.CloseFuture;
 import org.apache.sshd.common.future.SshFutureListener;
-import org.apache.sshd.common.subsystem.sftp.SftpException;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.sftp.client.SftpClient;
+import org.apache.sshd.sftp.client.SftpClient.CloseableHandle;
+import org.apache.sshd.sftp.client.SftpClient.CopyMode;
+import org.apache.sshd.sftp.client.SftpClientFactory;
+import org.apache.sshd.sftp.common.SftpException;
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
 import org.eclipse.jgit.internal.transport.sshd.SshdText;
 import org.eclipse.jgit.transport.FtpChannel;
-import org.eclipse.jgit.transport.RemoteSession;
+import org.eclipse.jgit.transport.RemoteSession2;
 import org.eclipse.jgit.transport.SshConstants;
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.util.StringUtils;
@@ -61,11 +63,12 @@
 import org.slf4j.LoggerFactory;
 
 /**
- * An implementation of {@link RemoteSession} based on Apache MINA sshd.
+ * An implementation of {@link org.eclipse.jgit.transport.RemoteSession
+ * RemoteSession} based on Apache MINA sshd.
  *
  * @since 5.2
  */
-public class SshdSession implements RemoteSession {
+public class SshdSession implements RemoteSession2 {
 
 	private static final Logger LOG = LoggerFactory
 			.getLogger(SshdSession.class);
@@ -203,7 +206,7 @@
 	private HostConfigEntry getHostConfig(String username, String host,
 			int port) throws IOException {
 		HostConfigEntry entry = client.getHostConfigEntryResolver()
-				.resolveEffectiveHost(host, port, null, username, null);
+				.resolveEffectiveHost(host, port, null, username, null, null);
 		if (entry == null) {
 			if (SshdSocketAddress.isIPv6Address(host)) {
 				return new HostConfigEntry("", host, port, username); //$NON-NLS-1$
@@ -290,8 +293,15 @@
 
 	@Override
 	public Process exec(String commandName, int timeout) throws IOException {
+		return exec(commandName, Collections.emptyMap(), timeout);
+	}
+
+	@Override
+	public Process exec(String commandName, Map<String, String> environment,
+			int timeout) throws IOException {
 		@SuppressWarnings("resource")
-		ChannelExec exec = session.createExecChannel(commandName);
+		ChannelExec exec = session.createExecChannel(commandName, null,
+				environment);
 		if (timeout <= 0) {
 			try {
 				exec.open().verify();
@@ -430,13 +440,12 @@
 		@Override
 		public void connect(int timeout, TimeUnit unit) throws IOException {
 			if (timeout <= 0) {
-				session.getProperties().put(
-						SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
-						Long.valueOf(Long.MAX_VALUE));
+				// This timeout must not be null!
+				SFTP_CHANNEL_OPEN_TIMEOUT.set(session,
+						Duration.ofMillis(Long.MAX_VALUE));
 			} else {
-				session.getProperties().put(
-						SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
-						Long.valueOf(unit.toMillis(timeout)));
+				SFTP_CHANNEL_OPEN_TIMEOUT.set(session,
+						Duration.ofMillis(unit.toMillis(timeout)));
 			}
 			ftp = SftpClientFactory.instance().createSftpClient(session);
 			try {
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 df0e1d2..cad959c 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
@@ -32,13 +32,15 @@
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.auth.UserAuthFactory;
 import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
-import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
 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;
 import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.apache.sshd.common.signature.Signature;
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
@@ -46,6 +48,7 @@
 import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
 import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
 import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
+import org.eclipse.jgit.internal.transport.sshd.JGitPublicKeyAuthFactory;
 import org.eclipse.jgit.internal.transport.sshd.JGitServerKeyVerifier;
 import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
 import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
@@ -205,6 +208,7 @@
 						.hostConfigEntryResolver(configFile)
 						.serverKeyVerifier(new JGitServerKeyVerifier(
 								getServerKeyDatabase(home, sshDir)))
+						.signatureFactories(getSignatureFactories())
 						.compressionFactories(
 								new ArrayList<>(BuiltinCompressions.VALUES))
 						.build();
@@ -573,7 +577,7 @@
 		// Password auth doesn't have this problem.
 		return Collections.unmodifiableList(
 				Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
-						UserAuthPublicKeyFactory.INSTANCE,
+						JGitPublicKeyAuthFactory.FACTORY,
 						JGitPasswordAuthFactory.INSTANCE,
 						UserAuthKeyboardInteractiveFactory.INSTANCE));
 	}
@@ -590,4 +594,35 @@
 	protected String getDefaultPreferredAuthentications() {
 		return null;
 	}
+
+	/**
+	 * Apache MINA sshd 2.6.0 has removed DSA, DSA_CERT and RSA_CERT. We have to
+	 * set it up explicitly to still allow users to connect with DSA keys.
+	 *
+	 * @return a list of supported signature factories
+	 */
+	@SuppressWarnings("deprecation")
+	private static List<NamedFactory<Signature>> getSignatureFactories() {
+		// @formatter:off
+		return Arrays.asList(
+				BuiltinSignatures.nistp256_cert,
+				BuiltinSignatures.nistp384_cert,
+				BuiltinSignatures.nistp521_cert,
+				BuiltinSignatures.ed25519_cert,
+				BuiltinSignatures.rsaSHA512_cert,
+				BuiltinSignatures.rsaSHA256_cert,
+				BuiltinSignatures.rsa_cert,
+				BuiltinSignatures.nistp256,
+				BuiltinSignatures.nistp384,
+				BuiltinSignatures.nistp521,
+				BuiltinSignatures.ed25519,
+				BuiltinSignatures.sk_ecdsa_sha2_nistp256,
+				BuiltinSignatures.sk_ssh_ed25519,
+				BuiltinSignatures.rsaSHA512,
+				BuiltinSignatures.rsaSHA256,
+				BuiltinSignatures.rsa,
+				BuiltinSignatures.dsa_cert,
+				BuiltinSignatures.dsa);
+		// @formatter:on
+	}
 }
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 7d776e1..a5e4f39 100644
--- a/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
@@ -3,22 +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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: com.jcraft.jsch;version="[0.1.54,0.2.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit.ssh;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit.ssh;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)"
-Export-Package: org.eclipse.jgit.transport;version="5.10.1";
-  uses:="org.eclipse.jgit.transport,
-    org.eclipse.jgit.junit,
-    org.eclipse.jgit.junit.ssh"
 Require-Bundle: org.hamcrest.core;bundle-version="[1.3.0,2.0.0)"
diff --git a/org.eclipse.jgit.ssh.jsch.test/pom.xml b/org.eclipse.jgit.ssh.jsch.test/pom.xml
index 0911d6d..18cce42 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.jsch.test</artifactId>
diff --git a/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/JSchSshProtocol2Test.java b/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/JSchSshProtocol2Test.java
new file mode 100644
index 0000000..0929c55
--- /dev/null
+++ b/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/JSchSshProtocol2Test.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 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
+ */
+
+//TODO(ms): move to org.eclipse.jgit.ssh.jsch in 6.0
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.junit.ssh.SshBasicTestBase;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.OpenSshConfig.Host;
+import org.eclipse.jgit.util.FS;
+
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+public class JSchSshProtocol2Test extends SshBasicTestBase {
+
+	private class TestSshSessionFactory extends JschConfigSessionFactory {
+
+		@Override
+		protected void configure(Host hc, Session session) {
+			// Nothing
+		}
+
+		@Override
+		public synchronized RemoteSession getSession(URIish uri,
+				CredentialsProvider credentialsProvider, FS fs, int tms)
+				throws TransportException {
+			return super.getSession(uri, credentialsProvider, fs, tms);
+		}
+
+		@Override
+		protected JSch createDefaultJSch(FS fs) throws JSchException {
+			JSch defaultJSch = super.createDefaultJSch(fs);
+			if (knownHosts.exists()) {
+				defaultJSch.setKnownHosts(knownHosts.getAbsolutePath());
+			}
+			return defaultJSch;
+		}
+	}
+
+	@Override
+	protected SshSessionFactory createSessionFactory() {
+		return new TestSshSessionFactory();
+	}
+
+	@Override
+	protected void installConfig(String... config) {
+		SshSessionFactory factory = getSessionFactory();
+		assertTrue(factory instanceof JschConfigSessionFactory);
+		JschConfigSessionFactory j = (JschConfigSessionFactory) factory;
+		try {
+			j.setConfig(createConfig(config));
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
+	private OpenSshConfig createConfig(String... content) throws IOException {
+		File configFile = new File(sshDir, Constants.CONFIG);
+		if (content != null) {
+			Files.write(configFile.toPath(), Arrays.asList(content));
+		}
+		return new OpenSshConfig(getTemporaryDirectory(), configFile);
+	}
+
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		StoredConfig config = ((Repository) db).getConfig();
+		config.setInt("protocol", null, "version", 2);
+		config.save();
+	}
+}
diff --git a/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java b/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java
index af09f49..4c7e99e 100644
--- a/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java
+++ b/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java
@@ -467,4 +467,34 @@
 				new File(new File(home, ".ssh"), localhost + "_id_dsa"),
 				h.getIdentityFile());
 	}
+
+	@Test
+	public void testPubKeyAcceptedAlgorithms() throws Exception {
+		config("Host=orcz\n\tPubkeyAcceptedAlgorithms ^ssh-rsa");
+		Host h = osc.lookup("orcz");
+		Config c = h.getConfig();
+		assertEquals("^ssh-rsa",
+				c.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
+		assertEquals("^ssh-rsa", c.getValue("PubkeyAcceptedKeyTypes"));
+	}
+
+	@Test
+	public void testPubKeyAcceptedKeyTypes() throws Exception {
+		config("Host=orcz\n\tPubkeyAcceptedKeyTypes ^ssh-rsa");
+		Host h = osc.lookup("orcz");
+		Config c = h.getConfig();
+		assertEquals("^ssh-rsa",
+				c.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
+		assertEquals("^ssh-rsa", c.getValue("PubkeyAcceptedKeyTypes"));
+	}
+
+	@Test
+	public void testEolComments() throws Exception {
+		config("#Comment\nHost=orcz #Comment\n\tPubkeyAcceptedAlgorithms ^ssh-rsa # Comment\n#Comment");
+		Host h = osc.lookup("orcz");
+		assertNotNull(h);
+		Config c = h.getConfig();
+		assertEquals("^ssh-rsa",
+				c.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
+	}
 }
diff --git a/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
index 032d4f9..d7e868f 100644
--- a/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
@@ -3,24 +3,24 @@
 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="[5.10.1,5.11.0)"
+Fragment-Host: org.eclipse.jgit;bundle-version="[5.11.2,5.12.0)"
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-ActivationPolicy: lazy
-Bundle-Version: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Export-Package: org.eclipse.jgit.internal.transport.jsch;version="5.10.1";x-friends:="org.eclipse.egit.core",
- org.eclipse.jgit.transport;version="5.10.1";
+Export-Package: org.eclipse.jgit.internal.transport.jsch;version="5.11.2";x-friends:="org.eclipse.egit.core",
+ org.eclipse.jgit.transport;version="5.11.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.internal.transport.ssh,
    org.eclipse.jgit.util,
    com.jcraft.jsch"
 Import-Package: com.jcraft.jsch;version="[0.1.37,0.2.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util.io;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util.io;version="[5.11.2,5.12.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 d27830c..64a37b7 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.jsch;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.jsch;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.jsch/pom.xml b/org.eclipse.jgit.ssh.jsch/pom.xml
index cf0eaae..d6c2be4 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschSession.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschSession.java
index 858bdf3..c7d0941 100644
--- a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschSession.java
+++ b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschSession.java
@@ -22,7 +22,9 @@
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 
@@ -44,7 +46,7 @@
  * {@link org.eclipse.jgit.transport.JschConfigSessionFactory} is used to create
  * the actual session passed to the constructor.
  */
-public class JschSession implements RemoteSession {
+public class JschSession implements RemoteSession2 {
 	final Session sock;
 	final URIish uri;
 
@@ -65,7 +67,14 @@
 	/** {@inheritDoc} */
 	@Override
 	public Process exec(String command, int timeout) throws IOException {
-		return new JschProcess(command, timeout);
+		return exec(command, Collections.emptyMap(), timeout);
+	}
+
+	/** {@inheritDoc} */
+	@Override
+	public Process exec(String command, Map<String, String> environment,
+			int timeout) throws IOException {
+		return new JschProcess(command, environment, timeout);
 	}
 
 	/** {@inheritDoc} */
@@ -124,6 +133,8 @@
 		 *
 		 * @param commandName
 		 *            the command to execute
+		 * @param environment
+		 *            environment variables to pass on
 		 * @param tms
 		 *            the timeout value, in seconds, for the command.
 		 * @throws TransportException
@@ -132,11 +143,17 @@
 		 * @throws IOException
 		 *             on problems opening streams
 		 */
-		JschProcess(String commandName, int tms)
-				throws TransportException, IOException {
+		JschProcess(String commandName, Map<String, String> environment,
+				int tms) throws TransportException, IOException {
 			timeout = tms;
 			try {
 				channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$
+				if (environment != null) {
+					for (Map.Entry<String, String> envVar : environment
+							.entrySet()) {
+						channel.setEnv(envVar.getKey(), envVar.getValue());
+					}
+				}
 				channel.setCommand(commandName);
 				setupStreams();
 				channel.connect(timeout > 0 ? timeout * 1000 : 0);
diff --git a/org.eclipse.jgit.test/.settings/edu.umd.cs.findbugs.core.prefs b/org.eclipse.jgit.test/.settings/edu.umd.cs.findbugs.core.prefs
new file mode 100644
index 0000000..70c173f
--- /dev/null
+++ b/org.eclipse.jgit.test/.settings/edu.umd.cs.findbugs.core.prefs
@@ -0,0 +1,145 @@
+#SpotBugs User Preferences
+#Fri Dec 04 11:35:48 CET 2020
+detectorExplicitSerialization=ExplicitSerialization|true
+detectorMultithreadedInstanceAccess=MultithreadedInstanceAccess|true
+detectorConfusionBetweenInheritedAndOuterMethod=ConfusionBetweenInheritedAndOuterMethod|true
+detectorWrongMapIterator=WrongMapIterator|true
+detectorUnnecessaryMath=UnnecessaryMath|true
+detectorUselessSubclassMethod=UselessSubclassMethod|false
+filter_settings=Medium|BAD_PRACTICE,CORRECTNESS,EXPERIMENTAL,I18N,MALICIOUS_CODE,MT_CORRECTNESS,PERFORMANCE,SECURITY,STYLE|false|15
+detectorURLProblems=URLProblems|true
+detectorIteratorIdioms=IteratorIdioms|true
+detectorMutableEnum=MutableEnum|true
+detectorFindNonShortCircuit=FindNonShortCircuit|true
+detectorSynchronizeAndNullCheckField=SynchronizeAndNullCheckField|true
+detectorVolatileUsage=VolatileUsage|true
+detectorFindNakedNotify=FindNakedNotify|true
+detectorFindUninitializedGet=FindUninitializedGet|true
+detectorFindUseOfNonSerializableValue=FindUseOfNonSerializableValue|true
+detectorFindJSR166LockMonitorenter=FindJSR166LockMonitorenter|true
+detectorQuestionableBooleanAssignment=QuestionableBooleanAssignment|true
+detectorSwitchFallthrough=SwitchFallthrough|true
+detectorFindLocalSelfAssignment2=FindLocalSelfAssignment2|true
+detectorConfusedInheritance=ConfusedInheritance|true
+detectorSynchronizationOnSharedBuiltinConstant=SynchronizationOnSharedBuiltinConstant|true
+detectorMutableStaticFields=MutableStaticFields|true
+detectorInvalidJUnitTest=InvalidJUnitTest|true
+detectorInfiniteLoop=InfiniteLoop|true
+detectorFindRunInvocations=FindRunInvocations|true
+detectorBadSyntaxForRegularExpression=BadSyntaxForRegularExpression|true
+detectorXMLFactoryBypass=XMLFactoryBypass|true
+detectorFindOpenStream=FindOpenStream|true
+detectorCheckExpectedWarnings=CheckExpectedWarnings|false
+detectorHugeSharedStringConstants=HugeSharedStringConstants|true
+detectorLostLoggerDueToWeakReference=LostLoggerDueToWeakReference|true
+detectorStringConcatenation=StringConcatenation|true
+detectorLoadOfKnownNullValue=LoadOfKnownNullValue|true
+detectorFinalizerNullsFields=FinalizerNullsFields|true
+detectorFindFieldSelfAssignment=FindFieldSelfAssignment|true
+detectorInefficientToArray=InefficientToArray|false
+detectorDontCatchIllegalMonitorStateException=DontCatchIllegalMonitorStateException|true
+detectorInconsistentAnnotations=InconsistentAnnotations|true
+detectorBadlyOverriddenAdapter=BadlyOverriddenAdapter|true
+detectorInstantiateStaticClass=InstantiateStaticClass|true
+detectorCheckRelaxingNullnessAnnotation=CheckRelaxingNullnessAnnotation|true
+detectorMethodReturnCheck=MethodReturnCheck|true
+detectorEqualsOperandShouldHaveClassCompatibleWithThis=EqualsOperandShouldHaveClassCompatibleWithThis|true
+detectorFindDoubleCheck=FindDoubleCheck|true
+detectorFindBadForLoop=FindBadForLoop|true
+detectorDefaultEncodingDetector=DefaultEncodingDetector|true
+detectorFindInconsistentSync2=FindInconsistentSync2|true
+detectorFindSpinLoop=FindSpinLoop|true
+detectorFindMaskedFields=FindMaskedFields|true
+detectorBooleanReturnNull=BooleanReturnNull|true
+detectorFindUnsyncGet=FindUnsyncGet|true
+detectorCrossSiteScripting=CrossSiteScripting|true
+detectorDroppedException=DroppedException|true
+detectorFindDeadLocalStores=FindDeadLocalStores|true
+detectorCheckImmutableAnnotation=CheckImmutableAnnotation|true
+detectorInfiniteRecursiveLoop=InfiniteRecursiveLoop|true
+detectorFindRefComparison=FindRefComparison|true
+detectorFindRoughConstants=FindRoughConstants|true
+detectorMutableLock=MutableLock|true
+detectorFindNullDeref=FindNullDeref|true
+detectorFindReturnRef=FindReturnRef|true
+detectorSynchronizeOnClassLiteralNotGetClass=SynchronizeOnClassLiteralNotGetClass|true
+detectorFindUselessControlFlow=FindUselessControlFlow|true
+detectorOverridingEqualsNotSymmetrical=OverridingEqualsNotSymmetrical|true
+detectorIDivResultCastToDouble=IDivResultCastToDouble|true
+detectorReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass=ReadOfInstanceFieldInMethodInvokedByConstructorInSuperclass|true
+detectorFindSelfComparison=FindSelfComparison|true
+detectorFindFloatEquality=FindFloatEquality|true
+detectorFindComparatorProblems=FindComparatorProblems|true
+detectorRepeatedConditionals=RepeatedConditionals|true
+filter_settings_neg=NOISE|
+detectorInefficientMemberAccess=InefficientMemberAccess|false
+detectorFindUncalledPrivateMethods=FindUncalledPrivateMethods|true
+detectorNumberConstructor=NumberConstructor|true
+detectorDontAssertInstanceofInTests=DontAssertInstanceofInTests|true
+detectorFindFinalizeInvocations=FindFinalizeInvocations|true
+detectorFindNullDerefsInvolvingNonShortCircuitEvaluation=FindNullDerefsInvolvingNonShortCircuitEvaluation|true
+detectorDontIgnoreResultOfPutIfAbsent=DontIgnoreResultOfPutIfAbsent|true
+detectorFindUnconditionalWait=FindUnconditionalWait|true
+detectorFindTwoLockWait=FindTwoLockWait|true
+detectorFindSleepWithLockHeld=FindSleepWithLockHeld|true
+detectorFindUnreleasedLock=FindUnreleasedLock|true
+detectorInefficientIndexOf=InefficientIndexOf|false
+detectorDoInsideDoPrivileged=DoInsideDoPrivileged|true
+detectorFindEmptySynchronizedBlock=FindEmptySynchronizedBlock|true
+detectorOverridingMethodsMustInvokeSuperDetector=OverridingMethodsMustInvokeSuperDetector|true
+detectorWaitInLoop=WaitInLoop|true
+detectorIntCast2LongAsInstant=IntCast2LongAsInstant|true
+detectorBadUseOfReturnValue=BadUseOfReturnValue|true
+detectorFindSqlInjection=FindSqlInjection|true
+detectorUnreadFields=UnreadFields|true
+detectorSynchronizingOnContentsOfFieldToProtectField=SynchronizingOnContentsOfFieldToProtectField|true
+detectorFindUselessObjects=FindUselessObjects|true
+detectorBadAppletConstructor=BadAppletConstructor|false
+detectorInheritanceUnsafeGetResource=InheritanceUnsafeGetResource|true
+detectorSerializableIdiom=SerializableIdiom|true
+detectorNaming=Naming|true
+detectorNoteUnconditionalParamDerefs=NoteUnconditionalParamDerefs|true
+detectorFormatStringChecker=FormatStringChecker|true
+detectorSuspiciousThreadInterrupted=SuspiciousThreadInterrupted|true
+detectorEmptyZipFileEntry=EmptyZipFileEntry|false
+detectorFindCircularDependencies=FindCircularDependencies|false
+detectorPreferZeroLengthArrays=PreferZeroLengthArrays|true
+detectorAtomicityProblem=AtomicityProblem|true
+detectorRuntimeExceptionCapture=RuntimeExceptionCapture|true
+detectorInitializationChain=InitializationChain|true
+detectorInitializeNonnullFieldsInConstructor=InitializeNonnullFieldsInConstructor|true
+detectorOptionalReturnNull=OptionalReturnNull|true
+detectorStartInConstructor=StartInConstructor|true
+detectorFindUnsatisfiedObligation=FindUnsatisfiedObligation|true
+detectorRedundantConditions=RedundantConditions|true
+effort=default
+detectorRedundantInterfaces=RedundantInterfaces|true
+detectorDuplicateBranches=DuplicateBranches|true
+detectorCheckTypeQualifiers=CheckTypeQualifiers|true
+detectorComparatorIdiom=ComparatorIdiom|true
+detectorFindBadCast2=FindBadCast2|true
+detectorFindMismatchedWaitOrNotify=FindMismatchedWaitOrNotify|true
+excludefilter0=findBugs/FindBugsExcludeFilter.xml|true
+detectorBadResultSetAccess=BadResultSetAccess|true
+detectorIncompatMask=IncompatMask|true
+detectorCovariantArrayAssignment=CovariantArrayAssignment|false
+detectorDumbMethodInvocations=DumbMethodInvocations|true
+run_at_full_build=false
+detectorStaticCalendarDetector=StaticCalendarDetector|true
+detectorUncallableMethodOfAnonymousClass=UncallableMethodOfAnonymousClass|true
+detectorVarArgsProblems=VarArgsProblems|true
+detectorInefficientInitializationInsideLoop=InefficientInitializationInsideLoop|false
+detectorCloneIdiom=CloneIdiom|true
+detectorFindHEmismatch=FindHEmismatch|true
+detectorAppendingToAnObjectOutputStream=AppendingToAnObjectOutputStream|true
+detectorFindSelfComparison2=FindSelfComparison2|true
+detectorLazyInit=LazyInit|true
+detectorFindUnrelatedTypesInGenericContainer=FindUnrelatedTypesInGenericContainer|true
+detectorDontUseEnum=DontUseEnum|true
+detectorFindPuzzlers=FindPuzzlers|true
+detectorCallToUnsupportedMethod=CallToUnsupportedMethod|false
+detectorSuperfluousInstanceOf=SuperfluousInstanceOf|true
+detectorReadReturnShouldBeChecked=ReadReturnShouldBeChecked|true
+detector_threshold=2
+detectorPublicSemaphores=PublicSemaphores|false
+detectorDumbMethods=DumbMethods|true
diff --git a/org.eclipse.jgit.test/BUILD b/org.eclipse.jgit.test/BUILD
index f12646e..c9b5d37 100644
--- a/org.eclipse.jgit.test/BUILD
+++ b/org.eclipse.jgit.test/BUILD
@@ -13,6 +13,8 @@
 ) + [PKG + c for c in [
     "api/AbstractRemoteCommandTest.java",
     "diff/AbstractDiffTestCase.java",
+    "internal/revwalk/ObjectReachabilityTestCase.java",
+    "internal/revwalk/ReachabilityCheckerTestCase.java",
     "internal/storage/file/GcTestCase.java",
     "internal/storage/file/PackIndexTestCase.java",
     "internal/storage/file/XInputStream.java",
@@ -20,8 +22,6 @@
     "nls/MissingPropertyBundle.java",
     "nls/NoPropertiesBundle.java",
     "nls/NonTranslatedBundle.java",
-    "revwalk/ObjectReachabilityTestCase.java",
-    "revwalk/ReachabilityCheckerTestCase.java",
     "revwalk/RevQueueTestCase.java",
     "revwalk/RevWalkTestCase.java",
     "transport/ObjectIdMatcher.java",
@@ -44,8 +44,6 @@
     PKG + "api/SecurityManagerMissingPermissionsTest.java",
 ]
 
-RESOURCES = glob(["resources/**"])
-
 tests(tests = glob(
     ["tst/**/*.java"],
     exclude = HELPERS + DATA + EXCLUDED,
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index fc79f43..c68c0bc 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
@@ -16,59 +16,59 @@
  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="[5.10.1,5.11.0)",
- org.eclipse.jgit.api;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.api.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.archive;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.attributes;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.awtui;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.blame;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.diff;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.dircache;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.events;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.fnmatch;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.gitrepo;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.hooks;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.ignore;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.ignore.internal;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.fsck;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.io;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.pack;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.storage.reftree;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.transport.connectivity;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.transport.http;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.internal.transport.parser;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.junit.time;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lfs;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib.internal;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.logging;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.merge;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.notes;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.patch;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.pgm;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.pgm.internal;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revplot;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk.filter;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.file;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.storage.pack;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.submodule;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.http;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport.resolver;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.treewalk.filter;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util.io;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util.sha1;version="[5.10.1,5.11.0)",
+ org.eclipse.jgit.annotations;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.api.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.archive;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.attributes;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.awtui;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.blame;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.diff;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.dircache;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.events;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.fnmatch;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.gitrepo;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.hooks;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.ignore;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.ignore.internal;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.fsck;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.io;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.transport.connectivity;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.transport.http;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.internal.transport.parser;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.junit.time;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lfs;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib.internal;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.logging;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.merge;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.notes;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.patch;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.pgm;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.pgm.internal;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revplot;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk.filter;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.file;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.storage.pack;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.submodule;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.http;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport.resolver;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.treewalk.filter;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util.io;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util.sha1;version="[5.11.2,5.12.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.experimental.theories;version="[4.13,5.0.0)",
  org.junit.function;version="[4.13.0,5.0.0)",
diff --git a/org.eclipse.jgit.test/findBugs/FindBugsExcludeFilter.xml b/org.eclipse.jgit.test/findBugs/FindBugsExcludeFilter.xml
new file mode 100644
index 0000000..b4ef953
--- /dev/null
+++ b/org.eclipse.jgit.test/findBugs/FindBugsExcludeFilter.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<FindBugsFilter>
+     <!-- We want complete control over clone behavior and
+          don't want to use Object's clone implementation.
+       -->
+     <Match>
+       <Bug pattern="CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE" />
+     </Match>
+</FindBugsFilter>
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index 0037684..8a59070 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.test</artifactId>
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt
index e06b38c..527893a 100644
--- a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt
@@ -1,2 +1,2 @@
-some-domain1	TRUE	/some/path1	FALSE	1893499200000	key1	valueFromSimple1
-some-domain1	TRUE	/some/path1	FALSE	1893499200000	key2	valueFromSimple1
\ No newline at end of file
+some-domain1	TRUE	/some/path1	FALSE	1893499200	key1	valueFromSimple1
+some-domain1	TRUE	/some/path1	FALSE	1893499200	key2	valueFromSimple1
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt
index 4bf6723f..5ec0606 100644
--- a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt
@@ -1,2 +1,2 @@
-some-domain1	TRUE	/some/path1	FALSE	1893499200000	key1	valueFromSimple2
-some-domain1	TRUE	/some/path1	FALSE	1893499200000	key3	valueFromSimple2
\ No newline at end of file
+some-domain1	TRUE	/some/path1	FALSE	1893499200	key1	valueFromSimple2
+some-domain1	TRUE	/some/path1	FALSE	1893499200	key3	valueFromSimple2
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt
index a9b8a28..573ee9e 100644
--- a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt
@@ -3,6 +3,6 @@
 some-domain1	TRUE	/some/path1	FALSE	0	key1	value1
 
 # expires date is 01/01/2030 @ 12:00am (UTC)
-#HttpOnly_.some-domain2	TRUE	/some/path2	TRUE	1893499200000	key2	value2
+#HttpOnly_.some-domain2	TRUE	/some/path2	TRUE	1893499200	key2	value2
 
-some-domain3	TRUE	/some/path3	FALSE	1893499200000	key3	value3
\ No newline at end of file
+some-domain3	TRUE	/some/path3	FALSE	1893499200	key3	value3
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-milliseconds.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-milliseconds.txt
new file mode 100644
index 0000000..940e3b1
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-milliseconds.txt
@@ -0,0 +1,2 @@
+some-domain1	TRUE	/some/path1	FALSE	1893499200000	key1	valueFromSimple1
+some-domain1	TRUE	/some/path1	FALSE	1893499200	key2	valueFromSimple1
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java
index 0f98a63..f2cceac 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java
@@ -12,6 +12,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import java.beans.Statement;
 import java.io.BufferedInputStream;
@@ -28,6 +29,7 @@
 import java.util.List;
 import java.util.Map;
 
+import java.util.Random;
 import org.apache.commons.compress.archivers.ArchiveEntry;
 import org.apache.commons.compress.archivers.ArchiveInputStream;
 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
@@ -55,6 +57,7 @@
 import org.eclipse.jgit.util.StringUtils;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class ArchiveCommandTest extends RepositoryTestCase {
@@ -184,9 +187,55 @@
 
 	@Test
 	public void archiveHeadAllFilesTarTimestamps() throws Exception {
+		archiveHeadAllFiles("tar");
+	}
+
+	@Test
+	public void archiveHeadAllFilesTgzTimestamps() throws Exception {
+		archiveHeadAllFiles("tgz");
+	}
+
+	@Test
+	public void archiveHeadAllFilesTbz2Timestamps() throws Exception {
+		archiveHeadAllFiles("tbz2");
+	}
+
+	@Test
+	public void archiveHeadAllFilesTxzTimestamps() throws Exception {
+		archiveHeadAllFiles("txz");
+	}
+
+	@Test
+	public void archiveHeadAllFilesZipTimestamps() throws Exception {
+		archiveHeadAllFiles("zip");
+	}
+
+	@Test
+	public void archiveHeadAllFilesTgzWithCompressionReducesArchiveSize() throws Exception {
+		archiveHeadAllFilesWithCompression("tgz");
+	}
+
+	@Test
+	public void archiveHeadAllFilesTbz2WithCompressionReducesArchiveSize() throws Exception {
+		archiveHeadAllFilesWithCompression("tbz2");
+	}
+
+	@Test
+	@Ignore
+	public void archiveHeadAllFilesTxzWithCompressionReducesArchiveSize() throws Exception {
+		// We ignore this test because the txz format consumes a lot of memory for high level
+		// compressions.
+		archiveHeadAllFilesWithCompression("txz");
+	}
+
+	@Test
+	public void archiveHeadAllFilesZipWithCompressionReducesArchiveSize() throws Exception {
+		archiveHeadAllFilesWithCompression("zip");
+	}
+
+	private void archiveHeadAllFiles(String fmt) throws Exception {
 		try (Git git = new Git(db)) {
 			createTestContent(git);
-			String fmt = "tar";
 			File archive = new File(getTemporaryDirectory(),
 					"archive." + format);
 			archive(git, archive, fmt);
@@ -194,7 +243,7 @@
 
 			try (InputStream fi = Files.newInputStream(archive.toPath());
 					InputStream bi = new BufferedInputStream(fi);
-					ArchiveInputStream o = new TarArchiveInputStream(bi)) {
+					ArchiveInputStream o = createArchiveInputStream(fmt, bi)) {
 				assertEntries(o);
 			}
 
@@ -205,97 +254,42 @@
 		}
 	}
 
-	@Test
-	public void archiveHeadAllFilesTgzTimestamps() throws Exception {
+	@SuppressWarnings({ "serial", "boxing" })
+	private void archiveHeadAllFilesWithCompression(String fmt) throws Exception {
 		try (Git git = new Git(db)) {
-			createTestContent(git);
-			String fmt = "tgz";
+			createLargeTestContent(git);
 			File archive = new File(getTemporaryDirectory(),
-					"archive." + fmt);
-			archive(git, archive, fmt);
-			ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
+					"archive." + format);
 
-			try (InputStream fi = Files.newInputStream(archive.toPath());
-					InputStream bi = new BufferedInputStream(fi);
-					InputStream gzi = new GzipCompressorInputStream(bi);
-					ArchiveInputStream o = new TarArchiveInputStream(gzi)) {
-				assertEntries(o);
-			}
+			archive(git, archive, fmt, new HashMap<String, Object>() {{
+				put("compression-level", 1);
+			}});
+			int sizeCompression1 = getNumBytes(archive);
 
-			Thread.sleep(WAIT);
-			archive(git, archive, fmt);
-			assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
-					ObjectId.fromRaw(IO.readFully(archive)));
+			archive(git, archive, fmt, new HashMap<String, Object>() {{
+				put("compression-level", 9);
+			}});
+			int sizeCompression9 = getNumBytes(archive);
+
+			assertTrue(sizeCompression1 > sizeCompression9);
 		}
 	}
 
-	@Test
-	public void archiveHeadAllFilesTbz2Timestamps() throws Exception {
-		try (Git git = new Git(db)) {
-			createTestContent(git);
-			String fmt = "tbz2";
-			File archive = new File(getTemporaryDirectory(),
-					"archive." + fmt);
-			archive(git, archive, fmt);
-			ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
-
-			try (InputStream fi = Files.newInputStream(archive.toPath());
-					InputStream bi = new BufferedInputStream(fi);
-					InputStream gzi = new BZip2CompressorInputStream(bi);
-					ArchiveInputStream o = new TarArchiveInputStream(gzi)) {
-				assertEntries(o);
-			}
-
-			Thread.sleep(WAIT);
-			archive(git, archive, fmt);
-			assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
-					ObjectId.fromRaw(IO.readFully(archive)));
+	private static ArchiveInputStream createArchiveInputStream (String fmt, InputStream bi)
+			throws IOException {
+		switch (fmt) {
+			case "tar":
+				return new TarArchiveInputStream(bi);
+			case "tgz":
+				return new TarArchiveInputStream(new GzipCompressorInputStream(bi));
+			case "tbz2":
+				return new TarArchiveInputStream(new BZip2CompressorInputStream(bi));
+			case "txz":
+				return new TarArchiveInputStream(new XZCompressorInputStream(bi));
+			case "zip":
+				return new ZipArchiveInputStream(new BufferedInputStream(bi));
 		}
-	}
-
-	@Test
-	public void archiveHeadAllFilesTxzTimestamps() throws Exception {
-		try (Git git = new Git(db)) {
-			createTestContent(git);
-			String fmt = "txz";
-			File archive = new File(getTemporaryDirectory(), "archive." + fmt);
-			archive(git, archive, fmt);
-			ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
-
-			try (InputStream fi = Files.newInputStream(archive.toPath());
-					InputStream bi = new BufferedInputStream(fi);
-					InputStream gzi = new XZCompressorInputStream(bi);
-					ArchiveInputStream o = new TarArchiveInputStream(gzi)) {
-				assertEntries(o);
-			}
-
-			Thread.sleep(WAIT);
-			archive(git, archive, fmt);
-			assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
-					ObjectId.fromRaw(IO.readFully(archive)));
-		}
-	}
-
-	@Test
-	public void archiveHeadAllFilesZipTimestamps() throws Exception {
-		try (Git git = new Git(db)) {
-			createTestContent(git);
-			String fmt = "zip";
-			File archive = new File(getTemporaryDirectory(), "archive." + fmt);
-			archive(git, archive, fmt);
-			ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
-
-			try (InputStream fi = Files.newInputStream(archive.toPath());
-					InputStream bi = new BufferedInputStream(fi);
-					ArchiveInputStream o = new ZipArchiveInputStream(bi)) {
-				assertEntries(o);
-			}
-
-			Thread.sleep(WAIT);
-			archive(git, archive, fmt);
-			assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
-					ObjectId.fromRaw(IO.readFully(archive)));
-		}
+		throw new IllegalArgumentException("Format " + fmt + " is not supported.");
 	}
 
 	private void createTestContent(Git git) throws IOException, GitAPIException,
@@ -312,13 +306,40 @@
 		git.commit().setMessage("updated file").call();
 	}
 
+	private void createLargeTestContent(Git git) throws IOException, GitAPIException,
+			NoFilepatternException, NoHeadException, NoMessageException,
+			UnmergedPathsException, ConcurrentRefUpdateException,
+			WrongRepositoryStateException, AbortedByHookException {
+		StringBuilder largeContent = new StringBuilder();
+		Random r = new Random();
+		for (int i = 0; i < 2000; i++) {
+			for (int j = 0; j < 80; j++) {
+				largeContent.append((char)(r.nextInt(26) + 'a'));
+			}
+			largeContent.append("\n");
+		}
+		writeTrashFile("large_file.txt", largeContent.toString());
+		git.add().addFilepattern("large_file.txt").call();
+		git.commit().setMessage("create file").call();
+	}
+
 	private static void archive(Git git, File archive, String fmt)
 			throws GitAPIException,
 			FileNotFoundException, AmbiguousObjectException,
 			IncorrectObjectTypeException, IOException {
+		archive(git, archive, fmt, new HashMap<>());
+	}
+
+	private static void archive(Git git, File archive, String fmt, Map<String,
+			Object> options)
+			throws GitAPIException,
+			FileNotFoundException, AmbiguousObjectException,
+			IncorrectObjectTypeException, IOException {
 		git.archive().setOutputStream(new FileOutputStream(archive))
 				.setFormat(fmt)
-				.setTree(git.getRepository().resolve("HEAD")).call();
+				.setTree(git.getRepository().resolve("HEAD"))
+				.setFormatOptions(options)
+				.call();
 	}
 
 	private static void assertEntries(ArchiveInputStream o) throws IOException {
@@ -333,6 +354,13 @@
 		assertEquals(UNEXPECTED_ARCHIVE_SIZE, 2, n);
 	}
 
+	private static int getNumBytes(File archive) throws Exception {
+		try (InputStream fi = Files.newInputStream(archive.toPath());
+				InputStream bi = new BufferedInputStream(fi)) {
+			return bi.available();
+		}
+	}
+
 	private static class MockFormat
 			implements ArchiveCommand.Format<MockOutputStream> {
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java
index 0a0a88c..e520732 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java
@@ -147,6 +147,55 @@
 	}
 
 	@Test
+	public void testCheckoutForcedNoChangeNotInIndex() throws Exception {
+		git.checkout().setCreateBranch(true).setName("test2").call();
+		File f = writeTrashFile("NewFile.txt", "New file");
+		git.add().addFilepattern("NewFile.txt").call();
+		git.commit().setMessage("New file created").call();
+		git.checkout().setName("test").call();
+		assertFalse("NewFile.txt should not exist", f.exists());
+		writeTrashFile("NewFile.txt", "New file");
+		git.add().addFilepattern("NewFile.txt").call();
+		git.commit().setMessage("New file created again with same content")
+				.call();
+		// Now remove the file from the index only. So it exists in both
+		// commits, and in the working tree, but not in the index.
+		git.rm().addFilepattern("NewFile.txt").setCached(true).call();
+		assertTrue("NewFile.txt should exist", f.isFile());
+		git.checkout().setForced(true).setName("test2").call();
+		assertTrue("NewFile.txt should exist", f.isFile());
+		assertEquals(Constants.R_HEADS + "test2", git.getRepository()
+				.exactRef(Constants.HEAD).getTarget().getName());
+		assertTrue("Force checkout should have undone git rm --cached",
+				git.status().call().isClean());
+	}
+
+	@Test
+	public void testCheckoutNoChangeNotInIndex() throws Exception {
+		git.checkout().setCreateBranch(true).setName("test2").call();
+		File f = writeTrashFile("NewFile.txt", "New file");
+		git.add().addFilepattern("NewFile.txt").call();
+		git.commit().setMessage("New file created").call();
+		git.checkout().setName("test").call();
+		assertFalse("NewFile.txt should not exist", f.exists());
+		writeTrashFile("NewFile.txt", "New file");
+		git.add().addFilepattern("NewFile.txt").call();
+		git.commit().setMessage("New file created again with same content")
+				.call();
+		// Now remove the file from the index only. So it exists in both
+		// commits, and in the working tree, but not in the index.
+		git.rm().addFilepattern("NewFile.txt").setCached(true).call();
+		assertTrue("NewFile.txt should exist", f.isFile());
+		git.checkout().setName("test2").call();
+		assertTrue("NewFile.txt should exist", f.isFile());
+		assertEquals(Constants.R_HEADS + "test2", git.getRepository()
+				.exactRef(Constants.HEAD).getTarget().getName());
+		org.eclipse.jgit.api.Status status = git.status().call();
+		assertEquals("[NewFile.txt]", status.getRemoved().toString());
+		assertEquals("[NewFile.txt]", status.getUntracked().toString());
+	}
+
+	@Test
 	public void testCreateBranchOnCheckout() throws Exception {
 		git.checkout().setCreateBranch(true).setName("test2").call();
 		assertNotNull(db.exactRef("refs/heads/test2"));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/InitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/InitCommandTest.java
index 1c18b5a..48d835e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/InitCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/InitCommandTest.java
@@ -9,6 +9,7 @@
  */
 package org.eclipse.jgit.api;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -21,8 +22,10 @@
 import org.eclipse.jgit.errors.NoWorkTreeException;
 import org.eclipse.jgit.junit.MockSystemReader;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.util.SystemReader;
 import org.junit.Before;
 import org.junit.Test;
@@ -42,7 +45,73 @@
 		InitCommand command = new InitCommand();
 		command.setDirectory(directory);
 		try (Git git = command.call()) {
-			assertNotNull(git.getRepository());
+			Repository r = git.getRepository();
+			assertNotNull(r);
+			assertEquals("refs/heads/master", r.getFullBranch());
+		}
+	}
+
+	@Test
+	public void testInitRepositoryMainInitialBranch()
+			throws IOException, JGitInternalException, GitAPIException {
+		File directory = createTempDirectory("testInitRepository");
+		InitCommand command = new InitCommand();
+		command.setDirectory(directory);
+		command.setInitialBranch("main");
+		try (Git git = command.call()) {
+			Repository r = git.getRepository();
+			assertNotNull(r);
+			assertEquals("refs/heads/main", r.getFullBranch());
+		}
+	}
+
+	@Test
+	public void testInitRepositoryCustomDefaultBranch()
+			throws Exception {
+		File directory = createTempDirectory("testInitRepository");
+		InitCommand command = new InitCommand();
+		command.setDirectory(directory);
+		MockSystemReader reader = (MockSystemReader) SystemReader.getInstance();
+		StoredConfig c = reader.getUserConfig();
+		String old = c.getString(ConfigConstants.CONFIG_INIT_SECTION, null,
+				ConfigConstants.CONFIG_KEY_DEFAULT_BRANCH);
+		c.setString(ConfigConstants.CONFIG_INIT_SECTION, null,
+				ConfigConstants.CONFIG_KEY_DEFAULT_BRANCH, "main");
+		try (Git git = command.call()) {
+			Repository r = git.getRepository();
+			assertNotNull(r);
+			assertEquals("refs/heads/main", r.getFullBranch());
+		} finally {
+			c.setString(ConfigConstants.CONFIG_INIT_SECTION, null,
+					ConfigConstants.CONFIG_KEY_DEFAULT_BRANCH, old);
+		}
+	}
+
+	@Test
+	public void testInitRepositoryNullInitialBranch() throws Exception {
+		File directory = createTempDirectory("testInitRepository");
+		InitCommand command = new InitCommand();
+		command.setDirectory(directory);
+		command.setInitialBranch("main");
+		command.setInitialBranch(null);
+		try (Git git = command.call()) {
+			Repository r = git.getRepository();
+			assertNotNull(r);
+			assertEquals("refs/heads/master", r.getFullBranch());
+		}
+	}
+
+	@Test
+	public void testInitRepositoryEmptyInitialBranch() throws Exception {
+		File directory = createTempDirectory("testInitRepository");
+		InitCommand command = new InitCommand();
+		command.setDirectory(directory);
+		command.setInitialBranch("main");
+		command.setInitialBranch("");
+		try (Git git = command.call()) {
+			Repository r = git.getRepository();
+			assertNotNull(r);
+			assertEquals("refs/heads/master", r.getFullBranch());
 		}
 	}
 
@@ -72,6 +141,23 @@
 			Repository repository = git.getRepository();
 			assertNotNull(repository);
 			assertTrue(repository.isBare());
+			assertEquals("refs/heads/master", repository.getFullBranch());
+		}
+	}
+
+	@Test
+	public void testInitBareRepositoryMainInitialBranch()
+			throws IOException, JGitInternalException, GitAPIException {
+		File directory = createTempDirectory("testInitBareRepository");
+		InitCommand command = new InitCommand();
+		command.setDirectory(directory);
+		command.setBare(true);
+		command.setInitialBranch("main");
+		try (Git git = command.call()) {
+			Repository repository = git.getRepository();
+			assertNotNull(repository);
+			assertTrue(repository.isBare());
+			assertEquals("refs/heads/main", repository.getFullBranch());
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java
index 6460c79..c563d5a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java
@@ -232,6 +232,53 @@
 		assertFalse(i.hasNext());
 	}
 
+	/**
+	 * <pre>
+	 * A - B - C - M
+	 *      \     /
+	 *        -D(side)
+	 * </pre>
+	 */
+	@Test
+	public void addRangeWithMerge() throws Exception{
+		String fileA = "fileA";
+		String fileB = "fileB";
+		Git git = Git.wrap(db);
+
+		writeTrashFile(fileA, fileA);
+		git.add().addFilepattern(fileA).call();
+		git.commit().setMessage("commit a").call();
+
+		writeTrashFile(fileA, fileA);
+		git.add().addFilepattern(fileA).call();
+		RevCommit b = git.commit().setMessage("commit b").call();
+
+		writeTrashFile(fileA, fileA);
+		git.add().addFilepattern(fileA).call();
+		RevCommit c = git.commit().setMessage("commit c").call();
+
+		createBranch(b, "refs/heads/side");
+		checkoutBranch("refs/heads/side");
+
+		writeTrashFile(fileB, fileB);
+		git.add().addFilepattern(fileB).call();
+		RevCommit d = git.commit().setMessage("commit d").call();
+
+		checkoutBranch("refs/heads/master");
+		MergeResult m = git.merge().include(d.getId()).call();
+		assertEquals(MergeResult.MergeStatus.MERGED, m.getMergeStatus());
+
+		Iterator<RevCommit> rangeLog = git.log().addRange(b.getId(), m.getNewHead()).call().iterator();
+
+		RevCommit commit = rangeLog.next();
+		assertEquals(m.getNewHead(), commit.getId());
+		commit = rangeLog.next();
+		assertEquals(c.getId(), commit.getId());
+		commit = rangeLog.next();
+		assertEquals(d.getId(), commit.getId());
+		assertFalse(rangeLog.hasNext());
+	}
+
 	private void setCommitsAndMerge() throws Exception {
 		Git git = Git.wrap(db);
 		writeTrashFile("file1", "1\n2\n3\n4\n");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/TagCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/TagCommandTest.java
index b1c54b9..9903417 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/TagCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/TagCommandTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, 2013 Chris Aniszczyk <caniszczyk@gmail.com> and others
+ * Copyright (C) 2010, 2020 Chris Aniszczyk <caniszczyk@gmail.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -11,6 +11,9 @@
 
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import java.io.IOException;
@@ -19,8 +22,10 @@
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.InvalidTagNameException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -29,6 +34,59 @@
 public class TagCommandTest extends RepositoryTestCase {
 
 	@Test
+	public void testTagKind() {
+		try (Git git = new Git(db)) {
+			assertTrue(git.tag().isAnnotated());
+			assertTrue(git.tag().setSigned(true).isAnnotated());
+			assertTrue(git.tag().setSigned(false).isAnnotated());
+			assertTrue(git.tag().setSigningKey(null).isAnnotated());
+			assertTrue(git.tag().setSigningKey("something").isAnnotated());
+			assertTrue(git.tag().setSigned(false).setSigningKey(null)
+					.isAnnotated());
+			assertTrue(git.tag().setSigned(false).setSigningKey("something")
+					.isAnnotated());
+			assertTrue(git.tag().setSigned(true).setSigningKey(null)
+					.isAnnotated());
+			assertTrue(git.tag().setSigned(true).setSigningKey("something")
+					.isAnnotated());
+			assertTrue(git.tag().setAnnotated(true).isAnnotated());
+			assertTrue(
+					git.tag().setAnnotated(true).setSigned(true).isAnnotated());
+			assertTrue(git.tag().setAnnotated(true).setSigned(false)
+					.isAnnotated());
+			assertTrue(git.tag().setAnnotated(true).setSigningKey(null)
+					.isAnnotated());
+			assertTrue(git.tag().setAnnotated(true).setSigningKey("something")
+					.isAnnotated());
+			assertTrue(git.tag().setAnnotated(true).setSigned(false)
+					.setSigningKey(null).isAnnotated());
+			assertTrue(git.tag().setAnnotated(true).setSigned(false)
+					.setSigningKey("something").isAnnotated());
+			assertTrue(git.tag().setAnnotated(true).setSigned(true)
+					.setSigningKey(null).isAnnotated());
+			assertTrue(git.tag().setAnnotated(true).setSigned(true)
+					.setSigningKey("something").isAnnotated());
+			assertFalse(git.tag().setAnnotated(false).isAnnotated());
+			assertTrue(git.tag().setAnnotated(false).setSigned(true)
+					.isAnnotated());
+			assertFalse(git.tag().setAnnotated(false).setSigned(false)
+					.isAnnotated());
+			assertFalse(git.tag().setAnnotated(false).setSigningKey(null)
+					.isAnnotated());
+			assertTrue(git.tag().setAnnotated(false).setSigningKey("something")
+					.isAnnotated());
+			assertFalse(git.tag().setAnnotated(false).setSigned(false)
+					.setSigningKey(null).isAnnotated());
+			assertTrue(git.tag().setAnnotated(false).setSigned(false)
+					.setSigningKey("something").isAnnotated());
+			assertTrue(git.tag().setAnnotated(false).setSigned(true)
+					.setSigningKey(null).isAnnotated());
+			assertTrue(git.tag().setAnnotated(false).setSigned(true)
+					.setSigningKey("something").isAnnotated());
+		}
+	}
+
+	@Test
 	public void testTaggingOnHead() throws GitAPIException, IOException {
 		try (Git git = new Git(db);
 				RevWalk walk = new RevWalk(db)) {
@@ -67,6 +125,29 @@
 	}
 
 	@Test
+	public void testForceNoChangeLightweight() throws GitAPIException {
+		try (Git git = new Git(db)) {
+			git.commit().setMessage("initial commit").call();
+			RevCommit commit = git.commit().setMessage("second commit").call();
+			git.commit().setMessage("third commit").call();
+			Ref tagRef = git.tag().setObjectId(commit).setName("tag")
+					.setAnnotated(false).call();
+			assertEquals(commit.getId(), tagRef.getObjectId());
+			// Without force, we want to get a RefAlreadyExistsException
+			RefAlreadyExistsException e = assertThrows(
+					RefAlreadyExistsException.class,
+					() -> git.tag().setObjectId(commit).setName("tag")
+							.setAnnotated(false).call());
+			assertEquals(RefUpdate.Result.NO_CHANGE, e.getUpdateResult());
+			// With force the call should work
+			assertEquals(commit.getId(),
+					git.tag().setObjectId(commit).setName("tag")
+							.setAnnotated(false).setForceUpdate(true).call()
+							.getObjectId());
+		}
+	}
+
+	@Test
 	public void testEmptyTagName() throws GitAPIException {
 		try (Git git = new Git(db)) {
 			git.commit().setMessage("initial commit").call();
@@ -93,19 +174,6 @@
 		}
 	}
 
-	@Test
-	public void testFailureOnSignedTags() throws GitAPIException {
-		try (Git git = new Git(db)) {
-			git.commit().setMessage("initial commit").call();
-			try {
-				git.tag().setSigned(true).setName("tag").call();
-				fail("We should have failed with an UnsupportedOperationException due to signed tag");
-			} catch (UnsupportedOperationException e) {
-				// should hit here
-			}
-		}
-	}
-
 	private List<Ref> getTags() throws Exception {
 		return db.getRefDatabase().getRefsByPrefix(R_TAGS);
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmappedObjectReachabilityTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/BitmappedObjectReachabilityTest.java
similarity index 89%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmappedObjectReachabilityTest.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/BitmappedObjectReachabilityTest.java
index d2b6e89..f2f7405 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmappedObjectReachabilityTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/BitmappedObjectReachabilityTest.java
@@ -7,11 +7,12 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.internal.storage.file.GC;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.ObjectReachabilityChecker;
 
 public class BitmappedObjectReachabilityTest
 	extends ObjectReachabilityTestCase {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmappedReachabilityCheckerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityCheckerTest.java
similarity index 91%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmappedReachabilityCheckerTest.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityCheckerTest.java
index 2cb88b9..5833c7a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmappedReachabilityCheckerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityCheckerTest.java
@@ -7,13 +7,14 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import static org.junit.Assert.assertNotNull;
 
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.internal.storage.file.GC;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.ReachabilityChecker;
 
 public class BitmappedReachabilityCheckerTest
 		extends ReachabilityCheckerTestCase {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectReachabilityTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java
similarity index 94%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectReachabilityTestCase.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java
index 267b163..37ff40b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectReachabilityTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java
@@ -7,7 +7,7 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -20,6 +20,10 @@
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.junit.TestRepository.CommitBuilder;
+import org.eclipse.jgit.revwalk.ObjectReachabilityChecker;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
 import org.junit.Before;
 import org.junit.Test;
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/PedestrianObjectReachabilityTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/PedestrianObjectReachabilityTest.java
similarity index 87%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/PedestrianObjectReachabilityTest.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/PedestrianObjectReachabilityTest.java
index b1c9556..f06a768 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/PedestrianObjectReachabilityTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/PedestrianObjectReachabilityTest.java
@@ -7,10 +7,11 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.ObjectReachabilityChecker;
 
 public class PedestrianObjectReachabilityTest
 		extends ObjectReachabilityTestCase {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/PedestrianReachabilityCheckerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/PedestrianReachabilityCheckerTest.java
similarity index 87%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/PedestrianReachabilityCheckerTest.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/PedestrianReachabilityCheckerTest.java
index 3029e05..f9d4e18 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/PedestrianReachabilityCheckerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/PedestrianReachabilityCheckerTest.java
@@ -7,10 +7,11 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.ReachabilityChecker;
 
 public class PedestrianReachabilityCheckerTest
 		extends ReachabilityCheckerTestCase {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ReachabilityCheckerTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java
similarity index 96%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ReachabilityCheckerTestCase.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java
index 4695eaa..1ff6e7d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ReachabilityCheckerTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java
@@ -7,7 +7,7 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -19,6 +19,8 @@
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.ReachabilityChecker;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackDescriptionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackDescriptionTest.java
index 18cf117..b2c8ad5 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackDescriptionTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackDescriptionTest.java
@@ -13,7 +13,6 @@
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.COMPACT;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_REST;
-import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_TXN;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.RECEIVE;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
@@ -82,7 +81,7 @@
 				DfsPackDescription.objectLookupComparator(
 					new PackSource.ComparatorBuilder()
 						.add(GC)
-						.add(INSERT, RECEIVE, GC_REST, GC_TXN, UNREACHABLE_GARBAGE)
+						.add(INSERT, RECEIVE, GC_REST, UNREACHABLE_GARBAGE)
 						.add(COMPACT)
 						.build()),
 				a, b);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackSourceTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackSourceTest.java
index 6fcd4ac..dfd1129 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackSourceTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackSourceTest.java
@@ -14,7 +14,6 @@
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.DEFAULT_COMPARATOR;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_REST;
-import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_TXN;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.RECEIVE;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
@@ -30,7 +29,6 @@
 		assertEquals(0, DEFAULT_COMPARATOR.compare(COMPACT, COMPACT));
 		assertEquals(0, DEFAULT_COMPARATOR.compare(GC, GC));
 		assertEquals(0, DEFAULT_COMPARATOR.compare(GC_REST, GC_REST));
-		assertEquals(0, DEFAULT_COMPARATOR.compare(GC_TXN, GC_TXN));
 		assertEquals(0, DEFAULT_COMPARATOR.compare(UNREACHABLE_GARBAGE, UNREACHABLE_GARBAGE));
 
 		assertEquals(0, DEFAULT_COMPARATOR.compare(INSERT, RECEIVE));
@@ -47,11 +45,5 @@
 
 		assertEquals(-1, DEFAULT_COMPARATOR.compare(GC, GC_REST));
 		assertEquals(1, DEFAULT_COMPARATOR.compare(GC_REST, GC));
-
-		assertEquals(-1, DEFAULT_COMPARATOR.compare(GC_REST, GC_TXN));
-		assertEquals(1, DEFAULT_COMPARATOR.compare(GC_TXN, GC_REST));
-
-		assertEquals(-1, DEFAULT_COMPARATOR.compare(GC_TXN, UNREACHABLE_GARBAGE));
-		assertEquals(1, DEFAULT_COMPARATOR.compare(UNREACHABLE_GARBAGE, GC_TXN));
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
index 45d864d..bd36337 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
@@ -28,6 +28,7 @@
 import java.util.List;
 
 import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
@@ -144,10 +145,9 @@
 			objects.add(new PackedObjectInfo(ObjectId.fromRaw(idBuf)));
 		}
 
-		String packName = "pack-" + id.name();
 		File packDir = db.getObjectDatabase().getPackDirectory();
-		File idxFile = new File(packDir, packName + ".idx");
-		File packFile = new File(packDir, packName + ".pack");
+		PackFile idxFile = new PackFile(packDir, id, PackExt.INDEX);
+		PackFile packFile = idxFile.create(PackExt.PACK);
 		FileUtils.mkdir(packDir, true);
 		try (OutputStream dst = new BufferedOutputStream(
 				new FileOutputStream(idxFile))) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java
index da3b5bb..df5d952 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java
@@ -27,6 +27,7 @@
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -193,9 +194,10 @@
 				pw.addObject(o);
 			}
 
-			final ObjectId name = pw.computeName();
-			final File packFile = fullPackFileName(name, ".pack");
-			final File idxFile = fullPackFileName(name, ".idx");
+			PackFile packFile = new PackFile(
+					db.getObjectDatabase().getPackDirectory(), pw.computeName(),
+					PackExt.PACK);
+			PackFile idxFile = packFile.create(PackExt.INDEX);
 			final File[] files = new File[] { packFile, idxFile };
 			write(files, pw);
 			return files;
@@ -242,11 +244,6 @@
 		}
 	}
 
-	private File fullPackFileName(ObjectId name, String suffix) {
-		final File packdir = db.getObjectDatabase().getPackDirectory();
-		return new File(packdir, "pack-" + name.name() + suffix);
-	}
-
 	private RevObject writeBlob(Repository repo, String data)
 			throws IOException {
 		final byte[] bytes = Constants.encode(data);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java
index 72bff16..6c74f00 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java
@@ -18,7 +18,8 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.util.Arrays;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -90,8 +91,9 @@
 				assertEquals(ObjectId.zeroId(), c.getRef().getObjectId());
 			}
 
-			List<String> files = Arrays.asList(reftableDir.listFiles()).stream()
-					.map(File::getName).collect(Collectors.toList());
+			List<String> files = Files.list(reftableDir.toPath())
+					.map(Path::getFileName).map(Path::toString)
+					.collect(Collectors.toList());
 			Collections.sort(files);
 
 			assertTrue(files.size() < 20);
@@ -130,11 +132,14 @@
 				});
 				assertTrue(ok);
 
-				List<File> files = Arrays.asList(reftableDir.listFiles());
+				List<Path> files = Files.list(reftableDir.toPath())
+						.collect(Collectors.toList());
 				for (int j = 0; j < files.size(); j++) {
-					File f = files.get(j);
-					if (f.getName().endsWith(".ref")) {
-						assertTrue(f.delete());
+					Path f = files.get(j);
+					Path fileName = f.getFileName();
+					if (fileName != null
+							&& fileName.toString().endsWith(".ref")) {
+						Files.delete(f);
 						break outer;
 					}
 				}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java
index 33bacbe..15c9109 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java
@@ -28,14 +28,18 @@
 import java.io.IOException;
 import java.security.SecureRandom;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 
+import java.util.Set;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefRename;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
@@ -579,6 +583,64 @@
 		assertEquals(Ref.Storage.PACKED, b.getStorage());
 	}
 
+	@Test
+	public void testGetRefsExcludingPrefix() throws IOException {
+		Set<String> prefixes = new HashSet<>();
+		prefixes.add("refs/tags");
+		// HEAD + 12 refs/heads are present here.
+		List<Ref> refs =
+				db.getRefDatabase().getRefsByPrefixWithExclusions(RefDatabase.ALL, prefixes);
+		assertEquals(13, refs.size());
+		checkContainsRef(refs, db.exactRef("HEAD"));
+		checkContainsRef(refs, db.exactRef("refs/heads/a"));
+		for (Ref notInResult : db.getRefDatabase().getRefsByPrefix("refs/tags")) {
+			assertFalse(refs.contains(notInResult));
+		}
+	}
+
+	@Test
+	public void testGetRefsExcludingPrefixes() throws IOException {
+		Set<String> exclude = new HashSet<>();
+		exclude.add("refs/tags/");
+		exclude.add("refs/heads/");
+		List<Ref> refs = db.getRefDatabase().getRefsByPrefixWithExclusions(RefDatabase.ALL, exclude);
+		assertEquals(1, refs.size());
+		checkContainsRef(refs, db.exactRef("HEAD"));
+	}
+
+	@Test
+	public void testGetRefsExcludingNonExistingPrefixes() throws IOException {
+		Set<String> exclude = new HashSet<>();
+		exclude.add("refs/tags/");
+		exclude.add("refs/heads/");
+		exclude.add("refs/nonexistent/");
+		List<Ref> refs = db.getRefDatabase().getRefsByPrefixWithExclusions(RefDatabase.ALL, exclude);
+		assertEquals(1, refs.size());
+		checkContainsRef(refs, db.exactRef("HEAD"));
+	}
+
+	@Test
+	public void testGetRefsWithPrefixExcludingPrefixes() throws IOException {
+		Set<String> exclude = new HashSet<>();
+		exclude.add("refs/heads/pa");
+		String include = "refs/heads/p";
+		List<Ref> refs = db.getRefDatabase().getRefsByPrefixWithExclusions(include, exclude);
+		assertEquals(1, refs.size());
+		checkContainsRef(refs, db.exactRef("refs/heads/prefix/a"));
+	}
+
+	@Test
+	public void testGetRefsWithPrefixExcludingOverlappingPrefixes() throws IOException {
+		Set<String> exclude = new HashSet<>();
+		exclude.add("refs/heads/pa");
+		exclude.add("refs/heads/");
+		exclude.add("refs/heads/p");
+		exclude.add("refs/tags/");
+		List<Ref> refs = db.getRefDatabase().getRefsByPrefixWithExclusions(RefDatabase.ALL, exclude);
+		assertEquals(1, refs.size());
+		checkContainsRef(refs, db.exactRef("HEAD"));
+	}
+
 	private RefUpdate updateRef(String name) throws IOException {
 		final RefUpdate ref = db.updateRef(name);
 		ref.setNewObjectId(db.resolve(Constants.HEAD));
@@ -596,4 +658,14 @@
 			fail("link " + src + " to " + dst);
 		}
 	}
+
+	private static void checkContainsRef(Collection<Ref> haystack, Ref needle) {
+		for (Ref ref : haystack) {
+			if (ref.getName().equals(needle.getName()) &&
+					ref.getObjectId().equals(needle.getObjectId())) {
+				return;
+			}
+		}
+		fail("list " + haystack + " does not contain ref " + needle);
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
index d007dd4..8dc1ddb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
@@ -23,6 +23,7 @@
 
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -157,7 +158,7 @@
 				.create();
 		tr.update("refs/tags/t1", second);
 
-		Collection<PackFile> oldPacks = tr.getRepository().getObjectDatabase()
+		Collection<Pack> oldPacks = tr.getRepository().getObjectDatabase()
 				.getPacks();
 		assertEquals(0, oldPacks.size());
 		stats = gc.getStatistics();
@@ -171,7 +172,7 @@
 		stats = gc.getStatistics();
 		assertEquals(0, stats.numberOfLooseObjects);
 
-		List<PackFile> packs = new ArrayList<>(
+		List<Pack> packs = new ArrayList<>(
 				repo.getObjectDatabase().getPacks());
 		assertEquals(11, packs.get(0).getObjectCount());
 	}
@@ -295,7 +296,7 @@
 		// pack loose object into packfile
 		gc.setExpireAgeMillis(0);
 		gc.gc();
-		File oldPackfile = tr.getRepository().getObjectDatabase().getPacks()
+		PackFile oldPackfile = tr.getRepository().getObjectDatabase().getPacks()
 				.iterator().next().getPackFile();
 		assertTrue(oldPackfile.exists());
 
@@ -309,12 +310,59 @@
 		configureGc(gc, false).setPreserveOldPacks(true);
 		gc.gc();
 
-		File oldPackDir = repo.getObjectDatabase().getPreservedDirectory();
-		String oldPackFileName = oldPackfile.getName();
-		String oldPackName = oldPackFileName.substring(0,
-				oldPackFileName.lastIndexOf('.')) + ".old-pack";  //$NON-NLS-1$
-		File preservePackFile = new File(oldPackDir, oldPackName);
-		assertTrue(preservePackFile.exists());
+		File preservedPackFile = oldPackfile.createPreservedForDirectory(
+				repo.getObjectDatabase().getPreservedDirectory());
+		assertTrue(preservedPackFile.exists());
+	}
+
+	@Test
+	public void testPruneAndRestoreOldPacks() throws Exception {
+		String tempRef = "refs/heads/soon-to-be-unreferenced";
+		BranchBuilder bb = tr.branch(tempRef);
+		bb.commit().add("A", "A").add("B", "B").create();
+
+		// Verify setup conditions
+		stats = gc.getStatistics();
+		assertEquals(4, stats.numberOfLooseObjects);
+		assertEquals(0, stats.numberOfPackedObjects);
+
+		// Force all referenced objects into packs (to avoid having loose objects)
+		configureGc(gc, false);
+		gc.setExpireAgeMillis(0);
+		gc.setPackExpireAgeMillis(0);
+		gc.gc();
+		stats = gc.getStatistics();
+		assertEquals(0, stats.numberOfLooseObjects);
+		assertEquals(4, stats.numberOfPackedObjects);
+		assertEquals(1, stats.numberOfPackFiles);
+
+		// Delete the temp ref, orphaning its commit
+		RefUpdate update = tr.getRepository().getRefDatabase().newUpdate(tempRef, false);
+		update.setForceUpdate(true);
+		ObjectId objectId = update.getOldObjectId(); // remember it so we can restore it!
+		RefUpdate.Result result = update.delete();
+		assertEquals(RefUpdate.Result.FORCED, result);
+
+		fsTick();
+
+		// Repack with only orphaned commit, so packfile will be pruned
+		configureGc(gc, false).setPreserveOldPacks(true);
+		gc.gc();
+		stats = gc.getStatistics();
+		assertEquals(0, stats.numberOfLooseObjects);
+		assertEquals(0, stats.numberOfPackedObjects);
+		assertEquals(0, stats.numberOfPackFiles);
+
+		// Restore the temp ref to the deleted commit, should restore old-packs!
+		update = tr.getRepository().getRefDatabase().newUpdate(tempRef, false);
+		update.setNewObjectId(objectId);
+		update.setExpectedOldObjectId(null);
+		result = update.update();
+		assertEquals(RefUpdate.Result.NEW, result);
+
+		stats = gc.getStatistics();
+		assertEquals(4, stats.numberOfPackedObjects);
+		assertEquals(1, stats.numberOfPackFiles);
 	}
 
 	private PackConfig configureGc(GC myGc, boolean aggressive) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java
index bb8455f..5cac1e3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java
@@ -156,8 +156,8 @@
 		}
 	}
 
-	PackFile getSinglePack(FileRepository r) {
-		Collection<PackFile> packs = r.getObjectDatabase().getPacks();
+	Pack getSinglePack(FileRepository r) {
+		Collection<Pack> packs = r.getObjectDatabase().getPacks();
 		assertEquals(1, packs.size());
 		return packs.iterator().next();
 	}
@@ -206,11 +206,11 @@
 		SampleDataRepositoryTestCase.copyCGitTestPacks(repo);
 		ExecutorService executor = Executors.newSingleThreadExecutor();
 		final CountDownLatch latch = new CountDownLatch(1);
-		Future<Collection<PackFile>> result = executor.submit(() -> {
+		Future<Collection<Pack>> result = executor.submit(() -> {
 			long start = System.currentTimeMillis();
 			System.out.println("starting gc");
 			latch.countDown();
-			Collection<PackFile> r = gc.gc();
+			Collection<Pack> r = gc.gc();
 			System.out.println(
 					"gc took " + (System.currentTimeMillis() - start) + " ms");
 			return r;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java
index e155958..5fcdd37 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java
@@ -14,10 +14,10 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import java.io.File;
 import java.util.Iterator;
 
 import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.junit.Test;
 
@@ -36,14 +36,11 @@
 		assertEquals(4, stats.numberOfPackedObjects);
 		assertEquals(1, stats.numberOfPackFiles);
 
-		Iterator<PackFile> packIt = repo.getObjectDatabase().getPacks()
+		Iterator<Pack> packIt = repo.getObjectDatabase().getPacks()
 				.iterator();
-		PackFile singlePack = packIt.next();
+		Pack singlePack = packIt.next();
 		assertFalse(packIt.hasNext());
-		String packFileName = singlePack.getPackFile().getPath();
-		String keepFileName = packFileName.substring(0,
-				packFileName.lastIndexOf('.')) + ".keep";
-		File keepFile = new File(keepFileName);
+		PackFile keepFile = singlePack.getPackFile().create(PackExt.KEEP);
 		assertFalse(keepFile.exists());
 		assertTrue(keepFile.createNewFile());
 		bb.commit().add("A", "A2").add("B", "B2").create();
@@ -58,7 +55,7 @@
 		assertEquals(2, stats.numberOfPackFiles);
 
 		// check that no object is packed twice
-		Iterator<PackFile> packs = repo.getObjectDatabase().getPacks()
+		Iterator<Pack> packs = repo.getObjectDatabase().getPacks()
 				.iterator();
 		PackIndex ind1 = packs.next().getIndex();
 		assertEquals(4, ind1.getObjectCount());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java
index 796df3d..7386621 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java
@@ -12,6 +12,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
@@ -89,6 +90,7 @@
 
 	private void assertNoEmptyFanoutDirectories() {
 		File[] fanout = repo.getObjectsDirectory().listFiles();
+		assertNotNull(fanout);
 		for (File f : fanout) {
 			if (f.isDirectory()) {
 				String[] entries = f.list();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
index a0bc63a..316e336 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
@@ -206,17 +206,17 @@
 				.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b");
 		WindowCursor curs = new WindowCursor(db.getObjectDatabase());
 
-		ObjectDirectory mock = mock(ObjectDirectory.class);
+		LooseObjects mock = mock(LooseObjects.class);
 		UnpackedObjectCache unpackedObjectCacheMock = mock(
 				UnpackedObjectCache.class);
 
 		Mockito.when(mock.getObjectLoader(any(), any(), any()))
 				.thenThrow(new IOException("Stale File Handle"));
-		Mockito.when(mock.openLooseObject(curs, id)).thenCallRealMethod();
+		Mockito.when(mock.open(curs, id)).thenCallRealMethod();
 		Mockito.when(mock.unpackedObjectCache())
 				.thenReturn(unpackedObjectCacheMock);
 
-		assertNull(mock.openLooseObject(curs, id));
+		assertNull(mock.open(curs, id));
 		verify(unpackedObjectCacheMock).remove(id);
 	}
 
@@ -226,13 +226,13 @@
 				.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b");
 		WindowCursor curs = new WindowCursor(db.getObjectDatabase());
 
-		ObjectDirectory mock = mock(ObjectDirectory.class);
+		LooseObjects mock = mock(LooseObjects.class);
 
 		Mockito.when(mock.getObjectLoader(any(), any(), any()))
 				.thenThrow(new IOException("some IO failure"));
-		Mockito.when(mock.openLooseObject(curs, id)).thenCallRealMethod();
+		Mockito.when(mock.open(curs, id)).thenCallRealMethod();
 
-		assertThrows(IOException.class, () -> mock.openLooseObject(curs, id));
+		assertThrows(IOException.class, () -> mock.open(curs, id));
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java
index ac65c33..7c32ce7 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileSnapshotTest.java
@@ -11,6 +11,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
@@ -71,14 +72,14 @@
 		c.setInt(ConfigConstants.CONFIG_GC_SECTION, null,
 				ConfigConstants.CONFIG_KEY_AUTOPACKLIMIT, 1);
 		c.save();
-		Collection<PackFile> packs = gc(Deflater.NO_COMPRESSION);
+		Collection<Pack> packs = gc(Deflater.NO_COMPRESSION);
 		assertEquals("expected 1 packfile after gc", 1, packs.size());
-		PackFile p1 = packs.iterator().next();
+		Pack p1 = packs.iterator().next();
 		PackFileSnapshot snapshot = p1.getFileSnapshot();
 
 		packs = gc(Deflater.BEST_COMPRESSION);
 		assertEquals("expected 1 packfile after gc", 1, packs.size());
-		PackFile p2 = packs.iterator().next();
+		Pack p2 = packs.iterator().next();
 		File pf = p2.getPackFile();
 
 		// changing compression level with aggressive gc may change size,
@@ -152,11 +153,11 @@
 		createTestRepo(testDataSeed, testDataLength);
 
 		// repack to create initial packfile
-		PackFile pf = repackAndCheck(5, null, null, null);
-		Path packFilePath = pf.getPackFile().toPath();
-		AnyObjectId chk1 = pf.getPackChecksum();
-		String name = pf.getPackName();
-		Long length = Long.valueOf(pf.getPackFile().length());
+		Pack p = repackAndCheck(5, null, null, null);
+		Path packFilePath = p.getPackFile().toPath();
+		AnyObjectId chk1 = p.getPackChecksum();
+		String name = p.getPackName();
+		Long length = Long.valueOf(p.getPackFile().length());
 		FS fs = db.getFS();
 		Instant m1 = fs.lastModifiedInstant(packFilePath);
 
@@ -206,13 +207,16 @@
 		createTestRepo(testDataSeed, testDataLength);
 
 		// Repack to create initial packfile. Make a copy of it
-		PackFile pf = repackAndCheck(5, null, null, null);
-		Path packFilePath = pf.getPackFile().toPath();
-		Path packFileBasePath = packFilePath.resolveSibling(
-				packFilePath.getFileName().toString().replaceAll(".pack", ""));
-		AnyObjectId chk1 = pf.getPackChecksum();
-		String name = pf.getPackName();
-		Long length = Long.valueOf(pf.getPackFile().length());
+		Pack p = repackAndCheck(5, null, null, null);
+		Path packFilePath = p.getPackFile().toPath();
+		Path fn = packFilePath.getFileName();
+		assertNotNull(fn);
+		String packFileName = fn.toString();
+		Path packFileBasePath = packFilePath
+				.resolveSibling(packFileName.replaceAll(".pack", ""));
+		AnyObjectId chk1 = p.getPackChecksum();
+		String name = p.getPackName();
+		Long length = Long.valueOf(p.getPackFile().length());
 		copyPack(packFileBasePath, "", ".copy1");
 
 		// Repack to create second packfile. Make a copy of it
@@ -276,10 +280,10 @@
 				Paths.get(base + ".pack" + dstSuffix));
 	}
 
-	private PackFile repackAndCheck(int compressionLevel, String oldName,
+	private Pack repackAndCheck(int compressionLevel, String oldName,
 			Long oldLength, AnyObjectId oldChkSum)
 			throws IOException, ParseException {
-		PackFile p = getSinglePack(gc(compressionLevel));
+		Pack p = getSinglePack(gc(compressionLevel));
 		File pf = p.getPackFile();
 		// The following two assumptions should not cause the test to fail. If
 		// on a certain platform we get packfiles (containing the same git
@@ -294,14 +298,14 @@
 		return p;
 	}
 
-	private PackFile getSinglePack(Collection<PackFile> packs) {
-		Iterator<PackFile> pIt = packs.iterator();
-		PackFile p = pIt.next();
+	private Pack getSinglePack(Collection<Pack> packs) {
+		Iterator<Pack> pIt = packs.iterator();
+		Pack p = pIt.next();
 		assertFalse(pIt.hasNext());
 		return p;
 	}
 
-	private Collection<PackFile> gc(int compressionLevel)
+	private Collection<Pack> gc(int compressionLevel)
 			throws IOException, ParseException {
 		GC gc = new GC(db);
 		PackConfig pc = new PackConfig(db.getConfig());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileTest.java
index 97a86e2..619cfca 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileTest.java
@@ -1,5 +1,6 @@
 /*
- * Copyright (C) 2010, Google Inc. and others
+ * Copyright (c) 2021 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -10,365 +11,159 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
-import static org.junit.Assert.assertArrayEquals;
 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.assertThrows;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.security.MessageDigest;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.zip.Deflater;
-
-import org.eclipse.jgit.errors.LargeObjectException;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.internal.storage.pack.DeltaEncoder;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
-import org.eclipse.jgit.junit.JGitTestUtil;
-import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.junit.TestRng;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectStream;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevBlob;
-import org.eclipse.jgit.storage.file.WindowCacheConfig;
-import org.eclipse.jgit.transport.PackParser;
-import org.eclipse.jgit.transport.PackedObjectInfo;
-import org.eclipse.jgit.util.IO;
-import org.eclipse.jgit.util.NB;
-import org.eclipse.jgit.util.TemporaryBuffer;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
-public class PackFileTest extends LocalDiskRepositoryTestCase {
-	private int streamThreshold = 16 * 1024;
+public class PackFileTest {
+	private static final ObjectId TEST_OID = ObjectId
+			.fromString("0123456789012345678901234567890123456789");
 
-	private TestRng rng;
+	private static final String TEST_ID = TEST_OID.name();
 
-	private FileRepository repo;
+	private static final String PREFIX = "pack-";
 
-	private TestRepository<Repository> tr;
+	private static final String OLD_PREFIX = "old-";
 
-	private WindowCursor wc;
+	private static final String OLD_PACK = PREFIX + TEST_ID + "." + OLD_PREFIX
+			+ PackExt.PACK.getExtension();
 
-	private TestRng getRng() {
-		if (rng == null)
-			rng = new TestRng(JGitTestUtil.getName());
-		return rng;
-	}
+	private static final File TEST_PACK_DIR = new File(
+			"/path/to/repo.git/objects/pack");
 
-	@Override
-	@Before
-	public void setUp() throws Exception {
-		super.setUp();
+	private static final File TEST_PRESERVED_DIR = new File(TEST_PACK_DIR,
+			"preserved");
 
-		WindowCacheConfig cfg = new WindowCacheConfig();
-		cfg.setStreamFileThreshold(streamThreshold);
-		cfg.install();
+	private static final PackFile TEST_PACKFILE_NO_EXT = new PackFile(
+			new File(TEST_PACK_DIR, PREFIX + TEST_ID));
 
-		repo = createBareRepository();
-		tr = new TestRepository<>(repo);
-		wc = (WindowCursor) repo.newObjectReader();
-	}
+	@Test
+	public void objectsAreSameFromAnyConstructor() throws Exception {
+		String name = PREFIX + TEST_ID + "." + PackExt.PACK.getExtension();
+		File pack = new File(TEST_PACK_DIR, name);
+		PackFile pf = new PackFile(pack);
+		PackFile pfFromDirAndName = new PackFile(TEST_PACK_DIR, name);
+		assertPackFilesEqual(pf, pfFromDirAndName);
 
-	@Override
-	@After
-	public void tearDown() throws Exception {
-		if (wc != null)
-			wc.close();
-		new WindowCacheConfig().install();
-		super.tearDown();
+		PackFile pfFromOIdAndExt = new PackFile(TEST_PACK_DIR, TEST_OID,
+				PackExt.PACK);
+		assertPackFilesEqual(pf, pfFromOIdAndExt);
+
+		PackFile pfFromIdAndExt = new PackFile(TEST_PACK_DIR, TEST_ID,
+				PackExt.PACK);
+		assertPackFilesEqual(pf, pfFromIdAndExt);
 	}
 
 	@Test
-	public void testWhole_SmallObject() throws Exception {
-		final int type = Constants.OBJ_BLOB;
-		byte[] data = getRng().nextBytes(300);
-		RevBlob id = tr.blob(data);
-		tr.branch("master").commit().add("A", id).create();
-		tr.packAndPrune();
-		assertTrue("has blob", wc.has(id));
-
-		ObjectLoader ol = wc.open(id);
-		assertNotNull("created loader", ol);
-		assertEquals(type, ol.getType());
-		assertEquals(data.length, ol.getSize());
-		assertFalse("is not large", ol.isLarge());
-		assertTrue("same content", Arrays.equals(data, ol.getCachedBytes()));
-
-		try (ObjectStream in = ol.openStream()) {
-			assertNotNull("have stream", in);
-			assertEquals(type, in.getType());
-			assertEquals(data.length, in.getSize());
-			byte[] data2 = new byte[data.length];
-			IO.readFully(in, data2, 0, data.length);
-			assertTrue("same content", Arrays.equals(data2, data));
-			assertEquals("stream at EOF", -1, in.read());
-		}
+	public void idIsSameFromFileWithOrWithoutExt() throws Exception {
+		PackFile packWithExt = new PackFile(new File(TEST_PACK_DIR,
+				PREFIX + TEST_ID + "." + PackExt.PACK.getExtension()));
+		assertEquals(packWithExt.getId(), TEST_PACKFILE_NO_EXT.getId());
 	}
 
 	@Test
-	public void testWhole_LargeObject() throws Exception {
-		final int type = Constants.OBJ_BLOB;
-		byte[] data = getRng().nextBytes(streamThreshold + 5);
-		RevBlob id = tr.blob(data);
-		tr.branch("master").commit().add("A", id).create();
-		tr.packAndPrune();
-		assertTrue("has blob", wc.has(id));
-
-		ObjectLoader ol = wc.open(id);
-		assertNotNull("created loader", ol);
-		assertEquals(type, ol.getType());
-		assertEquals(data.length, ol.getSize());
-		assertTrue("is large", ol.isLarge());
-		try {
-			ol.getCachedBytes();
-			fail("Should have thrown LargeObjectException");
-		} catch (LargeObjectException tooBig) {
-			assertEquals(MessageFormat.format(
-					JGitText.get().largeObjectException, id.name()), tooBig
-					.getMessage());
-		}
-
-		try (ObjectStream in = ol.openStream()) {
-			assertNotNull("have stream", in);
-			assertEquals(type, in.getType());
-			assertEquals(data.length, in.getSize());
-			byte[] data2 = new byte[data.length];
-			IO.readFully(in, data2, 0, data.length);
-			assertTrue("same content", Arrays.equals(data2, data));
-			assertEquals("stream at EOF", -1, in.read());
-		}
+	public void idIsSameFromFileWithOrWithoutPrefix() throws Exception {
+		PackFile packWithoutPrefix = new PackFile(
+				new File(TEST_PACK_DIR, TEST_ID));
+		assertEquals(packWithoutPrefix.getId(), TEST_PACKFILE_NO_EXT.getId());
 	}
 
 	@Test
-	public void testDelta_SmallObjectChain() throws Exception {
-		try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
-			byte[] data0 = new byte[512];
-			Arrays.fill(data0, (byte) 0xf3);
-			ObjectId id0 = fmt.idFor(Constants.OBJ_BLOB, data0);
+	public void canCreatePreservedFromFile() throws Exception {
+		PackFile preserved = new PackFile(
+				new File(TEST_PRESERVED_DIR, OLD_PACK));
+		assertTrue(preserved.getName().contains(OLD_PACK));
+		assertEquals(preserved.getId(), TEST_ID);
+		assertEquals(preserved.getPackExt(), PackExt.PACK);
+	}
 
-			TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(64 * 1024);
-			packHeader(pack, 4);
-			objectHeader(pack, Constants.OBJ_BLOB, data0.length);
-			deflate(pack, data0);
+	@Test
+	public void canCreatePreservedFromDirAndName() throws Exception {
+		PackFile preserved = new PackFile(TEST_PRESERVED_DIR, OLD_PACK);
+		assertTrue(preserved.getName().contains(OLD_PACK));
+		assertEquals(preserved.getId(), TEST_ID);
+		assertEquals(preserved.getPackExt(), PackExt.PACK);
+	}
 
-			byte[] data1 = clone(0x01, data0);
-			byte[] delta1 = delta(data0, data1);
-			ObjectId id1 = fmt.idFor(Constants.OBJ_BLOB, data1);
-			objectHeader(pack, Constants.OBJ_REF_DELTA, delta1.length);
-			id0.copyRawTo(pack);
-			deflate(pack, delta1);
+	@Test
+	public void cannotCreatePreservedNoExtFromNonPreservedNoExt()
+			throws Exception {
+		assertThrows(IllegalArgumentException.class, () -> TEST_PACKFILE_NO_EXT
+				.createPreservedForDirectory(TEST_PRESERVED_DIR));
+	}
 
-			byte[] data2 = clone(0x02, data1);
-			byte[] delta2 = delta(data1, data2);
-			ObjectId id2 = fmt.idFor(Constants.OBJ_BLOB, data2);
-			objectHeader(pack, Constants.OBJ_REF_DELTA, delta2.length);
-			id1.copyRawTo(pack);
-			deflate(pack, delta2);
-
-			byte[] data3 = clone(0x03, data2);
-			byte[] delta3 = delta(data2, data3);
-			ObjectId id3 = fmt.idFor(Constants.OBJ_BLOB, data3);
-			objectHeader(pack, Constants.OBJ_REF_DELTA, delta3.length);
-			id2.copyRawTo(pack);
-			deflate(pack, delta3);
-
-			digest(pack);
-			PackParser ip = index(pack.toByteArray());
-			ip.setAllowThin(true);
-			ip.parse(NullProgressMonitor.INSTANCE);
-
-			assertTrue("has blob", wc.has(id3));
-
-			ObjectLoader ol = wc.open(id3);
-			assertNotNull("created loader", ol);
-			assertEquals(Constants.OBJ_BLOB, ol.getType());
-			assertEquals(data3.length, ol.getSize());
-			assertFalse("is large", ol.isLarge());
-			assertNotNull(ol.getCachedBytes());
-			assertArrayEquals(data3, ol.getCachedBytes());
-
-			try (ObjectStream in = ol.openStream()) {
-				assertNotNull("have stream", in);
-				assertEquals(Constants.OBJ_BLOB, in.getType());
-				assertEquals(data3.length, in.getSize());
-				byte[] act = new byte[data3.length];
-				IO.readFully(in, act, 0, data3.length);
-				assertTrue("same content", Arrays.equals(act, data3));
-				assertEquals("stream at EOF", -1, in.read());
+	@Test
+	public void canCreateAnyExtFromAnyExt() throws Exception {
+		for (PackExt from : PackExt.values()) {
+			PackFile dotFrom = TEST_PACKFILE_NO_EXT.create(from);
+			for (PackExt to : PackExt.values()) {
+				PackFile dotTo = dotFrom.create(to);
+				File expected = new File(TEST_PACK_DIR,
+						PREFIX + TEST_ID + "." + to.getExtension());
+				assertEquals(dotTo.getPackExt(), to);
+				assertEquals(dotFrom.getId(), dotTo.getId());
+				assertEquals(expected.getName(), dotTo.getName());
 			}
 		}
 	}
 
 	@Test
-	public void testDelta_FailsOver2GiB() throws Exception {
-		try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
-			byte[] base = new byte[] { 'a' };
-			ObjectId idA = fmt.idFor(Constants.OBJ_BLOB, base);
-			ObjectId idB = fmt.idFor(Constants.OBJ_BLOB, new byte[] { 'b' });
+	public void canCreatePreservedFromAnyExt() throws Exception {
+		for (PackExt ext : PackExt.values()) {
+			PackFile nonPreserved = TEST_PACKFILE_NO_EXT.create(ext);
+			PackFile preserved = nonPreserved
+					.createPreservedForDirectory(TEST_PRESERVED_DIR);
+			File expected = new File(TEST_PRESERVED_DIR,
+					PREFIX + TEST_ID + "." + OLD_PREFIX + ext.getExtension());
+			assertEquals(preserved.getName(), expected.getName());
+			assertEquals(preserved.getId(), TEST_ID);
+			assertEquals(preserved.getPackExt(), nonPreserved.getPackExt());
+		}
+	}
 
-			PackedObjectInfo a = new PackedObjectInfo(idA);
-			PackedObjectInfo b = new PackedObjectInfo(idB);
-
-			TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(64 * 1024);
-			packHeader(pack, 2);
-			a.setOffset(pack.length());
-			objectHeader(pack, Constants.OBJ_BLOB, base.length);
-			deflate(pack, base);
-
-			ByteArrayOutputStream tmp = new ByteArrayOutputStream();
-			DeltaEncoder de = new DeltaEncoder(tmp, base.length, 3L << 30);
-			de.copy(0, 1);
-			byte[] delta = tmp.toByteArray();
-			b.setOffset(pack.length());
-			objectHeader(pack, Constants.OBJ_REF_DELTA, delta.length);
-			idA.copyRawTo(pack);
-			deflate(pack, delta);
-			byte[] footer = digest(pack);
-
-			File dir = new File(repo.getObjectDatabase().getDirectory(),
-					"pack");
-			File packName = new File(dir, idA.name() + ".pack");
-			File idxName = new File(dir, idA.name() + ".idx");
-
-			try (FileOutputStream f = new FileOutputStream(packName)) {
-				f.write(pack.toByteArray());
-			}
-
-			try (FileOutputStream f = new FileOutputStream(idxName)) {
-				List<PackedObjectInfo> list = new ArrayList<>();
-				list.add(a);
-				list.add(b);
-				Collections.sort(list);
-				new PackIndexWriterV1(f).write(list, footer);
-			}
-
-			PackFile packFile = new PackFile(packName, PackExt.INDEX.getBit());
-			try {
-				packFile.get(wc, b);
-				fail("expected LargeObjectException.ExceedsByteArrayLimit");
-			} catch (LargeObjectException.ExceedsByteArrayLimit bad) {
-				assertNull(bad.getObjectId());
-			} finally {
-				packFile.close();
+	@Test
+	public void canCreateAnyPreservedExtFromAnyPreservedExt() throws Exception {
+		// Preserved PackFiles must have an extension
+		PackFile preserved = new PackFile(TEST_PRESERVED_DIR, OLD_PACK);
+		for (PackExt from : PackExt.values()) {
+			PackFile preservedWithExt = preserved.create(from);
+			for (PackExt to : PackExt.values()) {
+				PackFile preservedNewExt = preservedWithExt.create(to);
+				File expected = new File(TEST_PRESERVED_DIR, PREFIX + TEST_ID
+						+ "." + OLD_PREFIX + to.getExtension());
+				assertEquals(preservedNewExt.getPackExt(), to);
+				assertEquals(preservedWithExt.getId(), preservedNewExt.getId());
+				assertEquals(preservedNewExt.getName(), expected.getName());
 			}
 		}
 	}
 
 	@Test
-	public void testConfigurableStreamFileThreshold() throws Exception {
-		byte[] data = getRng().nextBytes(300);
-		RevBlob id = tr.blob(data);
-		tr.branch("master").commit().add("A", id).create();
-		tr.packAndPrune();
-		assertTrue("has blob", wc.has(id));
-
-		ObjectLoader ol = wc.open(id);
-		try (ObjectStream in = ol.openStream()) {
-			assertTrue(in instanceof ObjectStream.SmallStream);
-			assertEquals(300, in.available());
-		}
-
-		wc.setStreamFileThreshold(299);
-		ol = wc.open(id);
-		try (ObjectStream in = ol.openStream()) {
-			assertTrue(in instanceof ObjectStream.Filter);
-			assertEquals(1, in.available());
+	public void canCreateNonPreservedFromAnyPreservedExt() throws Exception {
+		// Preserved PackFiles must have an extension
+		PackFile preserved = new PackFile(TEST_PRESERVED_DIR, OLD_PACK);
+		for (PackExt ext : PackExt.values()) {
+			PackFile preservedWithExt = preserved.create(ext);
+			PackFile nonPreserved = preservedWithExt
+					.createForDirectory(TEST_PACK_DIR);
+			File expected = new File(TEST_PACK_DIR,
+					PREFIX + TEST_ID + "." + ext.getExtension());
+			assertEquals(nonPreserved.getName(), expected.getName());
+			assertEquals(nonPreserved.getId(), TEST_ID);
+			assertEquals(nonPreserved.getPackExt(),
+					preservedWithExt.getPackExt());
 		}
 	}
 
-	private static byte[] clone(int first, byte[] base) {
-		byte[] r = new byte[base.length];
-		System.arraycopy(base, 1, r, 1, r.length - 1);
-		r[0] = (byte) first;
-		return r;
-	}
-
-	private static byte[] delta(byte[] base, byte[] dest) throws IOException {
-		ByteArrayOutputStream tmp = new ByteArrayOutputStream();
-		DeltaEncoder de = new DeltaEncoder(tmp, base.length, dest.length);
-		de.insert(dest, 0, 1);
-		de.copy(1, base.length - 1);
-		return tmp.toByteArray();
-	}
-
-	private static void packHeader(TemporaryBuffer.Heap pack, int cnt)
-			throws IOException {
-		final byte[] hdr = new byte[8];
-		NB.encodeInt32(hdr, 0, 2);
-		NB.encodeInt32(hdr, 4, cnt);
-		pack.write(Constants.PACK_SIGNATURE);
-		pack.write(hdr, 0, 8);
-	}
-
-	private static void objectHeader(TemporaryBuffer.Heap pack, int type, int sz)
-			throws IOException {
-		byte[] buf = new byte[8];
-		int nextLength = sz >>> 4;
-		buf[0] = (byte) ((nextLength > 0 ? 0x80 : 0x00) | (type << 4) | (sz & 0x0F));
-		sz = nextLength;
-		int n = 1;
-		while (sz > 0) {
-			nextLength >>>= 7;
-			buf[n++] = (byte) ((nextLength > 0 ? 0x80 : 0x00) | (sz & 0x7F));
-			sz = nextLength;
-		}
-		pack.write(buf, 0, n);
-	}
-
-	private static void deflate(TemporaryBuffer.Heap pack, byte[] content)
-			throws IOException {
-		final Deflater deflater = new Deflater();
-		final byte[] buf = new byte[128];
-		deflater.setInput(content, 0, content.length);
-		deflater.finish();
-		do {
-			final int n = deflater.deflate(buf, 0, buf.length);
-			if (n > 0)
-				pack.write(buf, 0, n);
-		} while (!deflater.finished());
-		deflater.end();
-	}
-
-	private static byte[] digest(TemporaryBuffer.Heap buf)
-			throws IOException {
-		MessageDigest md = Constants.newMessageDigest();
-		md.update(buf.toByteArray());
-		byte[] footer = md.digest();
-		buf.write(footer);
-		return footer;
-	}
-
-	private ObjectInserter inserter;
-
-	@After
-	public void release() {
-		if (inserter != null) {
-			inserter.close();
-		}
-	}
-
-	private PackParser index(byte[] raw) throws IOException {
-		if (inserter == null)
-			inserter = repo.newObjectInserter();
-		return inserter.newPackParser(new ByteArrayInputStream(raw));
+	private void assertPackFilesEqual(PackFile p1, PackFile p2) {
+		// for test purposes, considered equal if id, name, and ext are equal
+		assertEquals(p1.getId(), p2.getId());
+		assertEquals(p1.getPackExt(), p2.getPackExt());
+		assertEquals(p1.getName(), p2.getName());
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java
index 8c56480..8504303 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java
@@ -160,7 +160,7 @@
 		}
 
 		assertPacksOnly();
-		List<PackFile> packs = listPacks();
+		List<Pack> packs = listPacks();
 		assertEquals(1, packs.size());
 		assertEquals(3, packs.get(0).getObjectCount());
 
@@ -193,7 +193,7 @@
 		}
 
 		assertPacksOnly();
-		List<PackFile> packs = listPacks();
+		List<Pack> packs = listPacks();
 		assertEquals(2, packs.size());
 		assertEquals(1, packs.get(0).getObjectCount());
 		assertEquals(1, packs.get(1).getObjectCount());
@@ -216,9 +216,9 @@
 		}
 
 		assertPacksOnly();
-		Collection<PackFile> packs = listPacks();
+		Collection<Pack> packs = listPacks();
 		assertEquals(1, packs.size());
-		PackFile p = packs.iterator().next();
+		Pack p = packs.iterator().next();
 		assertEquals(1, p.getObjectCount());
 
 		try (ObjectReader reader = db.newObjectReader()) {
@@ -237,9 +237,9 @@
 		}
 
 		assertPacksOnly();
-		List<PackFile> packs = listPacks();
+		List<Pack> packs = listPacks();
 		assertEquals(1, packs.size());
-		PackFile pack = packs.get(0);
+		Pack pack = packs.get(0);
 		assertEquals(1, pack.getObjectCount());
 
 		String inode = getInode(pack.getPackFile());
@@ -372,7 +372,7 @@
 		}
 
 		assertPacksOnly();
-		List<PackFile> packs = listPacks();
+		List<Pack> packs = listPacks();
 		assertEquals(1, packs.size());
 		assertEquals(2, packs.get(0).getObjectCount());
 
@@ -489,16 +489,16 @@
 		}
 	}
 
-	private List<PackFile> listPacks() throws Exception {
-		List<PackFile> fromOpenDb = listPacks(db);
-		List<PackFile> reopened;
+	private List<Pack> listPacks() throws Exception {
+		List<Pack> fromOpenDb = listPacks(db);
+		List<Pack> reopened;
 		try (FileRepository db2 = new FileRepository(db.getDirectory())) {
 			reopened = listPacks(db2);
 		}
 		assertEquals(fromOpenDb.size(), reopened.size());
 		for (int i = 0 ; i < fromOpenDb.size(); i++) {
-			PackFile a = fromOpenDb.get(i);
-			PackFile b = reopened.get(i);
+			Pack a = fromOpenDb.get(i);
+			Pack b = reopened.get(i);
 			assertEquals(a.getPackName(), b.getPackName());
 			assertEquals(
 					a.getPackFile().getAbsolutePath(), b.getPackFile().getAbsolutePath());
@@ -508,9 +508,9 @@
 		return fromOpenDb;
 	}
 
-	private static List<PackFile> listPacks(FileRepository db) throws Exception {
+	private static List<Pack> listPacks(FileRepository db) throws Exception {
 		return db.getObjectDatabase().getPacks().stream()
-				.sorted(comparing(PackFile::getPackName)).collect(toList());
+				.sorted(comparing(Pack::getPackName)).collect(toList());
 	}
 
 	private PackInserter newInserter() {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java
new file mode 100644
index 0000000..a359654
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2010, Google Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.junit.Assert.assertArrayEquals;
+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.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.zip.Deflater;
+
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.DeltaEncoder;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.junit.TestRng;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectStream;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.storage.file.WindowCacheConfig;
+import org.eclipse.jgit.transport.PackParser;
+import org.eclipse.jgit.transport.PackedObjectInfo;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.NB;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PackTest extends LocalDiskRepositoryTestCase {
+	private int streamThreshold = 16 * 1024;
+
+	private TestRng rng;
+
+	private FileRepository repo;
+
+	private TestRepository<Repository> tr;
+
+	private WindowCursor wc;
+
+	private TestRng getRng() {
+		if (rng == null)
+			rng = new TestRng(JGitTestUtil.getName());
+		return rng;
+	}
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+
+		WindowCacheConfig cfg = new WindowCacheConfig();
+		cfg.setStreamFileThreshold(streamThreshold);
+		cfg.install();
+
+		repo = createBareRepository();
+		tr = new TestRepository<>(repo);
+		wc = (WindowCursor) repo.newObjectReader();
+	}
+
+	@Override
+	@After
+	public void tearDown() throws Exception {
+		if (wc != null)
+			wc.close();
+		new WindowCacheConfig().install();
+		super.tearDown();
+	}
+
+	@Test
+	public void testWhole_SmallObject() throws Exception {
+		final int type = Constants.OBJ_BLOB;
+		byte[] data = getRng().nextBytes(300);
+		RevBlob id = tr.blob(data);
+		tr.branch("master").commit().add("A", id).create();
+		tr.packAndPrune();
+		assertTrue("has blob", wc.has(id));
+
+		ObjectLoader ol = wc.open(id);
+		assertNotNull("created loader", ol);
+		assertEquals(type, ol.getType());
+		assertEquals(data.length, ol.getSize());
+		assertFalse("is not large", ol.isLarge());
+		assertTrue("same content", Arrays.equals(data, ol.getCachedBytes()));
+
+		try (ObjectStream in = ol.openStream()) {
+			assertNotNull("have stream", in);
+			assertEquals(type, in.getType());
+			assertEquals(data.length, in.getSize());
+			byte[] data2 = new byte[data.length];
+			IO.readFully(in, data2, 0, data.length);
+			assertTrue("same content", Arrays.equals(data2, data));
+			assertEquals("stream at EOF", -1, in.read());
+		}
+	}
+
+	@Test
+	public void testWhole_LargeObject() throws Exception {
+		final int type = Constants.OBJ_BLOB;
+		byte[] data = getRng().nextBytes(streamThreshold + 5);
+		RevBlob id = tr.blob(data);
+		tr.branch("master").commit().add("A", id).create();
+		tr.packAndPrune();
+		assertTrue("has blob", wc.has(id));
+
+		ObjectLoader ol = wc.open(id);
+		assertNotNull("created loader", ol);
+		assertEquals(type, ol.getType());
+		assertEquals(data.length, ol.getSize());
+		assertTrue("is large", ol.isLarge());
+		try {
+			ol.getCachedBytes();
+			fail("Should have thrown LargeObjectException");
+		} catch (LargeObjectException tooBig) {
+			assertEquals(MessageFormat.format(
+					JGitText.get().largeObjectException, id.name()), tooBig
+					.getMessage());
+		}
+
+		try (ObjectStream in = ol.openStream()) {
+			assertNotNull("have stream", in);
+			assertEquals(type, in.getType());
+			assertEquals(data.length, in.getSize());
+			byte[] data2 = new byte[data.length];
+			IO.readFully(in, data2, 0, data.length);
+			assertTrue("same content", Arrays.equals(data2, data));
+			assertEquals("stream at EOF", -1, in.read());
+		}
+	}
+
+	@Test
+	public void testDelta_SmallObjectChain() throws Exception {
+		try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
+			byte[] data0 = new byte[512];
+			Arrays.fill(data0, (byte) 0xf3);
+			ObjectId id0 = fmt.idFor(Constants.OBJ_BLOB, data0);
+
+			TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(64 * 1024);
+			packHeader(pack, 4);
+			objectHeader(pack, Constants.OBJ_BLOB, data0.length);
+			deflate(pack, data0);
+
+			byte[] data1 = clone(0x01, data0);
+			byte[] delta1 = delta(data0, data1);
+			ObjectId id1 = fmt.idFor(Constants.OBJ_BLOB, data1);
+			objectHeader(pack, Constants.OBJ_REF_DELTA, delta1.length);
+			id0.copyRawTo(pack);
+			deflate(pack, delta1);
+
+			byte[] data2 = clone(0x02, data1);
+			byte[] delta2 = delta(data1, data2);
+			ObjectId id2 = fmt.idFor(Constants.OBJ_BLOB, data2);
+			objectHeader(pack, Constants.OBJ_REF_DELTA, delta2.length);
+			id1.copyRawTo(pack);
+			deflate(pack, delta2);
+
+			byte[] data3 = clone(0x03, data2);
+			byte[] delta3 = delta(data2, data3);
+			ObjectId id3 = fmt.idFor(Constants.OBJ_BLOB, data3);
+			objectHeader(pack, Constants.OBJ_REF_DELTA, delta3.length);
+			id2.copyRawTo(pack);
+			deflate(pack, delta3);
+
+			digest(pack);
+			PackParser ip = index(pack.toByteArray());
+			ip.setAllowThin(true);
+			ip.parse(NullProgressMonitor.INSTANCE);
+
+			assertTrue("has blob", wc.has(id3));
+
+			ObjectLoader ol = wc.open(id3);
+			assertNotNull("created loader", ol);
+			assertEquals(Constants.OBJ_BLOB, ol.getType());
+			assertEquals(data3.length, ol.getSize());
+			assertFalse("is large", ol.isLarge());
+			assertNotNull(ol.getCachedBytes());
+			assertArrayEquals(data3, ol.getCachedBytes());
+
+			try (ObjectStream in = ol.openStream()) {
+				assertNotNull("have stream", in);
+				assertEquals(Constants.OBJ_BLOB, in.getType());
+				assertEquals(data3.length, in.getSize());
+				byte[] act = new byte[data3.length];
+				IO.readFully(in, act, 0, data3.length);
+				assertTrue("same content", Arrays.equals(act, data3));
+				assertEquals("stream at EOF", -1, in.read());
+			}
+		}
+	}
+
+	@Test
+	public void testDelta_FailsOver2GiB() throws Exception {
+		try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
+			byte[] base = new byte[] { 'a' };
+			ObjectId idA = fmt.idFor(Constants.OBJ_BLOB, base);
+			ObjectId idB = fmt.idFor(Constants.OBJ_BLOB, new byte[] { 'b' });
+
+			PackedObjectInfo a = new PackedObjectInfo(idA);
+			PackedObjectInfo b = new PackedObjectInfo(idB);
+
+			TemporaryBuffer.Heap packContents = new TemporaryBuffer.Heap(64 * 1024);
+			packHeader(packContents, 2);
+			a.setOffset(packContents.length());
+			objectHeader(packContents, Constants.OBJ_BLOB, base.length);
+			deflate(packContents, base);
+
+			ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+			DeltaEncoder de = new DeltaEncoder(tmp, base.length, 3L << 30);
+			de.copy(0, 1);
+			byte[] delta = tmp.toByteArray();
+			b.setOffset(packContents.length());
+			objectHeader(packContents, Constants.OBJ_REF_DELTA, delta.length);
+			idA.copyRawTo(packContents);
+			deflate(packContents, delta);
+			byte[] footer = digest(packContents);
+
+			File dir = new File(repo.getObjectDatabase().getDirectory(),
+					"pack");
+			PackFile packName = new PackFile(dir, idA.name() + ".pack");
+			PackFile idxName = packName.create(PackExt.INDEX);
+
+			try (FileOutputStream f = new FileOutputStream(packName)) {
+				f.write(packContents.toByteArray());
+			}
+
+			try (FileOutputStream f = new FileOutputStream(idxName)) {
+				List<PackedObjectInfo> list = new ArrayList<>();
+				list.add(a);
+				list.add(b);
+				Collections.sort(list);
+				new PackIndexWriterV1(f).write(list, footer);
+			}
+
+			Pack pack = new Pack(packName, null);
+			try {
+				pack.get(wc, b);
+				fail("expected LargeObjectException.ExceedsByteArrayLimit");
+			} catch (LargeObjectException.ExceedsByteArrayLimit bad) {
+				assertNull(bad.getObjectId());
+			} finally {
+				pack.close();
+			}
+		}
+	}
+
+	@Test
+	public void testConfigurableStreamFileThreshold() throws Exception {
+		byte[] data = getRng().nextBytes(300);
+		RevBlob id = tr.blob(data);
+		tr.branch("master").commit().add("A", id).create();
+		tr.packAndPrune();
+		assertTrue("has blob", wc.has(id));
+
+		ObjectLoader ol = wc.open(id);
+		try (ObjectStream in = ol.openStream()) {
+			assertTrue(in instanceof ObjectStream.SmallStream);
+			assertEquals(300, in.available());
+		}
+
+		wc.setStreamFileThreshold(299);
+		ol = wc.open(id);
+		try (ObjectStream in = ol.openStream()) {
+			assertTrue(in instanceof ObjectStream.Filter);
+			assertEquals(1, in.available());
+		}
+	}
+
+	private static byte[] clone(int first, byte[] base) {
+		byte[] r = new byte[base.length];
+		System.arraycopy(base, 1, r, 1, r.length - 1);
+		r[0] = (byte) first;
+		return r;
+	}
+
+	private static byte[] delta(byte[] base, byte[] dest) throws IOException {
+		ByteArrayOutputStream tmp = new ByteArrayOutputStream();
+		DeltaEncoder de = new DeltaEncoder(tmp, base.length, dest.length);
+		de.insert(dest, 0, 1);
+		de.copy(1, base.length - 1);
+		return tmp.toByteArray();
+	}
+
+	private static void packHeader(TemporaryBuffer.Heap pack, int cnt)
+			throws IOException {
+		final byte[] hdr = new byte[8];
+		NB.encodeInt32(hdr, 0, 2);
+		NB.encodeInt32(hdr, 4, cnt);
+		pack.write(Constants.PACK_SIGNATURE);
+		pack.write(hdr, 0, 8);
+	}
+
+	private static void objectHeader(TemporaryBuffer.Heap pack, int type, int sz)
+			throws IOException {
+		byte[] buf = new byte[8];
+		int nextLength = sz >>> 4;
+		buf[0] = (byte) ((nextLength > 0 ? 0x80 : 0x00) | (type << 4) | (sz & 0x0F));
+		sz = nextLength;
+		int n = 1;
+		while (sz > 0) {
+			nextLength >>>= 7;
+			buf[n++] = (byte) ((nextLength > 0 ? 0x80 : 0x00) | (sz & 0x7F));
+			sz = nextLength;
+		}
+		pack.write(buf, 0, n);
+	}
+
+	private static void deflate(TemporaryBuffer.Heap pack, byte[] content)
+			throws IOException {
+		final Deflater deflater = new Deflater();
+		final byte[] buf = new byte[128];
+		deflater.setInput(content, 0, content.length);
+		deflater.finish();
+		do {
+			final int n = deflater.deflate(buf, 0, buf.length);
+			if (n > 0)
+				pack.write(buf, 0, n);
+		} while (!deflater.finished());
+		deflater.end();
+	}
+
+	private static byte[] digest(TemporaryBuffer.Heap buf)
+			throws IOException {
+		MessageDigest md = Constants.newMessageDigest();
+		md.update(buf.toByteArray());
+		byte[] footer = md.digest();
+		buf.write(footer);
+		return footer;
+	}
+
+	private ObjectInserter inserter;
+
+	@After
+	public void release() {
+		if (inserter != null) {
+			inserter.close();
+		}
+	}
+
+	private PackParser index(byte[] raw) throws IOException {
+		if (inserter == null)
+			inserter = repo.newObjectInserter();
+		return inserter.newPackParser(new ByteArrayInputStream(raw));
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
index c90310e..e422ab9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
@@ -34,6 +34,7 @@
 
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.TestRepository;
@@ -72,7 +73,7 @@
 
 	private ByteArrayOutputStream os;
 
-	private PackFile pack;
+	private Pack pack;
 
 	private ObjectInserter inserter;
 
@@ -305,9 +306,9 @@
 	@Test
 	public void testWritePack2DeltasCRC32Copy() throws IOException {
 		final File packDir = db.getObjectDatabase().getPackDirectory();
-		final File crc32Pack = new File(packDir,
+		final PackFile crc32Pack = new PackFile(packDir,
 				"pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.pack");
-		final File crc32Idx = new File(packDir,
+		final PackFile crc32Idx = new PackFile(packDir,
 				"pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.idx");
 		copyFile(JGitTestUtil.getTestResourceFile(
 				"pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.idxV2"),
@@ -471,10 +472,8 @@
 		config.setIndexVersion(2);
 		writeVerifyPack4(false);
 
-		File packFile = pack.getPackFile();
-		String name = packFile.getName();
-		String base = name.substring(0, name.lastIndexOf('.'));
-		File indexFile = new File(packFile.getParentFile(), base + ".idx");
+		PackFile packFile = pack.getPackFile();
+		PackFile indexFile = packFile.create(PackExt.INDEX);
 
 		// Validate that IndexPack came up with the right CRC32 value.
 		final PackIndex idx1 = PackIndex.open(indexFile);
@@ -685,14 +684,14 @@
 			ObjectWalk ow = walk.toObjectWalkWithSameObjects();
 
 			pw.preparePack(NullProgressMonitor.INSTANCE, ow, want, have, NONE);
-			String id = pw.computeName().getName();
 			File packdir = repo.getObjectDatabase().getPackDirectory();
-			File packFile = new File(packdir, "pack-" + id + ".pack");
+			PackFile packFile = new PackFile(packdir, pw.computeName(),
+					PackExt.PACK);
 			try (FileOutputStream packOS = new FileOutputStream(packFile)) {
 				pw.writePack(NullProgressMonitor.INSTANCE,
 						NullProgressMonitor.INSTANCE, packOS);
 			}
-			File idxFile = new File(packdir, "pack-" + id + ".idx");
+			PackFile idxFile = packFile.create(PackExt.INDEX);
 			try (FileOutputStream idxOS = new FileOutputStream(idxFile)) {
 				pw.writeIndex(idxOS);
 			}
@@ -840,7 +839,7 @@
 		p.setAllowThin(thin);
 		p.setIndexVersion(2);
 		p.parse(NullProgressMonitor.INSTANCE);
-		pack = p.getPackFile();
+		pack = p.getPack();
 		assertNotNull("have PackFile after parsing", pack);
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
index 97ef599..38c545e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
@@ -30,8 +30,10 @@
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -353,6 +355,24 @@
 	}
 
 	@Test
+	public void testGetRefs_ExcludingPrefixes() throws IOException {
+		writeLooseRef("refs/heads/A", A);
+		writeLooseRef("refs/heads/B", B);
+		writeLooseRef("refs/tags/tag", A);
+		writeLooseRef("refs/something/something", B);
+		writeLooseRef("refs/aaa/aaa", A);
+
+		Set<String> toExclude = new HashSet<>();
+		toExclude.add("refs/aaa/");
+		toExclude.add("refs/heads/");
+		List<Ref> refs = refdir.getRefsByPrefixWithExclusions(RefDatabase.ALL, toExclude);
+
+		assertEquals(2, refs.size());
+		assertTrue(refs.contains(refdir.exactRef("refs/tags/tag")));
+		assertTrue(refs.contains(refdir.exactRef("refs/something/something")));
+	}
+
+	@Test
 	public void testFirstExactRef_IgnoresGarbageRef() throws IOException {
 		writeLooseRef("refs/heads/A", A);
 		write(new File(diskRepo.getDirectory(), "refs/heads/bad"), "FAIL\n");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0004_PackReaderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0004_PackReaderTest.java
index ee4c9b1..8f1371e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0004_PackReaderTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0004_PackReaderTest.java
@@ -32,8 +32,8 @@
 		final ObjectId id;
 		final ObjectLoader or;
 
-		PackFile pr = null;
-		for (PackFile p : db.getObjectDatabase().getPacks()) {
+		Pack pr = null;
+		for (Pack p : db.getObjectDatabase().getPacks()) {
 			if (PACK_NAME.equals(p.getPackName())) {
 				pr = p;
 				break;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/MergedReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/MergedReftableTest.java
index 0a03fc3..9aea3b4b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/MergedReftableTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/MergedReftableTest.java
@@ -138,6 +138,118 @@
 	}
 
 	@Test
+	public void twoTableSeekPastWithRefCursor() throws IOException {
+		List<Ref> delta1 = Arrays.asList(
+				ref("refs/heads/apple", 1),
+				ref("refs/heads/master", 2));
+		List<Ref> delta2 = Arrays.asList(
+				ref("refs/heads/banana", 3),
+				ref("refs/heads/zzlast", 4));
+
+		MergedReftable mr = merge(write(delta1), write(delta2));
+		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
+			assertTrue(rc.next());
+			assertEquals("refs/heads/apple", rc.getRef().getName());
+			assertEquals(id(1), rc.getRef().getObjectId());
+
+			rc.seekPastPrefix("refs/heads/banana/");
+
+			assertTrue(rc.next());
+			assertEquals("refs/heads/master", rc.getRef().getName());
+			assertEquals(id(2), rc.getRef().getObjectId());
+
+			assertTrue(rc.next());
+			assertEquals("refs/heads/zzlast", rc.getRef().getName());
+			assertEquals(id(4), rc.getRef().getObjectId());
+
+			assertEquals(1, rc.getRef().getUpdateIndex());
+		}
+	}
+
+	@Test
+	public void oneTableSeekPastWithRefCursor() throws IOException {
+		List<Ref> delta1 = Arrays.asList(
+				ref("refs/heads/apple", 1),
+				ref("refs/heads/master", 2));
+
+		MergedReftable mr = merge(write(delta1));
+		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
+			rc.seekPastPrefix("refs/heads/apple");
+
+			assertTrue(rc.next());
+			assertEquals("refs/heads/master", rc.getRef().getName());
+			assertEquals(id(2), rc.getRef().getObjectId());
+
+			assertEquals(1, rc.getRef().getUpdateIndex());
+		}
+	}
+
+	@Test
+	public void seekPastToNonExistentPrefixToTheMiddle() throws IOException {
+		List<Ref> delta1 = Arrays.asList(
+				ref("refs/heads/apple", 1),
+				ref("refs/heads/master", 2));
+		List<Ref> delta2 = Arrays.asList(
+				ref("refs/heads/banana", 3),
+				ref("refs/heads/zzlast", 4));
+
+		MergedReftable mr = merge(write(delta1), write(delta2));
+		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
+			rc.seekPastPrefix("refs/heads/x");
+
+			assertTrue(rc.next());
+			assertEquals("refs/heads/zzlast", rc.getRef().getName());
+			assertEquals(id(4), rc.getRef().getObjectId());
+
+			assertEquals(1, rc.getRef().getUpdateIndex());
+		}
+	}
+
+	@Test
+	public void seekPastToNonExistentPrefixToTheEnd() throws IOException {
+		List<Ref> delta1 = Arrays.asList(
+				ref("refs/heads/apple", 1),
+				ref("refs/heads/master", 2));
+		List<Ref> delta2 = Arrays.asList(
+				ref("refs/heads/banana", 3),
+				ref("refs/heads/zzlast", 4));
+
+		MergedReftable mr = merge(write(delta1), write(delta2));
+		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
+			rc.seekPastPrefix("refs/heads/zzz");
+			assertFalse(rc.next());
+		}
+	}
+
+	@Test
+	public void seekPastManyTimes() throws IOException {
+		List<Ref> delta1 = Arrays.asList(
+				ref("refs/heads/apple", 1),
+				ref("refs/heads/master", 2));
+		List<Ref> delta2 = Arrays.asList(
+				ref("refs/heads/banana", 3),
+				ref("refs/heads/zzlast", 4));
+
+		MergedReftable mr = merge(write(delta1), write(delta2));
+		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
+			rc.seekPastPrefix("refs/heads/apple");
+			rc.seekPastPrefix("refs/heads/banana");
+			rc.seekPastPrefix("refs/heads/master");
+			rc.seekPastPrefix("refs/heads/zzlast");
+			assertFalse(rc.next());
+		}
+	}
+
+	@Test
+	public void seekPastOnEmptyTable() throws IOException {
+		MergedReftable mr = merge(write(), write());
+		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
+			rc.seekPastPrefix("refs/");
+			assertFalse(rc.next());
+		}
+	}
+
+	@Test
 	public void twoTableById() throws IOException {
 		List<Ref> delta1 = Arrays.asList(
 				ref("refs/heads/apple", 1),
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java
index 009914b..0d739b9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java
@@ -10,6 +10,7 @@
 
 package org.eclipse.jgit.internal.storage.reftable;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
@@ -49,8 +50,16 @@
 import org.junit.Test;
 
 public class ReftableTest {
+	private static final byte[] LAST_UTF8_CHAR = new byte[] {
+			(byte)0x10,
+			(byte)0xFF,
+			(byte)0xFF};
+
 	private static final String MASTER = "refs/heads/master";
 	private static final String NEXT = "refs/heads/next";
+	private static final String AFTER_NEXT = "refs/heads/nextnext";
+	private static final String LAST = "refs/heads/nextnextnext";
+	private static final String NOT_REF_HEADS = "refs/zzz/zzz";
 	private static final String V1_0 = "refs/tags/v1.0";
 
 	private Stats stats;
@@ -396,6 +405,135 @@
 	}
 
 	@Test
+	public void seekPastRefWithRefCursor() throws IOException {
+		Ref exp = ref(MASTER, 1);
+		Ref next = ref(NEXT, 2);
+		Ref afterNext = ref(AFTER_NEXT, 3);
+		Ref afterNextNext = ref(LAST, 4);
+		ReftableReader t = read(write(exp, next, afterNext, afterNextNext));
+		try (RefCursor rc = t.seekRefsWithPrefix("")) {
+			assertTrue(rc.next());
+			assertEquals(MASTER, rc.getRef().getName());
+
+			rc.seekPastPrefix("refs/heads/next/");
+
+			assertTrue(rc.next());
+			assertEquals(AFTER_NEXT, rc.getRef().getName());
+			assertTrue(rc.next());
+			assertEquals(LAST, rc.getRef().getName());
+
+			assertFalse(rc.next());
+		}
+	}
+
+	@Test
+	public void seekPastToNonExistentPrefixToTheMiddle() throws IOException {
+		Ref exp = ref(MASTER, 1);
+		Ref next = ref(NEXT, 2);
+		Ref afterNext = ref(AFTER_NEXT, 3);
+		Ref afterNextNext = ref(LAST, 4);
+		ReftableReader t = read(write(exp, next, afterNext, afterNextNext));
+		try (RefCursor rc = t.seekRefsWithPrefix("")) {
+			rc.seekPastPrefix("refs/heads/master_non_existent");
+
+			assertTrue(rc.next());
+			assertEquals(NEXT, rc.getRef().getName());
+
+			assertTrue(rc.next());
+			assertEquals(AFTER_NEXT, rc.getRef().getName());
+
+			assertTrue(rc.next());
+			assertEquals(LAST, rc.getRef().getName());
+
+			assertFalse(rc.next());
+		}
+	}
+
+	@Test
+	public void seekPastToNonExistentPrefixToTheEnd() throws IOException {
+		Ref exp = ref(MASTER, 1);
+		Ref next = ref(NEXT, 2);
+		Ref afterNext = ref(AFTER_NEXT, 3);
+		Ref afterNextNext = ref(LAST, 4);
+		ReftableReader t = read(write(exp, next, afterNext, afterNextNext));
+		try (RefCursor rc = t.seekRefsWithPrefix("")) {
+			rc.seekPastPrefix("refs/heads/nextnon_existent_end");
+			assertFalse(rc.next());
+		}
+	}
+
+	@Test
+	public void seekPastWithSeekRefsWithPrefix() throws IOException {
+		Ref exp = ref(MASTER, 1);
+		Ref next = ref(NEXT, 2);
+		Ref afterNext = ref(AFTER_NEXT, 3);
+		Ref afterNextNext = ref(LAST, 4);
+		Ref notRefsHeads = ref(NOT_REF_HEADS, 5);
+		ReftableReader t = read(write(exp, next, afterNext, afterNextNext, notRefsHeads));
+		try (RefCursor rc = t.seekRefsWithPrefix("refs/heads/")) {
+			rc.seekPastPrefix("refs/heads/next/");
+			assertTrue(rc.next());
+			assertEquals(AFTER_NEXT, rc.getRef().getName());
+			assertTrue(rc.next());
+			assertEquals(LAST, rc.getRef().getName());
+
+			// NOT_REF_HEADS is next, but it's omitted because of
+			// seekRefsWithPrefix("refs/heads/").
+			assertFalse(rc.next());
+		}
+	}
+
+	@Test
+	public void seekPastWithLotsOfRefs() throws IOException {
+		Ref[] refs = new Ref[500];
+		for (int i = 1; i <= 500; i++) {
+			refs[i - 1] = ref(String.format("refs/%d", Integer.valueOf(i)), i);
+		}
+		ReftableReader t = read(write(refs));
+		try (RefCursor rc = t.allRefs()) {
+			rc.seekPastPrefix("refs/3");
+			assertTrue(rc.next());
+			assertEquals("refs/4", rc.getRef().getName());
+			assertTrue(rc.next());
+			assertEquals("refs/40", rc.getRef().getName());
+
+			rc.seekPastPrefix("refs/8");
+			assertTrue(rc.next());
+			assertEquals("refs/9", rc.getRef().getName());
+			assertTrue(rc.next());
+			assertEquals("refs/90", rc.getRef().getName());
+			assertTrue(rc.next());
+			assertEquals("refs/91", rc.getRef().getName());
+		}
+	}
+
+	@Test
+	public void seekPastManyTimes() throws IOException {
+		Ref exp = ref(MASTER, 1);
+		Ref next = ref(NEXT, 2);
+		Ref afterNext = ref(AFTER_NEXT, 3);
+		Ref afterNextNext = ref(LAST, 4);
+		ReftableReader t = read(write(exp, next, afterNext, afterNextNext));
+
+		try (RefCursor rc = t.seekRefsWithPrefix("")) {
+			rc.seekPastPrefix("refs/heads/master");
+			rc.seekPastPrefix("refs/heads/next");
+			rc.seekPastPrefix("refs/heads/nextnext");
+			rc.seekPastPrefix("refs/heads/nextnextnext");
+			assertFalse(rc.next());
+		}
+	}
+
+	@Test
+	public void seekPastOnEmptyTable() throws IOException {
+		ReftableReader t = read(write());
+		try (RefCursor rc = t.seekRefsWithPrefix("")) {
+			rc.seekPastPrefix("refs/");
+			assertFalse(rc.next());
+		}
+	}
+
+	@Test
 	public void indexScan() throws IOException {
 		List<Ref> refs = new ArrayList<>();
 		for (int i = 1; i <= 5670; i++) {
@@ -874,6 +1012,14 @@
 	}
 
 	@Test
+	public void byObjectIdSkipPastPrefix() throws IOException {
+		ReftableReader t = read(write());
+		try (RefCursor rc = t.byObjectId(id(2))) {
+			assertThrows(UnsupportedOperationException.class, () -> rc.seekPastPrefix("refs/heads/"));
+		}
+	}
+
+	@Test
 	public void unpeeledDoesNotWrite() {
 		try {
 			write(new ObjectIdRef.Unpeeled(PACKED, MASTER, id(1)));
@@ -884,6 +1030,18 @@
 	}
 
 	@Test
+	public void skipPastRefWithLastUTF8() throws IOException {
+		ReftableReader t = read(write(ref(String.format("refs/heads/%sbla", new String(LAST_UTF8_CHAR
+				, UTF_8)), 1)));
+
+		try (RefCursor rc = t.allRefs()) {
+			rc.seekPastPrefix("refs/heads/");
+			assertFalse(rc.next());
+		}
+	}
+
+
+	@Test
 	public void nameTooLongDoesNotWrite() throws IOException {
 		try {
 			ReftableConfig cfg = new ReftableConfig();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java
deleted file mode 100644
index 86016d8..0000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2016 Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.Constants.MASTER;
-import static org.eclipse.jgit.lib.Constants.ORIG_HEAD;
-import static org.eclipse.jgit.lib.Constants.R_HEADS;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
-import org.junit.Before;
-import org.junit.Test;
-
-public class LocalDiskRefTreeDatabaseTest extends LocalDiskRepositoryTestCase {
-	private FileRepository repo;
-	private RefTreeDatabase refdb;
-	private RefDatabase bootstrap;
-
-	private TestRepository<FileRepository> testRepo;
-	private RevCommit A;
-	private RevCommit B;
-
-	@Override
-	@Before
-	public void setUp() throws Exception {
-		super.setUp();
-		FileRepository init = createWorkRepository();
-		FileBasedConfig cfg = init.getConfig();
-		cfg.setInt("core", null, "repositoryformatversion", 1);
-		cfg.setString("extensions", null, "refStorage", "reftree");
-		cfg.save();
-
-		repo = (FileRepository) new FileRepositoryBuilder()
-				.setGitDir(init.getDirectory())
-				.build();
-		refdb = (RefTreeDatabase) repo.getRefDatabase();
-		bootstrap = refdb.getBootstrap();
-		addRepoToClose(repo);
-
-		RefUpdate head = refdb.newUpdate(HEAD, true);
-		head.link(R_HEADS + MASTER);
-
-		testRepo = new TestRepository<>(init);
-		A = testRepo.commit().create();
-		B = testRepo.commit(testRepo.getRevWalk().parseCommit(A));
-	}
-
-	@Test
-	public void testHeadOrigHead() throws IOException {
-		RefUpdate master = refdb.newUpdate(HEAD, false);
-		master.setExpectedOldObjectId(ObjectId.zeroId());
-		master.setNewObjectId(A);
-		assertEquals(RefUpdate.Result.NEW, master.update());
-		assertEquals(A, refdb.exactRef(HEAD).getObjectId());
-
-		RefUpdate orig = refdb.newUpdate(ORIG_HEAD, true);
-		orig.setNewObjectId(B);
-		assertEquals(RefUpdate.Result.NEW, orig.update());
-
-		File origFile = new File(repo.getDirectory(), ORIG_HEAD);
-		assertEquals(B.name() + '\n', read(origFile));
-		assertEquals(B, bootstrap.exactRef(ORIG_HEAD).getObjectId());
-		assertEquals(B, refdb.exactRef(ORIG_HEAD).getObjectId());
-		assertFalse(refdb.getRefs(ALL).containsKey(ORIG_HEAD));
-
-		List<Ref> addl = refdb.getAdditionalRefs();
-		assertEquals(2, addl.size());
-		assertEquals(ORIG_HEAD, addl.get(1).getName());
-		assertEquals(B, addl.get(1).getObjectId());
-	}
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java
deleted file mode 100644
index ecee5e5..0000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java
+++ /dev/null
@@ -1,689 +0,0 @@
-/*
- * Copyright (C) 2010, 2013, 2016 Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.Constants.ORIG_HEAD;
-import static org.eclipse.jgit.lib.Constants.R_HEADS;
-import static org.eclipse.jgit.lib.Constants.R_TAGS;
-import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
-import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.io.IOException;
-import java.text.MessageFormat;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.junit.Before;
-import org.junit.Test;
-
-public class RefTreeDatabaseTest {
-	private InMemRefTreeRepo repo;
-	private RefTreeDatabase refdb;
-	private RefDatabase bootstrap;
-
-	private TestRepository<InMemRefTreeRepo> testRepo;
-	private RevCommit A;
-	private RevCommit B;
-	private RevTag v1_0;
-
-	@Before
-	public void setUp() throws Exception {
-		repo = new InMemRefTreeRepo(new DfsRepositoryDescription("test"));
-		bootstrap = refdb.getBootstrap();
-
-		testRepo = new TestRepository<>(repo);
-		A = testRepo.commit().create();
-		B = testRepo.commit(testRepo.getRevWalk().parseCommit(A));
-		v1_0 = testRepo.tag("v1_0", B);
-		testRepo.getRevWalk().parseBody(v1_0);
-	}
-
-	@Test
-	public void testSupportsAtomic() {
-		assertTrue(refdb.performsAtomicTransactions());
-	}
-
-	@Test
-	public void testGetRefs_EmptyDatabase() throws IOException {
-		assertTrue("no references", refdb.getRefs(ALL).isEmpty());
-		assertTrue("no references", refdb.getRefs(R_HEADS).isEmpty());
-		assertTrue("no references", refdb.getRefs(R_TAGS).isEmpty());
-		assertTrue("no references", refdb.getAdditionalRefs().isEmpty());
-	}
-
-	@Test
-	public void testGetAdditionalRefs() throws IOException {
-		update("refs/heads/master", A);
-
-		List<Ref> addl = refdb.getAdditionalRefs();
-		assertEquals(1, addl.size());
-		assertEquals("refs/txn/committed", addl.get(0).getName());
-		assertEquals(getTxnCommitted(), addl.get(0).getObjectId());
-	}
-
-	@Test
-	public void testGetRefs_HeadOnOneBranch() throws IOException {
-		symref(HEAD, "refs/heads/master");
-		update("refs/heads/master", A);
-
-		Map<String, Ref> all = refdb.getRefs(ALL);
-		assertEquals(2, all.size());
-		assertTrue("has HEAD", all.containsKey(HEAD));
-		assertTrue("has master", all.containsKey("refs/heads/master"));
-
-		Ref head = all.get(HEAD);
-		Ref master = all.get("refs/heads/master");
-
-		assertEquals(HEAD, head.getName());
-		assertTrue(head.isSymbolic());
-		assertSame(LOOSE, head.getStorage());
-		assertSame("uses same ref as target", master, head.getTarget());
-
-		assertEquals("refs/heads/master", master.getName());
-		assertFalse(master.isSymbolic());
-		assertSame(PACKED, master.getStorage());
-		assertEquals(A, master.getObjectId());
-	}
-
-	@Test
-	public void testGetRefs_DetachedHead() throws IOException {
-		update(HEAD, A);
-
-		Map<String, Ref> all = refdb.getRefs(ALL);
-		assertEquals(1, all.size());
-		assertTrue("has HEAD", all.containsKey(HEAD));
-
-		Ref head = all.get(HEAD);
-		assertEquals(HEAD, head.getName());
-		assertFalse(head.isSymbolic());
-		assertSame(PACKED, head.getStorage());
-		assertEquals(A, head.getObjectId());
-	}
-
-	@Test
-	public void testGetRefs_DeeplyNestedBranch() throws IOException {
-		String name = "refs/heads/a/b/c/d/e/f/g/h/i/j/k";
-		update(name, A);
-
-		Map<String, Ref> all = refdb.getRefs(ALL);
-		assertEquals(1, all.size());
-
-		Ref r = all.get(name);
-		assertEquals(name, r.getName());
-		assertFalse(r.isSymbolic());
-		assertSame(PACKED, r.getStorage());
-		assertEquals(A, r.getObjectId());
-	}
-
-	@Test
-	public void testGetRefs_HeadBranchNotBorn() throws IOException {
-		update("refs/heads/A", A);
-		update("refs/heads/B", B);
-
-		Map<String, Ref> all = refdb.getRefs(ALL);
-		assertEquals(2, all.size());
-		assertFalse("no HEAD", all.containsKey(HEAD));
-
-		Ref a = all.get("refs/heads/A");
-		Ref b = all.get("refs/heads/B");
-
-		assertEquals(A, a.getObjectId());
-		assertEquals(B, b.getObjectId());
-
-		assertEquals("refs/heads/A", a.getName());
-		assertEquals("refs/heads/B", b.getName());
-	}
-
-	@Test
-	public void testGetRefs_HeadsOnly() throws IOException {
-		update("refs/heads/A", A);
-		update("refs/heads/B", B);
-		update("refs/tags/v1.0", v1_0);
-
-		Map<String, Ref> heads = refdb.getRefs(R_HEADS);
-		assertEquals(2, heads.size());
-
-		Ref a = heads.get("A");
-		Ref b = heads.get("B");
-
-		assertEquals("refs/heads/A", a.getName());
-		assertEquals("refs/heads/B", b.getName());
-
-		assertEquals(A, a.getObjectId());
-		assertEquals(B, b.getObjectId());
-	}
-
-	@Test
-	public void testGetRefs_TagsOnly() throws IOException {
-		update("refs/heads/A", A);
-		update("refs/heads/B", B);
-		update("refs/tags/v1.0", v1_0);
-
-		Map<String, Ref> tags = refdb.getRefs(R_TAGS);
-		assertEquals(1, tags.size());
-
-		Ref a = tags.get("v1.0");
-		assertEquals("refs/tags/v1.0", a.getName());
-		assertEquals(v1_0, a.getObjectId());
-		assertTrue(a.isPeeled());
-		assertEquals(v1_0.getObject(), a.getPeeledObjectId());
-	}
-
-	@Test
-	public void testGetRefs_HeadsSymref() throws IOException {
-		symref("refs/heads/other", "refs/heads/master");
-		update("refs/heads/master", A);
-
-		Map<String, Ref> heads = refdb.getRefs(R_HEADS);
-		assertEquals(2, heads.size());
-
-		Ref master = heads.get("master");
-		Ref other = heads.get("other");
-
-		assertEquals("refs/heads/master", master.getName());
-		assertEquals(A, master.getObjectId());
-
-		assertEquals("refs/heads/other", other.getName());
-		assertEquals(A, other.getObjectId());
-		assertSame(master, other.getTarget());
-	}
-
-	@Test
-	public void testGetRefs_InvalidPrefixes() throws IOException {
-		update("refs/heads/A", A);
-
-		assertTrue("empty refs/heads", refdb.getRefs("refs/heads").isEmpty());
-		assertTrue("empty objects", refdb.getRefs("objects").isEmpty());
-		assertTrue("empty objects/", refdb.getRefs("objects/").isEmpty());
-	}
-
-	@Test
-	public void testGetRefs_DiscoversNew() throws IOException {
-		update("refs/heads/master", A);
-		Map<String, Ref> orig = refdb.getRefs(ALL);
-
-		update("refs/heads/next", B);
-		Map<String, Ref> next = refdb.getRefs(ALL);
-
-		assertEquals(1, orig.size());
-		assertEquals(2, next.size());
-
-		assertFalse(orig.containsKey("refs/heads/next"));
-		assertTrue(next.containsKey("refs/heads/next"));
-
-		assertEquals(A, next.get("refs/heads/master").getObjectId());
-		assertEquals(B, next.get("refs/heads/next").getObjectId());
-	}
-
-	@Test
-	public void testGetRefs_DiscoversModified() throws IOException {
-		symref(HEAD, "refs/heads/master");
-		update("refs/heads/master", A);
-
-		Map<String, Ref> all = refdb.getRefs(ALL);
-		assertEquals(A, all.get(HEAD).getObjectId());
-
-		update("refs/heads/master", B);
-		all = refdb.getRefs(ALL);
-		assertEquals(B, all.get(HEAD).getObjectId());
-		assertEquals(B, refdb.exactRef(HEAD).getObjectId());
-	}
-
-	@Test
-	public void testGetRefs_CycleInSymbolicRef() throws IOException {
-		symref("refs/1", "refs/2");
-		symref("refs/2", "refs/3");
-		symref("refs/3", "refs/4");
-		symref("refs/4", "refs/5");
-		symref("refs/5", "refs/end");
-		update("refs/end", A);
-
-		Map<String, Ref> all = refdb.getRefs(ALL);
-		Ref r = all.get("refs/1");
-		assertNotNull("has 1", r);
-
-		assertEquals("refs/1", r.getName());
-		assertEquals(A, r.getObjectId());
-		assertTrue(r.isSymbolic());
-
-		r = r.getTarget();
-		assertEquals("refs/2", r.getName());
-		assertEquals(A, r.getObjectId());
-		assertTrue(r.isSymbolic());
-
-		r = r.getTarget();
-		assertEquals("refs/3", r.getName());
-		assertEquals(A, r.getObjectId());
-		assertTrue(r.isSymbolic());
-
-		r = r.getTarget();
-		assertEquals("refs/4", r.getName());
-		assertEquals(A, r.getObjectId());
-		assertTrue(r.isSymbolic());
-
-		r = r.getTarget();
-		assertEquals("refs/5", r.getName());
-		assertEquals(A, r.getObjectId());
-		assertTrue(r.isSymbolic());
-
-		r = r.getTarget();
-		assertEquals("refs/end", r.getName());
-		assertEquals(A, r.getObjectId());
-		assertFalse(r.isSymbolic());
-
-		symref("refs/5", "refs/6");
-		symref("refs/6", "refs/end");
-		all = refdb.getRefs(ALL);
-		assertNull("mising 1 due to cycle", all.get("refs/1"));
-		assertEquals(A, all.get("refs/2").getObjectId());
-		assertEquals(A, all.get("refs/3").getObjectId());
-		assertEquals(A, all.get("refs/4").getObjectId());
-		assertEquals(A, all.get("refs/5").getObjectId());
-		assertEquals(A, all.get("refs/6").getObjectId());
-		assertEquals(A, all.get("refs/end").getObjectId());
-	}
-
-	@Test
-	public void testGetRef_NonExistingBranchConfig() throws IOException {
-		assertNull("find branch config", refdb.findRef("config"));
-		assertNull("find branch config", refdb.findRef("refs/heads/config"));
-	}
-
-	@Test
-	public void testGetRef_FindBranchConfig() throws IOException {
-		update("refs/heads/config", A);
-
-		for (String t : new String[] { "config", "refs/heads/config" }) {
-			Ref r = refdb.findRef(t);
-			assertNotNull("find branch config (" + t + ")", r);
-			assertEquals("for " + t, "refs/heads/config", r.getName());
-			assertEquals("for " + t, A, r.getObjectId());
-		}
-	}
-
-	@Test
-	public void testFirstExactRef() throws IOException {
-		update("refs/heads/A", A);
-		update("refs/tags/v1.0", v1_0);
-
-		Ref a = refdb.firstExactRef("refs/heads/A", "refs/tags/v1.0");
-		Ref one = refdb.firstExactRef("refs/tags/v1.0", "refs/heads/A");
-
-		assertEquals("refs/heads/A", a.getName());
-		assertEquals("refs/tags/v1.0", one.getName());
-
-		assertEquals(A, a.getObjectId());
-		assertEquals(v1_0, one.getObjectId());
-	}
-
-	@Test
-	public void testExactRef_DiscoversModified() throws IOException {
-		symref(HEAD, "refs/heads/master");
-		update("refs/heads/master", A);
-		assertEquals(A, refdb.exactRef(HEAD).getObjectId());
-
-		update("refs/heads/master", B);
-		assertEquals(B, refdb.exactRef(HEAD).getObjectId());
-	}
-
-	@Test
-	public void testIsNameConflicting() throws IOException {
-		update("refs/heads/a/b", A);
-		update("refs/heads/q", B);
-
-		// new references cannot replace an existing container
-		assertTrue(refdb.isNameConflicting("refs"));
-		assertTrue(refdb.isNameConflicting("refs/heads"));
-		assertTrue(refdb.isNameConflicting("refs/heads/a"));
-
-		// existing reference is not conflicting
-		assertFalse(refdb.isNameConflicting("refs/heads/a/b"));
-
-		// new references are not conflicting
-		assertFalse(refdb.isNameConflicting("refs/heads/a/d"));
-		assertFalse(refdb.isNameConflicting("refs/heads/master"));
-
-		// existing reference must not be used as a container
-		assertTrue(refdb.isNameConflicting("refs/heads/a/b/c"));
-		assertTrue(refdb.isNameConflicting("refs/heads/q/master"));
-
-		// refs/txn/ names always conflict.
-		assertTrue(refdb.isNameConflicting(refdb.getTxnCommitted()));
-		assertTrue(refdb.isNameConflicting("refs/txn/foo"));
-	}
-
-	@Test
-	public void testUpdate_RefusesRefsTxnNamespace() throws IOException {
-		ObjectId txnId = getTxnCommitted();
-
-		RefUpdate u = refdb.newUpdate("refs/txn/tmp", false);
-		u.setNewObjectId(B);
-		assertEquals(RefUpdate.Result.LOCK_FAILURE, u.update());
-		assertEquals(txnId, getTxnCommitted());
-
-		ReceiveCommand cmd = command(null, B, "refs/txn/tmp");
-		BatchRefUpdate batch = refdb.newBatchUpdate();
-		batch.addCommand(cmd);
-		try (RevWalk rw = new RevWalk(repo)) {
-			batch.execute(rw, NullProgressMonitor.INSTANCE);
-		}
-		assertEquals(REJECTED_OTHER_REASON, cmd.getResult());
-		assertEquals(MessageFormat.format(JGitText.get().invalidRefName,
-				"refs/txn/tmp"), cmd.getMessage());
-		assertEquals(txnId, getTxnCommitted());
-	}
-
-	@Test
-	public void testUpdate_RefusesDotLockInRefName() throws IOException {
-		ObjectId txnId = getTxnCommitted();
-
-		RefUpdate u = refdb.newUpdate("refs/heads/pu.lock", false);
-		u.setNewObjectId(B);
-		assertEquals(RefUpdate.Result.REJECTED, u.update());
-		assertEquals(txnId, getTxnCommitted());
-
-		ReceiveCommand cmd = command(null, B, "refs/heads/pu.lock");
-		BatchRefUpdate batch = refdb.newBatchUpdate();
-		batch.addCommand(cmd);
-		try (RevWalk rw = new RevWalk(repo)) {
-			batch.execute(rw, NullProgressMonitor.INSTANCE);
-		}
-		assertEquals(REJECTED_OTHER_REASON, cmd.getResult());
-		assertEquals(JGitText.get().funnyRefname, cmd.getMessage());
-		assertEquals(txnId, getTxnCommitted());
-	}
-
-	@Test
-	public void testUpdate_RefusesOrigHeadOnBare() throws IOException {
-		assertTrue(refdb.getRepository().isBare());
-		ObjectId txnId = getTxnCommitted();
-
-		RefUpdate orig = refdb.newUpdate(ORIG_HEAD, true);
-		orig.setNewObjectId(B);
-		assertEquals(RefUpdate.Result.LOCK_FAILURE, orig.update());
-		assertEquals(txnId, getTxnCommitted());
-
-		ReceiveCommand cmd = command(null, B, ORIG_HEAD);
-		BatchRefUpdate batch = refdb.newBatchUpdate();
-		batch.addCommand(cmd);
-		try (RevWalk rw = new RevWalk(repo)) {
-			batch.execute(rw, NullProgressMonitor.INSTANCE);
-		}
-		assertEquals(REJECTED_OTHER_REASON, cmd.getResult());
-		assertEquals(
-				MessageFormat.format(JGitText.get().invalidRefName, ORIG_HEAD),
-				cmd.getMessage());
-		assertEquals(txnId, getTxnCommitted());
-	}
-
-	@Test
-	public void testBatchRefUpdate_NonFastForwardAborts() throws IOException {
-		update("refs/heads/master", A);
-		update("refs/heads/masters", B);
-		ObjectId txnId = getTxnCommitted();
-
-		List<ReceiveCommand> commands = Arrays.asList(
-				command(A, B, "refs/heads/master"),
-				command(B, A, "refs/heads/masters"));
-		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
-		batchUpdate.addCommand(commands);
-		try (RevWalk rw = new RevWalk(repo)) {
-			batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
-		}
-		assertEquals(txnId, getTxnCommitted());
-
-		assertEquals(REJECTED_NONFASTFORWARD,
-				commands.get(1).getResult());
-		assertEquals(REJECTED_OTHER_REASON,
-				commands.get(0).getResult());
-		assertEquals(JGitText.get().transactionAborted,
-				commands.get(0).getMessage());
-	}
-
-	@Test
-	public void testBatchRefUpdate_ForceUpdate() throws IOException {
-		update("refs/heads/master", A);
-		update("refs/heads/masters", B);
-		ObjectId txnId = getTxnCommitted();
-
-		List<ReceiveCommand> commands = Arrays.asList(
-				command(A, B, "refs/heads/master"),
-				command(B, A, "refs/heads/masters"));
-		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
-		batchUpdate.setAllowNonFastForwards(true);
-		batchUpdate.addCommand(commands);
-		try (RevWalk rw = new RevWalk(repo)) {
-			batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
-		}
-		assertNotEquals(txnId, getTxnCommitted());
-
-		Map<String, Ref> refs = refdb.getRefs(ALL);
-		assertEquals(OK, commands.get(0).getResult());
-		assertEquals(OK, commands.get(1).getResult());
-		assertEquals(
-				"[refs/heads/master, refs/heads/masters]",
-				refs.keySet().toString());
-		assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId());
-		assertEquals(A.getId(), refs.get("refs/heads/masters").getObjectId());
-	}
-
-	@Test
-	public void testBatchRefUpdate_NonFastForwardDoesNotDoExpensiveMergeCheck()
-			throws IOException {
-		update("refs/heads/master", B);
-		ObjectId txnId = getTxnCommitted();
-
-		List<ReceiveCommand> commands = Arrays.asList(
-				command(B, A, "refs/heads/master"));
-		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
-		batchUpdate.setAllowNonFastForwards(true);
-		batchUpdate.addCommand(commands);
-		try (RevWalk rw = new RevWalk(repo) {
-			@Override
-			public boolean isMergedInto(RevCommit base, RevCommit tip) {
-				fail("isMergedInto() should not be called");
-				return false;
-			}
-		}) {
-			batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
-		}
-		assertNotEquals(txnId, getTxnCommitted());
-
-		Map<String, Ref> refs = refdb.getRefs(ALL);
-		assertEquals(OK, commands.get(0).getResult());
-		assertEquals(A.getId(), refs.get("refs/heads/master").getObjectId());
-	}
-
-	@Test
-	public void testBatchRefUpdate_ConflictCausesAbort() throws IOException {
-		update("refs/heads/master", A);
-		update("refs/heads/masters", B);
-		ObjectId txnId = getTxnCommitted();
-
-		List<ReceiveCommand> commands = Arrays.asList(
-				command(A, B, "refs/heads/master"),
-				command(null, A, "refs/heads/master/x"),
-				command(null, A, "refs/heads"));
-		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
-		batchUpdate.setAllowNonFastForwards(true);
-		batchUpdate.addCommand(commands);
-		try (RevWalk rw = new RevWalk(repo)) {
-			batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
-		}
-		assertEquals(txnId, getTxnCommitted());
-
-		assertEquals(LOCK_FAILURE, commands.get(0).getResult());
-
-		assertEquals(REJECTED_OTHER_REASON, commands.get(1).getResult());
-		assertEquals(JGitText.get().transactionAborted,
-				commands.get(1).getMessage());
-
-		assertEquals(REJECTED_OTHER_REASON, commands.get(2).getResult());
-		assertEquals(JGitText.get().transactionAborted,
-				commands.get(2).getMessage());
-	}
-
-	@Test
-	public void testBatchRefUpdate_NoConflictIfDeleted() throws IOException {
-		update("refs/heads/master", A);
-		update("refs/heads/masters", B);
-		ObjectId txnId = getTxnCommitted();
-
-		List<ReceiveCommand> commands = Arrays.asList(
-				command(A, B, "refs/heads/master"),
-				command(null, A, "refs/heads/masters/x"),
-				command(B, null, "refs/heads/masters"));
-		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
-		batchUpdate.setAllowNonFastForwards(true);
-		batchUpdate.addCommand(commands);
-		try (RevWalk rw = new RevWalk(repo)) {
-			batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
-		}
-		assertNotEquals(txnId, getTxnCommitted());
-
-		assertEquals(OK, commands.get(0).getResult());
-		assertEquals(OK, commands.get(1).getResult());
-		assertEquals(OK, commands.get(2).getResult());
-
-		Map<String, Ref> refs = refdb.getRefs(ALL);
-		assertEquals(
-				"[refs/heads/master, refs/heads/masters/x]",
-				refs.keySet().toString());
-		assertEquals(A.getId(), refs.get("refs/heads/masters/x").getObjectId());
-	}
-
-	private ObjectId getTxnCommitted() throws IOException {
-		Ref r = bootstrap.exactRef(refdb.getTxnCommitted());
-		if (r != null && r.getObjectId() != null) {
-			return r.getObjectId();
-		}
-		return ObjectId.zeroId();
-	}
-
-	private static ReceiveCommand command(AnyObjectId a, AnyObjectId b,
-			String name) {
-		return new ReceiveCommand(
-				a != null ? a.copy() : ObjectId.zeroId(),
-				b != null ? b.copy() : ObjectId.zeroId(),
-				name);
-	}
-
-	private void symref(String name, String dst)
-			throws IOException {
-		commit((ObjectReader reader, RefTree tree) -> {
-			Ref old = tree.exactRef(reader, name);
-			Command n = new Command(old, new SymbolicRef(name,
-					new ObjectIdRef.Unpeeled(Ref.Storage.NEW, dst, null)));
-			return tree.apply(Collections.singleton(n));
-		});
-	}
-
-	private void update(String name, ObjectId id)
-			throws IOException {
-		commit((ObjectReader reader, RefTree tree) -> {
-			Ref old = tree.exactRef(reader, name);
-			Command n;
-			try (RevWalk rw = new RevWalk(repo)) {
-				n = new Command(old, Command.toRef(rw, id, null, name, true));
-			}
-			return tree.apply(Collections.singleton(n));
-		});
-	}
-
-	interface Function {
-		boolean apply(ObjectReader reader, RefTree tree) throws IOException;
-	}
-
-	private void commit(Function fun) throws IOException {
-		try (ObjectReader reader = repo.newObjectReader();
-				ObjectInserter inserter = repo.newObjectInserter();
-				RevWalk rw = new RevWalk(reader)) {
-			RefUpdate u = bootstrap.newUpdate(refdb.getTxnCommitted(), false);
-			CommitBuilder cb = new CommitBuilder();
-			testRepo.setAuthorAndCommitter(cb);
-
-			Ref ref = bootstrap.exactRef(refdb.getTxnCommitted());
-			RefTree tree;
-			if (ref != null && ref.getObjectId() != null) {
-				tree = RefTree.read(reader, rw.parseTree(ref.getObjectId()));
-				cb.setParentId(ref.getObjectId());
-				u.setExpectedOldObjectId(ref.getObjectId());
-			} else {
-				tree = RefTree.newEmptyTree();
-				u.setExpectedOldObjectId(ObjectId.zeroId());
-			}
-
-			assertTrue(fun.apply(reader, tree));
-			cb.setTreeId(tree.writeTree(inserter));
-			u.setNewObjectId(inserter.insert(cb));
-			inserter.flush();
-			switch (u.update(rw)) {
-			case NEW:
-			case FAST_FORWARD:
-				break;
-			default:
-				fail("Expected " + u.getName() + " to update");
-			}
-		}
-	}
-
-	private class InMemRefTreeRepo extends InMemoryRepository {
-		private final RefTreeDatabase refs;
-
-		InMemRefTreeRepo(DfsRepositoryDescription repoDesc) {
-			super(repoDesc);
-			refs = new RefTreeDatabase(this, super.getRefDatabase(),
-					"refs/txn/committed");
-			RefTreeDatabaseTest.this.refdb = refs;
-		}
-
-		@Override
-		public RefDatabase getRefDatabase() {
-			return refs;
-		}
-	}
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java
deleted file mode 100644
index a5b0190..0000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.Constants.R_HEADS;
-import static org.eclipse.jgit.lib.Constants.R_TAGS;
-import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
-import static org.eclipse.jgit.lib.Ref.Storage.NEW;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
-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.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.revwalk.RevBlob;
-import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.junit.Before;
-import org.junit.Test;
-
-public class RefTreeTest {
-	private static final String R_MASTER = R_HEADS + "master";
-	private InMemoryRepository repo;
-	private TestRepository<InMemoryRepository> git;
-
-	@Before
-	public void setUp() throws IOException {
-		repo = new InMemoryRepository(new DfsRepositoryDescription("RefTree"));
-		git = new TestRepository<>(repo);
-	}
-
-	@Test
-	public void testEmptyTree() throws IOException {
-		RefTree tree = RefTree.newEmptyTree();
-		try (ObjectReader reader = repo.newObjectReader()) {
-			assertNull(HEAD, tree.exactRef(reader, HEAD));
-			assertNull("master", tree.exactRef(reader, R_MASTER));
-		}
-	}
-
-	@Test
-	public void testApplyThenReadMaster() throws Exception {
-		RefTree tree = RefTree.newEmptyTree();
-		RevBlob id = git.blob("A");
-		Command cmd = new Command(null, ref(R_MASTER, id));
-		assertTrue(tree.apply(Collections.singletonList(cmd)));
-		assertSame(NOT_ATTEMPTED, cmd.getResult());
-
-		try (ObjectReader reader = repo.newObjectReader()) {
-			Ref m = tree.exactRef(reader, R_MASTER);
-			assertNotNull(R_MASTER, m);
-			assertEquals(R_MASTER, m.getName());
-			assertEquals(id, m.getObjectId());
-			assertTrue("peeled", m.isPeeled());
-		}
-	}
-
-	@Test
-	public void testUpdateMaster() throws Exception {
-		RefTree tree = RefTree.newEmptyTree();
-		RevBlob id1 = git.blob("A");
-		Command cmd1 = new Command(null, ref(R_MASTER, id1));
-		assertTrue(tree.apply(Collections.singletonList(cmd1)));
-		assertSame(NOT_ATTEMPTED, cmd1.getResult());
-
-		RevBlob id2 = git.blob("B");
-		Command cmd2 = new Command(ref(R_MASTER, id1), ref(R_MASTER, id2));
-		assertTrue(tree.apply(Collections.singletonList(cmd2)));
-		assertSame(NOT_ATTEMPTED, cmd2.getResult());
-
-		try (ObjectReader reader = repo.newObjectReader()) {
-			Ref m = tree.exactRef(reader, R_MASTER);
-			assertNotNull(R_MASTER, m);
-			assertEquals(R_MASTER, m.getName());
-			assertEquals(id2, m.getObjectId());
-			assertTrue("peeled", m.isPeeled());
-		}
-	}
-
-	@Test
-	public void testHeadSymref() throws Exception {
-		RefTree tree = RefTree.newEmptyTree();
-		RevBlob id = git.blob("A");
-		Command cmd1 = new Command(null, ref(R_MASTER, id));
-		Command cmd2 = new Command(null, symref(HEAD, R_MASTER));
-		assertTrue(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 })));
-		assertSame(NOT_ATTEMPTED, cmd1.getResult());
-		assertSame(NOT_ATTEMPTED, cmd2.getResult());
-
-		try (ObjectReader reader = repo.newObjectReader()) {
-			Ref m = tree.exactRef(reader, HEAD);
-			assertNotNull(HEAD, m);
-			assertEquals(HEAD, m.getName());
-			assertTrue("symbolic", m.isSymbolic());
-			assertNotNull(m.getTarget());
-			assertEquals(R_MASTER, m.getTarget().getName());
-			assertEquals(id, m.getTarget().getObjectId());
-		}
-
-		// Writing flushes some buffers, re-read from blob.
-		ObjectId newId = write(tree);
-		try (ObjectReader reader = repo.newObjectReader();
-				RevWalk rw = new RevWalk(reader)) {
-			tree = RefTree.read(reader, rw.parseTree(newId));
-			Ref m = tree.exactRef(reader, HEAD);
-			assertEquals(R_MASTER, m.getTarget().getName());
-		}
-	}
-
-	@Test
-	public void testTagIsPeeled() throws Exception {
-		String name = "v1.0";
-		RefTree tree = RefTree.newEmptyTree();
-		RevBlob id = git.blob("A");
-		RevTag tag = git.tag(name, id);
-
-		String ref = R_TAGS + name;
-		Command cmd = create(ref, tag);
-		assertTrue(tree.apply(Collections.singletonList(cmd)));
-		assertSame(NOT_ATTEMPTED, cmd.getResult());
-
-		try (ObjectReader reader = repo.newObjectReader()) {
-			Ref m = tree.exactRef(reader, ref);
-			assertNotNull(ref, m);
-			assertEquals(ref, m.getName());
-			assertEquals(tag, m.getObjectId());
-			assertTrue("peeled", m.isPeeled());
-			assertEquals(id, m.getPeeledObjectId());
-		}
-	}
-
-	@Test
-	public void testApplyAlreadyExists() throws Exception {
-		RefTree tree = RefTree.newEmptyTree();
-		RevBlob a = git.blob("A");
-		Command cmd = new Command(null, ref(R_MASTER, a));
-		assertTrue(tree.apply(Collections.singletonList(cmd)));
-		ObjectId treeId = write(tree);
-
-		RevBlob b = git.blob("B");
-		Command cmd1 = create(R_MASTER, b);
-		Command cmd2 = create(R_MASTER, b);
-		assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 })));
-		assertSame(LOCK_FAILURE, cmd1.getResult());
-		assertSame(REJECTED_OTHER_REASON, cmd2.getResult());
-		assertEquals(JGitText.get().transactionAborted, cmd2.getMessage());
-		assertEquals(treeId, write(tree));
-	}
-
-	@Test
-	public void testApplyWrongOldId() throws Exception {
-		RefTree tree = RefTree.newEmptyTree();
-		RevBlob a = git.blob("A");
-		Command cmd = new Command(null, ref(R_MASTER, a));
-		assertTrue(tree.apply(Collections.singletonList(cmd)));
-		ObjectId treeId = write(tree);
-
-		RevBlob b = git.blob("B");
-		RevBlob c = git.blob("C");
-		Command cmd1 = update(R_MASTER, b, c);
-		Command cmd2 = create(R_MASTER, b);
-		assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 })));
-		assertSame(LOCK_FAILURE, cmd1.getResult());
-		assertSame(REJECTED_OTHER_REASON, cmd2.getResult());
-		assertEquals(JGitText.get().transactionAborted, cmd2.getMessage());
-		assertEquals(treeId, write(tree));
-	}
-
-	@Test
-	public void testApplyWrongOldIdButAlreadyCurrentIsNoOp() throws Exception {
-		RefTree tree = RefTree.newEmptyTree();
-		RevBlob a = git.blob("A");
-		Command cmd = new Command(null, ref(R_MASTER, a));
-		assertTrue(tree.apply(Collections.singletonList(cmd)));
-		ObjectId treeId = write(tree);
-
-		RevBlob b = git.blob("B");
-		cmd = update(R_MASTER, b, a);
-		assertTrue(tree.apply(Collections.singletonList(cmd)));
-		assertEquals(treeId, write(tree));
-	}
-
-	@Test
-	public void testApplyCannotCreateSubdirectory() throws Exception {
-		RefTree tree = RefTree.newEmptyTree();
-		RevBlob a = git.blob("A");
-		Command cmd = new Command(null, ref(R_MASTER, a));
-		assertTrue(tree.apply(Collections.singletonList(cmd)));
-		ObjectId treeId = write(tree);
-
-		RevBlob b = git.blob("B");
-		Command cmd1 = create(R_MASTER + "/fail", b);
-		assertFalse(tree.apply(Collections.singletonList(cmd1)));
-		assertSame(LOCK_FAILURE, cmd1.getResult());
-		assertEquals(treeId, write(tree));
-	}
-
-	@Test
-	public void testApplyCannotCreateParentRef() throws Exception {
-		RefTree tree = RefTree.newEmptyTree();
-		RevBlob a = git.blob("A");
-		Command cmd = new Command(null, ref(R_MASTER, a));
-		assertTrue(tree.apply(Collections.singletonList(cmd)));
-		ObjectId treeId = write(tree);
-
-		RevBlob b = git.blob("B");
-		Command cmd1 = create("refs/heads", b);
-		assertFalse(tree.apply(Collections.singletonList(cmd1)));
-		assertSame(LOCK_FAILURE, cmd1.getResult());
-		assertEquals(treeId, write(tree));
-	}
-
-	private static Ref ref(String name, ObjectId id) {
-		return new ObjectIdRef.PeeledNonTag(LOOSE, name, id);
-	}
-
-	private static Ref symref(String name, String dest) {
-		Ref d = new ObjectIdRef.PeeledNonTag(NEW, dest, null);
-		return new SymbolicRef(name, d);
-	}
-
-	private Command create(String name, ObjectId id)
-			throws MissingObjectException, IOException {
-		return update(name, ObjectId.zeroId(), id);
-	}
-
-	private Command update(String name, ObjectId oldId, ObjectId newId)
-			throws MissingObjectException, IOException {
-		try (RevWalk rw = new RevWalk(repo)) {
-			return new Command(rw, new ReceiveCommand(oldId, newId, name));
-		}
-	}
-
-	private ObjectId write(RefTree tree) throws IOException {
-		try (ObjectInserter ins = repo.newObjectInserter()) {
-			ObjectId id = tree.writeTree(ins);
-			ins.flush();
-			return id;
-		}
-	}
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java
index 6c8c3ba..b11dd63 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java
@@ -22,13 +22,13 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
+import java.time.Duration;
 import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.regex.Pattern;
 
 import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.util.http.HttpCookiesMatcher;
@@ -48,10 +48,14 @@
 	private URL baseUrl;
 
 	/**
-	 * This is the expiration date that is used in the test cookie files
+	 * This is the expiration date that is used in the test cookie files.
 	 */
-	private static long JAN_01_2030_NOON = Instant
-			.parse("2030-01-01T12:00:00.000Z").toEpochMilli();
+	private static final Instant TEST_EXPIRY_DATE = Instant
+			.parse("2030-01-01T12:00:00.000Z");
+
+	/** Earlier than TEST_EXPIRY_DATE. */
+	private static final Instant TEST_DATE = TEST_EXPIRY_DATE.minus(180,
+			ChronoUnit.DAYS);
 
 	@Before
 	public void setUp() throws IOException {
@@ -102,14 +106,13 @@
 		cookie.setPath("/");
 		cookie.setMaxAge(1000);
 		cookies.add(cookie);
-		Date creationDate = new Date();
 		try (Writer writer = Files.newBufferedWriter(tmpFile,
 				StandardCharsets.US_ASCII)) {
-			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
+			NetscapeCookieFile.write(writer, cookies, baseUrl, TEST_DATE);
 		}
 
 		String expectedExpiration = String
-				.valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000));
+				.valueOf(TEST_DATE.getEpochSecond() + cookie.getMaxAge());
 
 		assertThat(Files.readAllLines(tmpFile, StandardCharsets.US_ASCII),
 				CoreMatchers
@@ -128,13 +131,12 @@
 		HttpCookie cookie = new HttpCookie("key2", "value2");
 		cookie.setMaxAge(1000);
 		cookies.add(cookie);
-		Date creationDate = new Date();
 		try (Writer writer = Files.newBufferedWriter(tmpFile,
 				StandardCharsets.US_ASCII)) {
-			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
+			NetscapeCookieFile.write(writer, cookies, baseUrl, TEST_DATE);
 		}
 		String expectedExpiration = String
-				.valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000));
+				.valueOf(TEST_DATE.getEpochSecond() + cookie.getMaxAge());
 
 		assertThat(Files.readAllLines(tmpFile, StandardCharsets.US_ASCII),
 				CoreMatchers.equalTo(
@@ -161,13 +163,29 @@
 	}
 
 	@Test
+	public void testReadCookieFileWithMilliseconds() throws IOException {
+		try (InputStream input = this.getClass()
+				.getResourceAsStream("cookies-with-milliseconds.txt")) {
+			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+		}
+		NetscapeCookieFile cookieFile = new NetscapeCookieFile(tmpFile,
+				TEST_DATE);
+		long expectedMaxAge = Duration.between(TEST_DATE, TEST_EXPIRY_DATE)
+				.getSeconds();
+		for (HttpCookie cookie : cookieFile.getCookies(true)) {
+			assertEquals(expectedMaxAge, cookie.getMaxAge());
+		}
+	}
+
+	@Test
 	public void testWriteAfterAnotherJgitProcessModifiedTheFile()
 			throws IOException, InterruptedException {
 		try (InputStream input = this.getClass()
 				.getResourceAsStream("cookies-simple1.txt")) {
 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
 		}
-		NetscapeCookieFile cookieFile = new NetscapeCookieFile(tmpFile);
+		NetscapeCookieFile cookieFile = new NetscapeCookieFile(tmpFile,
+				TEST_DATE);
 		cookieFile.getCookies(true);
 		// now modify file externally
 		try (InputStream input = this.getClass()
@@ -177,39 +195,19 @@
 		// now try to write
 		cookieFile.write(baseUrl);
 
-		// validate that the external changes are there as well
-		// due to rounding errors (conversion from ms to sec to ms)
-		// the expiration date might not be exact
 		List<String> lines = Files.readAllLines(tmpFile,
 				StandardCharsets.US_ASCII);
 
 		assertEquals("Expected 3 lines", 3, lines.size());
-		assertStringMatchesPatternWithInexactNumber(lines.get(0),
-				"some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey1\tvalueFromSimple2",
-				JAN_01_2030_NOON, 1000);
-		assertStringMatchesPatternWithInexactNumber(lines.get(1),
-				"some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey3\tvalueFromSimple2",
-				JAN_01_2030_NOON, 1000);
-		assertStringMatchesPatternWithInexactNumber(lines.get(2),
-				"some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey2\tvalueFromSimple1",
-				JAN_01_2030_NOON, 1000);
-	}
-
-	@SuppressWarnings("boxing")
-	private static final void assertStringMatchesPatternWithInexactNumber(
-			String string, String pattern, long expectedNumericValue,
-			long delta) {
-		java.util.regex.Matcher matcher = Pattern.compile(pattern)
-				.matcher(string);
-		assertTrue("Given string '" + string + "' does not match '" + pattern
-				+ "'", matcher.matches());
-		// extract numeric value
-		Long actualNumericValue = Long.decode(matcher.group(1));
-
-		assertTrue(
-				"Value is supposed to be close to " + expectedNumericValue
-						+ " but is " + actualNumericValue + ".",
-				Math.abs(expectedNumericValue - actualNumericValue) <= delta);
+		assertEquals(
+				"some-domain1\tTRUE\t/some/path1\tFALSE\t1893499200\tkey1\tvalueFromSimple2",
+				lines.get(0));
+		assertEquals(
+				"some-domain1\tTRUE\t/some/path1\tFALSE\t1893499200\tkey3\tvalueFromSimple2",
+				lines.get(1));
+		assertEquals(
+				"some-domain1\tTRUE\t/some/path1\tFALSE\t1893499200\tkey2\tvalueFromSimple1",
+				lines.get(2));
 	}
 
 	@Test
@@ -229,14 +227,13 @@
 		cookie.setHttpOnly(true);
 		cookies.add(cookie);
 
-		Date creationDate = new Date();
-
 		try (Writer writer = Files.newBufferedWriter(tmpFile,
 				StandardCharsets.US_ASCII)) {
-			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
+			NetscapeCookieFile.write(writer, cookies, baseUrl, TEST_DATE);
 		}
 		Set<HttpCookie> actualCookies = new NetscapeCookieFile(tmpFile,
-				creationDate).getCookies(true);
+				TEST_DATE)
+				.getCookies(true);
 		assertThat(actualCookies, HttpCookiesMatcher.containsInOrder(cookies));
 	}
 
@@ -246,15 +243,12 @@
 				.getResourceAsStream("cookies-simple1.txt")) {
 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
 		}
-		// round up to the next second (to prevent rounding errors)
-		Date creationDate = new Date(
-				(System.currentTimeMillis() / 1000) * 1000);
-		Set<HttpCookie> cookies = new NetscapeCookieFile(tmpFile, creationDate)
+		Set<HttpCookie> cookies = new NetscapeCookieFile(tmpFile, TEST_DATE)
 				.getCookies(true);
 		Path tmpFile2 = folder.newFile().toPath();
 		try (Writer writer = Files.newBufferedWriter(tmpFile2,
 				StandardCharsets.US_ASCII)) {
-			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
+			NetscapeCookieFile.write(writer, cookies, baseUrl, TEST_DATE);
 		}
 		// compare original file with newly written one, they should not differ
 		assertEquals(Files.readAllLines(tmpFile), Files.readAllLines(tmpFile2));
@@ -267,13 +261,13 @@
 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
 		}
 
-		Date creationDate = new Date();
 		Set<HttpCookie> cookies = new LinkedHashSet<>();
 
 		HttpCookie cookie = new HttpCookie("key2", "value2");
 		cookie.setDomain("some-domain2");
 		cookie.setPath("/some/path2");
-		cookie.setMaxAge((JAN_01_2030_NOON - creationDate.getTime()) / 1000);
+		cookie.setMaxAge(
+				Duration.between(TEST_DATE, TEST_EXPIRY_DATE).getSeconds());
 		cookie.setSecure(true);
 		cookie.setHttpOnly(true);
 		cookies.add(cookie);
@@ -281,11 +275,12 @@
 		cookie = new HttpCookie("key3", "value3");
 		cookie.setDomain("some-domain3");
 		cookie.setPath("/some/path3");
-		cookie.setMaxAge((JAN_01_2030_NOON - creationDate.getTime()) / 1000);
+		cookie.setMaxAge(
+				Duration.between(TEST_DATE, TEST_EXPIRY_DATE).getSeconds());
 		cookies.add(cookie);
 
-		Set<HttpCookie> actualCookies = new NetscapeCookieFile(tmpFile, creationDate)
-				.getCookies(true);
+		Set<HttpCookie> actualCookies = new NetscapeCookieFile(tmpFile,
+				TEST_DATE).getCookies(true);
 		assertThat(actualCookies, HttpCookiesMatcher.containsInOrder(cookies));
 	}
 
@@ -296,7 +291,7 @@
 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
 		}
 
-		new NetscapeCookieFile(tmpFile)
-				.getCookies(true);
+		assertTrue(new NetscapeCookieFile(tmpFile, TEST_DATE).getCookies(true)
+				.isEmpty());
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java
index dee58f9..2f1bada 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Salesforce. and others
+ * Copyright (C) 2018, 2020 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
@@ -53,7 +53,7 @@
 	private void assertGpgSignatureStringOutcome(String signature,
 			String expectedOutcome) throws IOException {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		CommitBuilder.writeGpgSignatureString(signature, out);
+		ObjectBuilder.writeMultiLineHeader(signature, out, true);
 		String formatted_signature = new String(out.toByteArray(), US_ASCII);
 		assertEquals(expectedOutcome, formatted_signature);
 	}
@@ -85,8 +85,8 @@
 		String signature = "Ü Ä";
 		IllegalArgumentException e = assertThrows(
 				IllegalArgumentException.class,
-				() -> CommitBuilder.writeGpgSignatureString(signature,
-						new ByteArrayOutputStream()));
+				() -> ObjectBuilder.writeMultiLineHeader(signature,
+						new ByteArrayOutputStream(), true));
 		String message = MessageFormat.format(JGitText.get().notASCIIString,
 				signature);
 		assertEquals(message, e.getMessage());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java
index 88d17ec..7590048 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java
@@ -26,6 +26,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -318,6 +319,64 @@
 	}
 
 	@Test
+	public void testGetRefsExcludingPrefix() throws IOException {
+		Set<String> exclude = new HashSet<>();
+		exclude.add("refs/tags");
+		// HEAD + 12 refs/heads are present here.
+		List<Ref> refs =
+				db.getRefDatabase().getRefsByPrefixWithExclusions(RefDatabase.ALL, exclude);
+		assertEquals(13, refs.size());
+		checkContainsRef(refs, db.exactRef("HEAD"));
+		checkContainsRef(refs, db.exactRef("refs/heads/a"));
+		for (Ref notInResult : db.getRefDatabase().getRefsByPrefix("refs/tags")) {
+			assertFalse(refs.contains(notInResult));
+		}
+	}
+
+	@Test
+	public void testGetRefsExcludingPrefixes() throws IOException {
+		Set<String> exclude = new HashSet<>();
+		exclude.add("refs/tags/");
+		exclude.add("refs/heads/");
+		List<Ref> refs = db.getRefDatabase().getRefsByPrefixWithExclusions(RefDatabase.ALL, exclude);
+		assertEquals(1, refs.size());
+		checkContainsRef(refs, db.exactRef("HEAD"));
+	}
+
+	@Test
+	public void testGetRefsExcludingNonExistingPrefixes() throws IOException {
+		Set<String> prefixes = new HashSet<>();
+		prefixes.add("refs/tags/");
+		prefixes.add("refs/heads/");
+		prefixes.add("refs/nonexistent/");
+		List<Ref> refs = db.getRefDatabase().getRefsByPrefixWithExclusions(RefDatabase.ALL, prefixes);
+		assertEquals(1, refs.size());
+		checkContainsRef(refs, db.exactRef("HEAD"));
+	}
+
+	@Test
+	public void testGetRefsWithPrefixExcludingPrefixes() throws IOException {
+		Set<String> exclude = new HashSet<>();
+		exclude.add("refs/heads/pa");
+		String include = "refs/heads/p";
+		List<Ref> refs = db.getRefDatabase().getRefsByPrefixWithExclusions(include, exclude);
+		assertEquals(1, refs.size());
+		checkContainsRef(refs, db.exactRef("refs/heads/prefix/a"));
+	}
+
+	@Test
+	public void testGetRefsWithPrefixExcludingOverlappingPrefixes() throws IOException {
+		Set<String> exclude = new HashSet<>();
+		exclude.add("refs/heads/pa");
+		exclude.add("refs/heads/");
+		exclude.add("refs/heads/p");
+		exclude.add("refs/tags/");
+		List<Ref> refs = db.getRefDatabase().getRefsByPrefixWithExclusions(RefDatabase.ALL, exclude);
+		assertEquals(1, refs.size());
+		checkContainsRef(refs, db.exactRef("HEAD"));
+	}
+
+	@Test
 	public void testResolveTipSha1() throws IOException {
 		ObjectId masterId = db.resolve("refs/heads/master");
 		Set<Ref> resolved = db.getRefDatabase().getTipsWithSha1(masterId);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java
new file mode 100644
index 0000000..5786022
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 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.lib;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.junit.Test;
+
+public class TagBuilderTest {
+
+	// @formatter:off
+	private static final String SIGNATURE = "-----BEGIN PGP SIGNATURE-----\n" +
+			"Version: BCPG v1.60\n" +
+			"\n" +
+			"iQEcBAABCAAGBQJb9cVhAAoJEKX+6Axg/6TZeFsH/0CY0WX/z7U8+7S5giFX4wH4\n" +
+			"opvBwqyt6OX8lgNwTwBGHFNt8LdmDCCmKoq/XwkNi3ARVjLhe3gBcKXNoavvPk2Z\n" +
+			"gIg5ChevGkU4afWCOMLVEYnkCBGw2+86XhrK1P7gTHEk1Rd+Yv1ZRDJBY+fFO7yz\n" +
+			"uSBuF5RpEY2sJiIvp27Gub/rY3B5NTR/feO/z+b9oiP/fMUhpRwG5KuWUsn9NPjw\n" +
+			"3tvbgawYpU/2UnS+xnavMY4t2fjRYjsoxndPLb2MUX8X7vC7FgWLBlmI/rquLZVM\n" +
+			"IQEKkjnA+lhejjK1rv+ulq4kGZJFKGYWYYhRDwFg5PTkzhudhN2SGUq5Wxq1Eg4=\n" +
+			"=b9OI\n" +
+			"-----END PGP SIGNATURE-----";
+
+	// @formatter:on
+
+	private static final String TAGGER_LINE = "A U. Thor <a_u_thor@example.com> 1218123387 +0700";
+
+	private static final PersonIdent TAGGER = RawParseUtils
+			.parsePersonIdent(TAGGER_LINE);
+
+	@Test
+	public void testTagSimple() throws Exception {
+		TagBuilder t = new TagBuilder();
+		t.setTag("sometag");
+		t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+		t.setEncoding(US_ASCII);
+		t.setMessage("Short message only");
+		t.setTagger(TAGGER);
+		String tag = new String(t.build(), UTF_8);
+		String expected = "object 0000000000000000000000000000000000000000\n"
+				+ "type commit\n" //
+				+ "tag sometag\n" //
+				+ "tagger " + TAGGER_LINE + '\n' //
+				+ "encoding US-ASCII\n" //
+				+ '\n' //
+				+ "Short message only";
+		assertEquals(expected, tag);
+	}
+
+	@Test
+	public void testTagWithSignatureShortMessageEndsInLF() throws Exception {
+		TagBuilder t = new TagBuilder();
+		t.setTag("sometag");
+		t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+		t.setEncoding(US_ASCII);
+		t.setMessage("Short message only\n");
+		t.setTagger(TAGGER);
+		t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+		String tag = new String(t.build(), UTF_8);
+		String expected = "object 0000000000000000000000000000000000000000\n"
+				+ "type commit\n" //
+				+ "tag sometag\n" //
+				+ "tagger " + TAGGER_LINE + '\n' //
+				+ "encoding US-ASCII\n" //
+				+ '\n' //
+				+ "Short message only\n" //
+				+ SIGNATURE + '\n';
+		assertEquals(expected, tag);
+	}
+
+	@Test
+	public void testTagWithSignatureMessageNoLF() {
+		TagBuilder t = new TagBuilder();
+		t.setTag("sometag");
+		t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+		t.setEncoding(US_ASCII);
+		t.setMessage("A message\n\nthat does not end in LF");
+		t.setTagger(TAGGER);
+		t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+		Throwable ex = assertThrows(Throwable.class, t::build);
+		assertEquals(JGitText.get().signedTagMessageNoLf, ex.getMessage());
+	}
+
+	@Test
+	public void testTagWithSignatureNoParagraphsMessage() throws Exception {
+		TagBuilder t = new TagBuilder();
+		t.setTag("sometag");
+		t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+		t.setEncoding(US_ASCII);
+		t.setMessage("A strange\ntag message\n");
+		t.setTagger(TAGGER);
+		t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+		String tag = new String(t.build(), UTF_8);
+		String expected = "object 0000000000000000000000000000000000000000\n"
+				+ "type commit\n" //
+				+ "tag sometag\n" //
+				+ "tagger " + TAGGER_LINE + '\n' //
+				+ "encoding US-ASCII\n" //
+				+ '\n' //
+				+ "A strange\ntag message\n" //
+				+ SIGNATURE + '\n';
+		assertEquals(expected, tag);
+	}
+
+	@Test
+	public void testTagWithSignatureLongMessage() throws Exception {
+		TagBuilder t = new TagBuilder();
+		t.setTag("sometag");
+		t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+		t.setMessage("Short message\n\nFollowed by explanations.\n");
+		t.setTagger(TAGGER);
+		t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+		String tag = new String(t.build(), UTF_8);
+		String expected = "object 0000000000000000000000000000000000000000\n"
+				+ "type commit\n" //
+				+ "tag sometag\n" //
+				+ "tagger " + TAGGER_LINE + '\n' //
+				+ '\n' //
+				+ "Short message\n\nFollowed by explanations.\n" //
+				+ SIGNATURE + '\n';
+		assertEquals(expected, tag);
+	}
+
+	@Test
+	public void testTagWithSignatureEmptyMessage() throws Exception {
+		TagBuilder t = new TagBuilder();
+		t.setTag("sometag");
+		t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+		t.setTagger(TAGGER);
+		t.setMessage("");
+		String emptyMsg = new String(t.build(), UTF_8);
+		t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+		String tag = new String(t.build(), UTF_8);
+		String expected = "object 0000000000000000000000000000000000000000\n"
+				+ "type commit\n" //
+				+ "tag sometag\n" //
+				+ "tagger " + TAGGER_LINE + '\n' //
+				+ '\n';
+		assertEquals(expected, emptyMsg);
+		assertEquals(expected + SIGNATURE + '\n', tag);
+	}
+
+	@Test
+	public void testTagWithSignatureOnly() throws Exception {
+		TagBuilder t = new TagBuilder();
+		t.setTag("sometag");
+		t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+		t.setTagger(TAGGER);
+		String emptyMsg = new String(t.build(), UTF_8);
+		t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+		String tag = new String(t.build(), UTF_8);
+		String expected = "object 0000000000000000000000000000000000000000\n"
+				+ "type commit\n" //
+				+ "tag sometag\n" //
+				+ "tagger " + TAGGER_LINE + '\n' //
+				+ '\n';
+		assertEquals(expected, emptyMsg);
+		assertEquals(expected + SIGNATURE + '\n', tag);
+	}
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
index e2ac89b..eecf25b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
@@ -1384,6 +1384,270 @@
 		git.merge().include(commitB).call();
 	}
 
+	/**
+	 * Merging two commits with a file/dir conflict in the virtual ancestor.
+	 *
+	 * <p>
+	 * Those conflicts should be ignored, otherwise the found base can not be used by the
+	 * RecursiveMerger.
+	 * <pre>
+	 *  --------------
+	 * |              \
+	 * |         C1 - C4 --- ?     master
+	 * |        /          /
+	 * |  I - A1 - C2 - C3         second-branch
+	 * |   \            /
+	 * \    \          /
+	 *  ----A2--------             branch-to-merge
+	 *  </pre>
+	 * <p>
+	 * <p>
+	 * Path "a" is initially a file in I and A1. It is changed to a directory in A2
+	 * ("branch-to-merge").
+	 * <p>
+	 * A2 is merged into "master" and "second-branch". The dir/file merge conflict is resolved
+	 * manually, results in C4 and C3.
+	 * <p>
+	 * While merging C3 and C4, A1 and A2 are the base commits found by the recursive merge that
+	 * have the dir/file conflict.
+	 */
+	@Theory
+	public void checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren(
+			MergeStrategy strategy)
+			throws Exception {
+		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+			return;
+		}
+
+		Git git = Git.wrap(db);
+
+		// master
+		writeTrashFile("a", "initial content");
+		git.add().addFilepattern("a").call();
+		RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+		writeTrashFile("a", "content in Ancestor 1");
+		git.add().addFilepattern("a").call();
+		RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+		writeTrashFile("a", "content in Child 1 (commited on master)");
+		git.add().addFilepattern("a").call();
+		// commit C1M
+		git.commit().setMessage("Child 1 on master").call();
+
+		git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
+		// "a" becomes a directory in A2
+		git.rm().addFilepattern("a").call();
+		writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
+		git.add().addFilepattern("a/content").call();
+		RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
+
+		// second branch
+		git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
+		writeTrashFile("a", "content in Child 2 (commited on second-branch)");
+		git.add().addFilepattern("a").call();
+		// commit C2S
+		git.commit().setMessage("Child 2 on second-branch").call();
+
+		// Merge branch-to-merge into second-branch
+		MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+		assertEquals(mergeResult.getNewHead(), null);
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+		// Resolve the conflict manually, merge "a" as a file
+		git.rm().addFilepattern("a").call();
+		git.rm().addFilepattern("a/content").call();
+		writeTrashFile("a", "merge conflict resolution");
+		git.add().addFilepattern("a").call();
+		RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict")
+				.call();
+
+		// Merge branch-to-merge into master
+		git.checkout().setName("master").call();
+		mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+		assertEquals(mergeResult.getNewHead(), null);
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+
+		// Resolve the conflict manually - merge "a" as a file
+		git.rm().addFilepattern("a").call();
+		git.rm().addFilepattern("a/content").call();
+		writeTrashFile("a", "merge conflict resolution");
+		git.add().addFilepattern("a").call();
+		// commit C4M
+		git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
+
+		// Merge C4M (second-branch) into master (C3S)
+		// Conflict in virtual base should be here, but there are no conflicts in
+		// children
+		mergeResult = git.merge().include(commitC3S).call();
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
+
+	}
+
+	@Theory
+	public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_FileDir(MergeStrategy strategy)
+			throws Exception {
+		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+			return;
+		}
+
+		Git git = Git.wrap(db);
+
+		// master
+		writeTrashFile("a", "initial content");
+		git.add().addFilepattern("a").call();
+		RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+		writeTrashFile("a", "content in Ancestor 1");
+		git.add().addFilepattern("a").call();
+		RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+		writeTrashFile("a", "content in Child 1 (commited on master)");
+		git.add().addFilepattern("a").call();
+		// commit C1M
+		git.commit().setMessage("Child 1 on master").call();
+
+		git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
+
+		// "a" becomes a directory in A2
+		git.rm().addFilepattern("a").call();
+		writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
+		git.add().addFilepattern("a/content").call();
+		RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
+
+		// second branch
+		git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
+		writeTrashFile("a", "content in Child 2 (commited on second-branch)");
+		git.add().addFilepattern("a").call();
+		// commit C2S
+		git.commit().setMessage("Child 2 on second-branch").call();
+
+		// Merge branch-to-merge into second-branch
+		MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+		assertEquals(mergeResult.getNewHead(), null);
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+		// Resolve the conflict manually - write a file
+		git.rm().addFilepattern("a").call();
+		git.rm().addFilepattern("a/content").call();
+		writeTrashFile("a",
+				"content in Child 3 (commited on second-branch) - merge conflict resolution");
+		git.add().addFilepattern("a").call();
+		RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict")
+				.call();
+
+		// Merge branch-to-merge into master
+		git.checkout().setName("master").call();
+		mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+		assertEquals(mergeResult.getNewHead(), null);
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+
+		// Resolve the conflict manually - write a file
+		git.rm().addFilepattern("a").call();
+		git.rm().addFilepattern("a/content").call();
+		writeTrashFile("a", "content in Child 4 (commited on master) - merge conflict resolution");
+		git.add().addFilepattern("a").call();
+		// commit C4M
+		git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
+
+		// Merge C4M (second-branch) into master (C3S)
+		// Conflict in virtual base should be here
+		mergeResult = git.merge().include(commitC3S).call();
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+		String expected =
+				"<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
+						+ "=======\n"
+						+ "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
+						+ ">>>>>>> " + commitC3S.name() + "\n";
+		assertEquals(expected, read("a"));
+		// Nothing was populated from the ancestors.
+		assertEquals(
+				"[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
+				indexState(CONTENT));
+	}
+
+	/**
+	 * Same test as above, but "a" is a dir in A1 and a file in A2
+	 */
+	@Theory
+	public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_DirFile(MergeStrategy strategy)
+			throws Exception {
+		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+			return;
+		}
+
+		Git git = Git.wrap(db);
+
+		// master
+		writeTrashFile("a/content", "initial content");
+		git.add().addFilepattern("a/content").call();
+		RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+		writeTrashFile("a/content", "content in Ancestor 1");
+		git.add().addFilepattern("a/content").call();
+		RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+		writeTrashFile("a/content", "content in Child 1 (commited on master)");
+		git.add().addFilepattern("a/content").call();
+		// commit C1M
+		git.commit().setMessage("Child 1 on master").call();
+
+		git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
+
+		// "a" becomes a file in A2
+		git.rm().addFilepattern("a/content").call();
+		writeTrashFile("a", "content in Ancestor 2 (commited on branch-to-merge)");
+		git.add().addFilepattern("a").call();
+		RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
+
+		// second branch
+		git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
+		writeTrashFile("a/content", "content in Child 2 (commited on second-branch)");
+		git.add().addFilepattern("a/content").call();
+		// commit C2S
+		git.commit().setMessage("Child 2 on second-branch").call();
+
+		// Merge branch-to-merge into second-branch
+		MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+		assertEquals(mergeResult.getNewHead(), null);
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+		// Resolve the conflict manually - write a file
+		git.rm().addFilepattern("a").call();
+		git.rm().addFilepattern("a/content").call();
+		deleteTrashFile("a/content");
+		deleteTrashFile("a");
+		writeTrashFile("a", "content in Child 3 (commited on second-branch) - merge conflict resolution");
+		git.add().addFilepattern("a").call();
+		RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call();
+
+		// Merge branch-to-merge into master
+		git.checkout().setName("master").call();
+		mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+		assertEquals(mergeResult.getNewHead(), null);
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+
+		// Resolve the conflict manually - write a file
+		git.rm().addFilepattern("a").call();
+		git.rm().addFilepattern("a/content").call();
+		deleteTrashFile("a/content");
+		deleteTrashFile("a");
+		writeTrashFile("a", "content in Child 4 (commited on master) - merge conflict resolution");
+		git.add().addFilepattern("a").call();
+		// commit C4M
+		git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
+
+		// Merge C4M (second-branch) into master (C3S)
+		// Conflict in virtual base should be here
+		mergeResult = git.merge().include(commitC3S).call();
+		assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+		String expected = "<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
+				+ "=======\n" + "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
+				+ ">>>>>>> " + commitC3S.name() + "\n";
+		assertEquals(expected, read("a"));
+		// Nothing was populated from the ancestors.
+		assertEquals(
+				"[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
+				indexState(CONTENT));
+	}
+
 	private void writeSubmodule(String path, ObjectId commit)
 			throws IOException, ConfigInvalidException {
 		addSubmoduleToIndex(path, commit);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevQueueTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevQueueTest.java
index a9dfe15..852d18c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevQueueTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevQueueTest.java
@@ -59,20 +59,26 @@
 	public void testInsertTie() throws Exception {
 		final RevCommit a = parseBody(commit());
 		final RevCommit b = parseBody(commit(0, a));
+		final RevCommit c = parseBody(commit(0, b));
+
 		{
 			q = create();
 			q.add(a);
 			q.add(b);
+			q.add(c);
 
 			assertCommit(a, q.next());
 			assertCommit(b, q.next());
+			assertCommit(c, q.next());
 			assertNull(q.next());
 		}
 		{
 			q = create();
+			q.add(c);
 			q.add(b);
 			q.add(a);
 
+			assertCommit(c, q.next());
 			assertCommit(b, q.next());
 			assertCommit(a, q.next());
 			assertNull(q.next());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
index b92a072..a3ba3d6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2010, Google Inc. and others
+ * Copyright (C) 2008, 2020, Google Inc. and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -11,6 +11,7 @@
 package org.eclipse.jgit.revwalk;
 
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.US_ASCII;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -18,6 +19,7 @@
 import static org.junit.Assert.assertSame;
 
 import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
 
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -117,6 +119,7 @@
 		assertNotNull(c.getTagName());
 		assertEquals(name, c.getTagName());
 		assertEquals("", c.getFullMessage());
+		assertNull(c.getRawGpgSignature());
 
 		final PersonIdent cTagger = c.getTaggerIdent();
 		assertNotNull(cTagger);
@@ -128,13 +131,12 @@
 	public void testParseOldStyleNoTagger() throws Exception {
 		final ObjectId treeId = id("9788669ad918b6fcce64af8882fc9a81cb6aba67");
 		final String name = "v1.2.3.4.5";
-		final String message = "test\n" //
-				+ "\n" //
-				+ "-----BEGIN PGP SIGNATURE-----\n" //
+		final String fakeSignature = "-----BEGIN PGP SIGNATURE-----\n" //
 				+ "Version: GnuPG v1.4.1 (GNU/Linux)\n" //
 				+ "\n" //
 				+ "iD8DBQBC0b9oF3Y\n" //
-				+ "-----END PGP SIGNATURE------n";
+				+ "-----END PGP SIGNATURE-----";
+		final String message = "test\n" + fakeSignature + '\n';
 
 		final StringBuilder body = new StringBuilder();
 
@@ -166,7 +168,9 @@
 		assertNotNull(c.getTagName());
 		assertEquals(name, c.getTagName());
 		assertEquals("test", c.getShortMessage());
-		assertEquals(message, c.getFullMessage());
+		assertEquals("test\n", c.getFullMessage());
+		assertEquals(fakeSignature + '\n',
+				new String(c.getRawGpgSignature(), US_ASCII));
 
 		assertNull(c.getTaggerIdent());
 	}
@@ -386,6 +390,108 @@
 	}
 
 	@Test
+	public void testParse_gpgSignature() throws Exception {
+		final String signature = "-----BEGIN PGP SIGNATURE-----\n\n"
+				+ "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+				+ "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+				+ "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+				+ "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+				+ "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+				+ "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+				+ "=TClh\n" + "-----END PGP SIGNATURE-----";
+		ByteArrayOutputStream b = new ByteArrayOutputStream();
+		b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
+				.getBytes(UTF_8));
+		b.write("type tree\n".getBytes(UTF_8));
+		b.write("tag v1.0\n".getBytes(UTF_8));
+		b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+		b.write('\n');
+		b.write("message\n".getBytes(UTF_8));
+		b.write(signature.getBytes(US_ASCII));
+		b.write('\n');
+
+		RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+		try (RevWalk rw = new RevWalk(db)) {
+			t.parseCanonical(rw, b.toByteArray());
+		}
+
+		assertEquals("t", t.getTaggerIdent().getName());
+		assertEquals("message", t.getShortMessage());
+		assertEquals("message\n", t.getFullMessage());
+		String gpgSig = new String(t.getRawGpgSignature(), UTF_8);
+		assertEquals(signature + '\n', gpgSig);
+	}
+
+	@Test
+	public void testParse_gpgSignature2() throws Exception {
+		final String signature = "-----BEGIN PGP SIGNATURE-----\n\n"
+				+ "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+				+ "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+				+ "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+				+ "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+				+ "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+				+ "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+				+ "=TClh\n" + "-----END PGP SIGNATURE-----";
+		ByteArrayOutputStream b = new ByteArrayOutputStream();
+		b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
+				.getBytes(UTF_8));
+		b.write("type tree\n".getBytes(UTF_8));
+		b.write("tag v1.0\n".getBytes(UTF_8));
+		b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+		b.write('\n');
+		String message = "message\n\n" + signature.replace("xXXy", "aAAb")
+				+ '\n';
+		b.write(message.getBytes(UTF_8));
+		b.write(signature.getBytes(US_ASCII));
+		b.write('\n');
+
+		RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+		try (RevWalk rw = new RevWalk(db)) {
+			t.parseCanonical(rw, b.toByteArray());
+		}
+
+		assertEquals("t", t.getTaggerIdent().getName());
+		assertEquals("message", t.getShortMessage());
+		assertEquals(message, t.getFullMessage());
+		String gpgSig = new String(t.getRawGpgSignature(), UTF_8);
+		assertEquals(signature + '\n', gpgSig);
+	}
+
+	@Test
+	public void testParse_gpgSignature3() throws Exception {
+		final String signature = "-----BEGIN PGP SIGNATURE-----\n\n"
+				+ "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+				+ "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+				+ "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+				+ "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+				+ "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+				+ "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+				+ "=TClh\n" + "-----END PGP SIGNATURE-----";
+		ByteArrayOutputStream b = new ByteArrayOutputStream();
+		b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
+				.getBytes(UTF_8));
+		b.write("type tree\n".getBytes(UTF_8));
+		b.write("tag v1.0\n".getBytes(UTF_8));
+		b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+		b.write('\n');
+		String message = "message\n\n-----BEGIN PGP SIGNATURE-----\n";
+		b.write(message.getBytes(UTF_8));
+		b.write(signature.getBytes(US_ASCII));
+		b.write('\n');
+
+		RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+		try (RevWalk rw = new RevWalk(db)) {
+			t.parseCanonical(rw, b.toByteArray());
+		}
+
+		assertEquals("t", t.getTaggerIdent().getName());
+		assertEquals("message", t.getShortMessage());
+		assertEquals(message, t.getFullMessage());
+		String gpgSig = new String(t.getRawGpgSignature(), UTF_8);
+		assertEquals(signature + '\n', gpgSig);
+	}
+
+	@Test
 	public void testParse_NoMessage() throws Exception {
 		final String msg = "";
 		final RevTag c = create(msg);
@@ -447,7 +553,8 @@
 	}
 
 	@Test
-	public void testParse_PublicParseMethod() throws CorruptObjectException {
+	public void testParse_PublicParseMethod()
+			throws CorruptObjectException, UnsupportedEncodingException {
 		TagBuilder src = new TagBuilder();
 		try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
 			src.setObjectId(fmt.idFor(Constants.OBJ_TREE, new byte[] {}),
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java
index 803ff10..b8b503c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java
@@ -13,6 +13,7 @@
 import static org.eclipse.jgit.util.FileUtils.pathToString;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -178,7 +179,7 @@
 		final Path includedFile = createFile(CONTENT1.getBytes(UTF_8), "dir1");
 		final ByteArrayOutputStream bos = new ByteArrayOutputStream();
 		bos.write("[include]\npath=".getBytes(UTF_8));
-		bos.write(("../" + includedFile.getParent().getFileName() + "/"
+		bos.write(("../" + parent(includedFile).getFileName() + "/"
 				+ includedFile.getFileName()).getBytes(UTF_8));
 
 		final Path file = createFile(bos.toByteArray(), "dir2");
@@ -213,7 +214,7 @@
 
 		final Path file = createFile(bos.toByteArray(), "repo");
 		final FS fs = FS.DETECTED.newInstance();
-		fs.setUserHome(includedFile.getParent().toFile());
+		fs.setUserHome(parent(includedFile).toFile());
 
 		final FileBasedConfig config = new FileBasedConfig(file.toFile(), fs);
 		config.load();
@@ -231,7 +232,7 @@
 		FileBasedConfig config = new FileBasedConfig(file.toFile(),
 				FS.DETECTED);
 		config.setString("include", null, "path",
-				("../" + includedFile.getParent().getFileName() + "/"
+				("../" + parent(includedFile).getFileName() + "/"
 						+ includedFile.getFileName()));
 
 		// just by setting the include.path, it won't be included
@@ -280,4 +281,10 @@
 		}
 		return f;
 	}
+
+	private Path parent(Path file) {
+		Path parent = file.getParent();
+		assertNotNull(parent);
+		return parent;
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java
index 64b16f6..7d438c1 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java
@@ -16,11 +16,11 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.Ref;
@@ -30,18 +30,6 @@
 public class BasePackConnectionTest {
 
 	@Test
-	public void testExtractSymRefsFromCapabilities() {
-		final Map<String, String> symRefs = BasePackConnection
-				.extractSymRefsFromCapabilities(
-						Arrays.asList("symref=HEAD:refs/heads/main",
-								"symref=refs/heads/sym:refs/heads/other"));
-
-		assertEquals(2, symRefs.size());
-		assertEquals("refs/heads/main", symRefs.get("HEAD"));
-		assertEquals("refs/heads/other", symRefs.get("refs/heads/sym"));
-	}
-
-	@Test
 	public void testUpdateWithSymRefsAdds() {
 		final Ref mainRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE,
 				"refs/heads/main", ObjectId.fromString(
@@ -230,4 +218,30 @@
 		assertThat(refMap, not(hasKey("refs/heads/sym1")));
 		assertThat(refMap, not(hasKey("refs/heads/sym2")));
 	}
+
+	@Test
+	public void testUpdateWithSymRefsFillInHead() {
+		final String oidName = "0000000000000000000000000000000000000001";
+		final Ref advertised = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK,
+				Constants.HEAD, ObjectId.fromString(oidName));
+
+		final Map<String, Ref> refMap = new HashMap<>();
+		refMap.put(advertised.getName(), advertised);
+
+		final Map<String, String> symRefs = new HashMap<>();
+		symRefs.put("HEAD", "refs/heads/main");
+
+		BasePackConnection.updateWithSymRefs(refMap, symRefs);
+
+		assertThat(refMap, hasKey("HEAD"));
+		assertThat(refMap, hasKey("refs/heads/main"));
+		final Ref headRef = refMap.get("HEAD");
+		final Ref mainRef = refMap.get("refs/heads/main");
+		assertThat(headRef, instanceOf(SymbolicRef.class));
+		final SymbolicRef headSymRef = (SymbolicRef) headRef;
+		assertEquals(Constants.HEAD, headSymRef.getName());
+		assertSame(mainRef, headSymRef.getTarget());
+		assertEquals(oidName, headRef.getObjectId().name());
+		assertEquals(oidName, mainRef.getObjectId().name());
+	}
 }
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java
index 07c236d..60b8098 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java
@@ -29,7 +29,7 @@
 import org.eclipse.jgit.errors.TooLargeObjectInPackException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.file.ObjectDirectoryPackParser;
-import org.eclipse.jgit.internal.storage.file.PackFile;
+import org.eclipse.jgit.internal.storage.file.Pack;
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
@@ -63,16 +63,16 @@
 		try (InputStream is = new FileInputStream(packFile)) {
 			ObjectDirectoryPackParser p = (ObjectDirectoryPackParser) index(is);
 			p.parse(NullProgressMonitor.INSTANCE);
-			PackFile file = p.getPackFile();
+			Pack pack = p.getPack();
 
-			assertTrue(file.hasObject(ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904")));
-			assertTrue(file.hasObject(ObjectId.fromString("540a36d136cf413e4b064c2b0e0a4db60f77feab")));
-			assertTrue(file.hasObject(ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259")));
-			assertTrue(file.hasObject(ObjectId.fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3")));
-			assertTrue(file.hasObject(ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7")));
-			assertTrue(file.hasObject(ObjectId.fromString("902d5476fa249b7abc9d84c611577a81381f0327")));
-			assertTrue(file.hasObject(ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035")));
-			assertTrue(file.hasObject(ObjectId.fromString("c59759f143fb1fe21c197981df75a7ee00290799")));
+			assertTrue(pack.hasObject(ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904")));
+			assertTrue(pack.hasObject(ObjectId.fromString("540a36d136cf413e4b064c2b0e0a4db60f77feab")));
+			assertTrue(pack.hasObject(ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259")));
+			assertTrue(pack.hasObject(ObjectId.fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3")));
+			assertTrue(pack.hasObject(ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7")));
+			assertTrue(pack.hasObject(ObjectId.fromString("902d5476fa249b7abc9d84c611577a81381f0327")));
+			assertTrue(pack.hasObject(ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035")));
+			assertTrue(pack.hasObject(ObjectId.fromString("c59759f143fb1fe21c197981df75a7ee00290799")));
 		}
 	}
 
@@ -88,20 +88,20 @@
 		try (InputStream is = new FileInputStream(packFile)) {
 			ObjectDirectoryPackParser p = (ObjectDirectoryPackParser) index(is);
 			p.parse(NullProgressMonitor.INSTANCE);
-			PackFile file = p.getPackFile();
+			Pack pack = p.getPack();
 
-			assertTrue(file.hasObject(ObjectId.fromString("02ba32d3649e510002c21651936b7077aa75ffa9")));
-			assertTrue(file.hasObject(ObjectId.fromString("0966a434eb1a025db6b71485ab63a3bfbea520b6")));
-			assertTrue(file.hasObject(ObjectId.fromString("09efc7e59a839528ac7bda9fa020dc9101278680")));
-			assertTrue(file.hasObject(ObjectId.fromString("0a3d7772488b6b106fb62813c4d6d627918d9181")));
-			assertTrue(file.hasObject(ObjectId.fromString("1004d0d7ac26fbf63050a234c9b88a46075719d3")));
-			assertTrue(file.hasObject(ObjectId.fromString("10da5895682013006950e7da534b705252b03be6")));
-			assertTrue(file.hasObject(ObjectId.fromString("1203b03dc816ccbb67773f28b3c19318654b0bc8")));
-			assertTrue(file.hasObject(ObjectId.fromString("15fae9e651043de0fd1deef588aa3fbf5a7a41c6")));
-			assertTrue(file.hasObject(ObjectId.fromString("16f9ec009e5568c435f473ba3a1df732d49ce8c3")));
-			assertTrue(file.hasObject(ObjectId.fromString("1fd7d579fb6ae3fe942dc09c2c783443d04cf21e")));
-			assertTrue(file.hasObject(ObjectId.fromString("20a8ade77639491ea0bd667bf95de8abf3a434c8")));
-			assertTrue(file.hasObject(ObjectId.fromString("2675188fd86978d5bc4d7211698b2118ae3bf658")));
+			assertTrue(pack.hasObject(ObjectId.fromString("02ba32d3649e510002c21651936b7077aa75ffa9")));
+			assertTrue(pack.hasObject(ObjectId.fromString("0966a434eb1a025db6b71485ab63a3bfbea520b6")));
+			assertTrue(pack.hasObject(ObjectId.fromString("09efc7e59a839528ac7bda9fa020dc9101278680")));
+			assertTrue(pack.hasObject(ObjectId.fromString("0a3d7772488b6b106fb62813c4d6d627918d9181")));
+			assertTrue(pack.hasObject(ObjectId.fromString("1004d0d7ac26fbf63050a234c9b88a46075719d3")));
+			assertTrue(pack.hasObject(ObjectId.fromString("10da5895682013006950e7da534b705252b03be6")));
+			assertTrue(pack.hasObject(ObjectId.fromString("1203b03dc816ccbb67773f28b3c19318654b0bc8")));
+			assertTrue(pack.hasObject(ObjectId.fromString("15fae9e651043de0fd1deef588aa3fbf5a7a41c6")));
+			assertTrue(pack.hasObject(ObjectId.fromString("16f9ec009e5568c435f473ba3a1df732d49ce8c3")));
+			assertTrue(pack.hasObject(ObjectId.fromString("1fd7d579fb6ae3fe942dc09c2c783443d04cf21e")));
+			assertTrue(pack.hasObject(ObjectId.fromString("20a8ade77639491ea0bd667bf95de8abf3a434c8")));
+			assertTrue(pack.hasObject(ObjectId.fromString("2675188fd86978d5bc4d7211698b2118ae3bf658")));
 			// and lots more...
 		}
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PacketLineInTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PacketLineInTest.java
index 7f03357..505e008 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PacketLineInTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PacketLineInTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009, Google Inc. and others
+ * Copyright (C) 2009, 2020 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
@@ -13,6 +13,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -297,6 +298,58 @@
 		}
 	}
 
+	// parseACKv2
+
+	@Test
+	public void testParseAckV2_NAK() throws IOException {
+		final ObjectId expid = ObjectId
+				.fromString("fcfcfb1fd94829c1a1704f894fc111d14770d34e");
+		final MutableObjectId actid = new MutableObjectId();
+		actid.fromString(expid.name());
+
+		assertSame(PacketLineIn.AckNackResult.NAK,
+				PacketLineIn.parseACKv2("NAK", actid));
+		assertEquals(expid, actid);
+	}
+
+	@Test
+	public void testParseAckV2_ACK() throws IOException {
+		final ObjectId expid = ObjectId
+				.fromString("fcfcfb1fd94829c1a1704f894fc111d14770d34e");
+		final MutableObjectId actid = new MutableObjectId();
+
+		assertSame(PacketLineIn.AckNackResult.ACK_COMMON,
+				PacketLineIn.parseACKv2(
+						"ACK fcfcfb1fd94829c1a1704f894fc111d14770d34e", actid));
+		assertEquals(expid, actid);
+	}
+
+	@Test
+	public void testParseAckV2_Ready() throws IOException {
+		final ObjectId expid = ObjectId
+				.fromString("fcfcfb1fd94829c1a1704f894fc111d14770d34e");
+		final MutableObjectId actid = new MutableObjectId();
+		actid.fromString(expid.name());
+
+		assertSame(PacketLineIn.AckNackResult.ACK_READY,
+				PacketLineIn.parseACKv2("ready", actid));
+		assertEquals(expid, actid);
+	}
+
+	@Test
+	public void testParseAckV2_ERR() {
+		IOException e = assertThrows(IOException.class, () -> PacketLineIn
+				.parseACKv2("ERR want is not valid", new MutableObjectId()));
+		assertTrue(e.getMessage().contains("want is not valid"));
+	}
+
+	@Test
+	public void testParseAckV2_Invalid() {
+		IOException e = assertThrows(IOException.class,
+				() -> PacketLineIn.parseACKv2("HELO", new MutableObjectId()));
+		assertTrue(e.getMessage().contains("xpected ACK/NAK"));
+	}
+
 	// test support
 
 	private void init(String msg) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransferConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransferConfigTest.java
new file mode 100644
index 0000000..d9b85fb
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransferConfigTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 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.assertNull;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+/**
+ * Tests for {@link TransferConfig} parsing.
+ */
+public class TransferConfigTest {
+
+	@Test
+	public void testParseProtocolV0() {
+		Config rc = new Config();
+		rc.setInt("protocol", null, "version", 0);
+		TransferConfig tc = new TransferConfig(rc);
+		assertEquals(TransferConfig.ProtocolVersion.V0, tc.protocolVersion);
+	}
+
+	@Test
+	public void testParseProtocolV1() {
+		Config rc = new Config();
+		rc.setInt("protocol", null, "version", 1);
+		TransferConfig tc = new TransferConfig(rc);
+		assertEquals(TransferConfig.ProtocolVersion.V0, tc.protocolVersion);
+	}
+
+	@Test
+	public void testParseProtocolV2() {
+		Config rc = new Config();
+		rc.setInt("protocol", null, "version", 2);
+		TransferConfig tc = new TransferConfig(rc);
+		assertEquals(TransferConfig.ProtocolVersion.V2, tc.protocolVersion);
+	}
+
+	@Test
+	public void testParseProtocolNotSet() {
+		Config rc = new Config();
+		TransferConfig tc = new TransferConfig(rc);
+		assertNull(tc.protocolVersion);
+	}
+
+	@Test
+	public void testParseProtocolUnknown() {
+		Config rc = new Config();
+		rc.setInt("protocol", null, "version", 3);
+		TransferConfig tc = new TransferConfig(rc);
+		assertNull(tc.protocolVersion);
+	}
+
+	@Test
+	public void testParseProtocolInvalid() {
+		Config rc = new Config();
+		rc.setString("protocol", null, "version", "foo");
+		TransferConfig tc = new TransferConfig(rc);
+		assertNull(tc.protocolVersion);
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IOTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IOTest.java
new file mode 100644
index 0000000..10a858f
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IOTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+public class IOTest {
+
+	private static final byte[] DATA = "abcdefghijklmnopqrstuvwxyz"
+			.getBytes(StandardCharsets.US_ASCII);
+
+	private byte[] initBuffer(int size) {
+		byte[] buffer = new byte[size];
+		for (int i = 0; i < size; i++) {
+			buffer[i] = (byte) ('0' + (i % 10));
+		}
+		return buffer;
+	}
+
+	private int read(byte[] buffer, int from) throws IOException {
+		try (InputStream in = new ByteArrayInputStream(DATA)) {
+			return IO.readFully(in, buffer, from);
+		}
+	}
+
+	@Test
+	public void readFullyBufferShorter() throws Exception {
+		byte[] buffer = initBuffer(9);
+		int length = read(buffer, 0);
+		assertEquals(buffer.length, length);
+		assertArrayEquals(buffer, Arrays.copyOfRange(DATA, 0, length));
+	}
+
+	@Test
+	public void readFullyBufferLonger() throws Exception {
+		byte[] buffer = initBuffer(50);
+		byte[] initial = Arrays.copyOf(buffer, buffer.length);
+		int length = read(buffer, 0);
+		assertEquals(DATA.length, length);
+		assertArrayEquals(Arrays.copyOfRange(buffer, 0, length), DATA);
+		assertArrayEquals(Arrays.copyOfRange(buffer, length, buffer.length),
+				Arrays.copyOfRange(initial, length, initial.length));
+	}
+
+	@Test
+	public void readFullyBufferShorterOffset() throws Exception {
+		byte[] buffer = initBuffer(9);
+		byte[] initial = Arrays.copyOf(buffer, buffer.length);
+		int length = read(buffer, 6);
+		assertEquals(3, length);
+		assertArrayEquals(Arrays.copyOfRange(buffer, 0, 6),
+				Arrays.copyOfRange(initial, 0, 6));
+		assertArrayEquals(Arrays.copyOfRange(buffer, 6, buffer.length),
+				Arrays.copyOfRange(DATA, 0, 3));
+	}
+
+	@Test
+	public void readFullyBufferLongerOffset() throws Exception {
+		byte[] buffer = initBuffer(50);
+		byte[] initial = Arrays.copyOf(buffer, buffer.length);
+		int length = read(buffer, 40);
+		assertEquals(10, length);
+		assertArrayEquals(Arrays.copyOfRange(buffer, 0, 40),
+				Arrays.copyOfRange(initial, 0, 40));
+		assertArrayEquals(Arrays.copyOfRange(buffer, 40, buffer.length),
+				Arrays.copyOfRange(DATA, 0, 10));
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/TemporaryBufferTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/TemporaryBufferTest.java
index 4e65ca7..01dcde2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/TemporaryBufferTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/TemporaryBufferTest.java
@@ -406,4 +406,69 @@
 			}
 		}
 	}
+
+	@Test
+	public void testHeapToByteArrayWithLimit() throws IOException {
+		int sz = 2 * Block.SZ;
+		try (TemporaryBuffer b = new TemporaryBuffer.Heap(sz / 2, sz)) {
+			for (int i = 0; i < sz; i++) {
+				b.write('a' + i % 26);
+			}
+			byte[] prefix = b.toByteArray(5);
+			assertEquals(5, prefix.length);
+			for (int i = 0; i < prefix.length; i++) {
+				assertEquals('a' + i % 26, prefix[i]);
+			}
+			prefix = b.toByteArray(Block.SZ + 37);
+			assertEquals(Block.SZ + 37, prefix.length);
+			for (int i = 0; i < prefix.length; i++) {
+				assertEquals('a' + i % 26, prefix[i]);
+			}
+			prefix = b.toByteArray(sz);
+			assertEquals(sz, prefix.length);
+			for (int i = 0; i < prefix.length; i++) {
+				assertEquals('a' + i % 26, prefix[i]);
+			}
+			prefix = b.toByteArray(sz + 37);
+			assertEquals(sz, prefix.length);
+			for (int i = 0; i < prefix.length; i++) {
+				assertEquals('a' + i % 26, prefix[i]);
+			}
+		}
+	}
+
+	@Test
+	public void testFileToByteArrayWithLimit() throws IOException {
+		@SuppressWarnings("resource") // Buffer is explicitly destroyed in finally block
+		TemporaryBuffer b = new TemporaryBuffer.LocalFile(null, 2 * Block.SZ);
+		int sz = 3 * Block.SZ;
+		try {
+			for (int i = 0; i < sz; i++) {
+				b.write('a' + i % 26);
+			}
+			b.close();
+			byte[] prefix = b.toByteArray(5);
+			assertEquals(5, prefix.length);
+			for (int i = 0; i < prefix.length; i++) {
+				assertEquals('a' + i % 26, prefix[i]);
+			}
+			prefix = b.toByteArray(Block.SZ + 37);
+			assertEquals(Block.SZ + 37, prefix.length);
+			for (int i = 0; i < prefix.length; i++) {
+				assertEquals('a' + i % 26, prefix[i]);
+			}
+			prefix = b.toByteArray(sz);
+			assertEquals(sz, prefix.length);
+			for (int i = 0; i < prefix.length; i++) {
+				assertEquals('a' + i % 26, prefix[i]);
+			}
+			prefix = b.toByteArray(sz + 37);
+			assertEquals(sz, prefix.length);
+			for (int i = 0; i < prefix.length; i++) {
+				assertEquals('a' + i % 26, prefix[i]);
+			}
+		} finally {
+			b.destroy();
+		}
+	}
 }
diff --git a/org.eclipse.jgit.ui/META-INF/MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
index 0d63662..89c1102 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Export-Package: org.eclipse.jgit.awtui;version="5.10.1"
-Import-Package: org.eclipse.jgit.errors;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.lib;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.nls;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revplot;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.revwalk;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.transport;version="[5.10.1,5.11.0)",
- org.eclipse.jgit.util;version="[5.10.1,5.11.0)"
+Export-Package: org.eclipse.jgit.awtui;version="5.11.2"
+Import-Package: org.eclipse.jgit.errors;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.lib;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.nls;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revplot;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.revwalk;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.transport;version="[5.11.2,5.12.0)",
+ org.eclipse.jgit.util;version="[5.11.2,5.12.0)"
diff --git a/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF
index ede7709..95ea630 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ui;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ui;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit.ui/pom.xml b/org.eclipse.jgit.ui/pom.xml
index 3a7fd87..2bbccf1 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>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 035ed37..d389ac5 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,56 +1,56 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <component id="org.eclipse.jgit" version="2">
     <resource path="src/org/eclipse/jgit/lib/ConfigConstants.java" type="org.eclipse.jgit.lib.ConfigConstants">
-        <filter id="1141899266">
+        <filter id="338755678">
             <message_arguments>
-                <message_argument value="5.9"/>
-                <message_argument value="5.10"/>
-                <message_argument value="CONFIG_PROTOCOL_SECTION"/>
+                <message_argument value="org.eclipse.jgit.lib.ConfigConstants"/>
+                <message_argument value="CONFIG_REFSTORAGE_REFTREE"/>
             </message_arguments>
         </filter>
     </resource>
-    <resource path="src/org/eclipse/jgit/lib/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter">
-        <filter id="404000815">
+    <resource path="src/org/eclipse/jgit/revwalk/ObjectWalk.java" type="org.eclipse.jgit.revwalk.ObjectWalk">
+        <filter id="421654647">
             <message_arguments>
-                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
-                <message_argument value="getPath(Config, String, String, String, FS, File, Path)"/>
+                <message_argument value="org.eclipse.jgit.revwalk.ObjectWalk"/>
+                <message_argument value="createObjectReachabilityChecker()"/>
             </message_arguments>
         </filter>
     </resource>
-    <resource path="src/org/eclipse/jgit/storage/pack/PackStatistics.java" type="org.eclipse.jgit.storage.pack.PackStatistics$Accumulator">
-        <filter id="336658481">
+    <resource path="src/org/eclipse/jgit/revwalk/RevWalk.java" type="org.eclipse.jgit.revwalk.RevWalk">
+        <filter id="421654647">
             <message_arguments>
-                <message_argument value="org.eclipse.jgit.storage.pack.PackStatistics.Accumulator"/>
-                <message_argument value="notAdvertisedWants"/>
-            </message_arguments>
-        </filter>
-        <filter id="336658481">
-            <message_arguments>
-                <message_argument value="org.eclipse.jgit.storage.pack.PackStatistics.Accumulator"/>
-                <message_argument value="reachabilityCheckDuration"/>
+                <message_argument value="org.eclipse.jgit.revwalk.RevWalk"/>
+                <message_argument value="createReachabilityChecker()"/>
             </message_arguments>
         </filter>
     </resource>
-    <resource path="src/org/eclipse/jgit/transport/HttpConfig.java" type="org.eclipse.jgit.transport.HttpConfig">
-        <filter id="336658481">
+    <resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS">
+        <filter id="338792546">
             <message_arguments>
-                <message_argument value="org.eclipse.jgit.transport.HttpConfig"/>
-                <message_argument value="EXTRA_HEADER"/>
+                <message_argument value="org.eclipse.jgit.util.FS"/>
+                <message_argument value="internalRunHookIfPresent(Repository, String, String[], PrintStream, PrintStream, String)"/>
             </message_arguments>
         </filter>
-        <filter id="336658481">
+        <filter id="338792546">
             <message_arguments>
-                <message_argument value="org.eclipse.jgit.transport.HttpConfig"/>
-                <message_argument value="USER_AGENT"/>
+                <message_argument value="org.eclipse.jgit.util.FS"/>
+                <message_argument value="runHookIfPresent(Repository, String, String[], PrintStream, PrintStream, String)"/>
             </message_arguments>
         </filter>
     </resource>
-    <resource path="src/org/eclipse/jgit/transport/TransferConfig.java" type="org.eclipse.jgit.transport.TransferConfig$ProtocolVersion">
-        <filter id="1175453698">
+    <resource path="src/org/eclipse/jgit/util/FS_POSIX.java" type="org.eclipse.jgit.util.FS_POSIX">
+        <filter id="338792546">
             <message_arguments>
-                <message_argument value="5.9"/>
-                <message_argument value="5.10"/>
-                <message_argument value="org.eclipse.jgit.transport.TransferConfig.ProtocolVersion"/>
+                <message_argument value="org.eclipse.jgit.util.FS_POSIX"/>
+                <message_argument value="runHookIfPresent(Repository, String, String[], PrintStream, PrintStream, String)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/util/FS_Win32_Cygwin.java" type="org.eclipse.jgit.util.FS_Win32_Cygwin">
+        <filter id="338792546">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.util.FS_Win32_Cygwin"/>
+                <message_argument value="runHookIfPresent(Repository, String, String[], PrintStream, PrintStream, String)"/>
             </message_arguments>
         </filter>
     </resource>
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 886e004..9bcca38 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: 5.10.1.qualifier
+Bundle-Version: 5.11.2.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Eclipse-ExtensibleAPI: true
-Export-Package: org.eclipse.jgit.annotations;version="5.10.1",
- org.eclipse.jgit.api;version="5.10.1";
+Export-Package: org.eclipse.jgit.annotations;version="5.11.2",
+ org.eclipse.jgit.api;version="5.11.2";
   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="5.10.1";
+ org.eclipse.jgit.api.errors;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.errors",
- org.eclipse.jgit.attributes;version="5.10.1";
+ org.eclipse.jgit.attributes;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.blame;version="5.10.1";
+ org.eclipse.jgit.blame;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.diff",
- org.eclipse.jgit.diff;version="5.10.1";
+ org.eclipse.jgit.diff;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.revwalk,
@@ -42,47 +42,44 @@
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.dircache;version="5.10.1";
+ org.eclipse.jgit.dircache;version="5.11.2";
   uses:="org.eclipse.jgit.events,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.errors;version="5.10.1";
+ org.eclipse.jgit.errors;version="5.11.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.internal.storage.pack",
- org.eclipse.jgit.events;version="5.10.1";
+ org.eclipse.jgit.events;version="5.11.2";
   uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.fnmatch;version="5.10.1",
- org.eclipse.jgit.gitrepo;version="5.10.1";
+ org.eclipse.jgit.fnmatch;version="5.11.2",
+ org.eclipse.jgit.gitrepo;version="5.11.2";
   uses:="org.xml.sax.helpers,
    org.eclipse.jgit.api,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.xml.sax",
- org.eclipse.jgit.gitrepo.internal;version="5.10.1";x-internal:=true,
- org.eclipse.jgit.hooks;version="5.10.1";uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.ignore;version="5.10.1",
- org.eclipse.jgit.ignore.internal;version="5.10.1";
+ org.eclipse.jgit.gitrepo.internal;version="5.11.2";x-internal:=true,
+ org.eclipse.jgit.hooks;version="5.11.2";uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.ignore;version="5.11.2",
+ org.eclipse.jgit.ignore.internal;version="5.11.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal;version="5.10.1";
+ org.eclipse.jgit.internal;version="5.11.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.http.test",
- org.eclipse.jgit.internal.fsck;version="5.10.1";
+ org.eclipse.jgit.internal.fsck;version="5.11.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.ketch;version="5.10.1";
-  x-friends:="org.eclipse.jgit.junit,
-   org.eclipse.jgit.test,
-   org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.revwalk;version="5.10.1";x-internal:=true,
- org.eclipse.jgit.internal.storage.dfs;version="5.10.1";
+ org.eclipse.jgit.internal.revwalk;version="5.11.2";
+  x-friends:="org.eclipse.jgit.test",
+ org.eclipse.jgit.internal.storage.dfs;version="5.11.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.http.server,
    org.eclipse.jgit.http.test,
    org.eclipse.jgit.lfs.test",
- org.eclipse.jgit.internal.storage.file;version="5.10.1";
+ org.eclipse.jgit.internal.storage.file;version="5.11.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.junit.http,
@@ -91,35 +88,31 @@
    org.eclipse.jgit.pgm,
    org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.ssh.apache",
- org.eclipse.jgit.internal.storage.io;version="5.10.1";
+ org.eclipse.jgit.internal.storage.io;version="5.11.2";
   x-friends:="org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.pack;version="5.10.1";
+ org.eclipse.jgit.internal.storage.pack;version="5.11.2";
   x-friends:="org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.reftable;version="5.10.1";
+ org.eclipse.jgit.internal.storage.reftable;version="5.11.2";
   x-friends:="org.eclipse.jgit.http.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.reftree;version="5.10.1";
-  x-friends:="org.eclipse.jgit.junit,
-   org.eclipse.jgit.test,
-   org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.submodule;version="5.10.1";x-internal:=true,
- org.eclipse.jgit.internal.transport.connectivity;version="5.10.1";
+ org.eclipse.jgit.internal.submodule;version="5.11.2";x-internal:=true,
+ org.eclipse.jgit.internal.transport.connectivity;version="5.11.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.http;version="5.10.1";
+ org.eclipse.jgit.internal.transport.http;version="5.11.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.parser;version="5.10.1";
+ org.eclipse.jgit.internal.transport.parser;version="5.11.2";
   x-friends:="org.eclipse.jgit.http.server,
    org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.ssh;version="5.10.1";
+ org.eclipse.jgit.internal.transport.ssh;version="5.11.2";
   x-friends:="org.eclipse.jgit.ssh.apache,
    org.eclipse.jgit.ssh.jsch",
- org.eclipse.jgit.lib;version="5.10.1";
+ org.eclipse.jgit.lib;version="5.11.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.util.sha1,
    org.eclipse.jgit.dircache,
@@ -133,10 +126,10 @@
    org.eclipse.jgit.util,
    org.eclipse.jgit.submodule,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.lib.internal;version="5.10.1";
+ org.eclipse.jgit.lib.internal;version="5.11.2";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.logging;version="5.10.1",
- org.eclipse.jgit.merge;version="5.10.1";
+ org.eclipse.jgit.logging;version="5.11.2",
+ org.eclipse.jgit.merge;version="5.11.2";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
@@ -145,40 +138,40 @@
    org.eclipse.jgit.util,
    org.eclipse.jgit.api,
    org.eclipse.jgit.attributes",
- org.eclipse.jgit.nls;version="5.10.1",
- org.eclipse.jgit.notes;version="5.10.1";
+ org.eclipse.jgit.nls;version="5.11.2",
+ org.eclipse.jgit.notes;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.patch;version="5.10.1";
+ org.eclipse.jgit.patch;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.diff",
- org.eclipse.jgit.revplot;version="5.10.1";
+ org.eclipse.jgit.revplot;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk",
- org.eclipse.jgit.revwalk;version="5.10.1";
+ org.eclipse.jgit.revwalk;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.diff,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.revwalk.filter,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.revwalk.filter;version="5.10.1";
+ org.eclipse.jgit.revwalk.filter;version="5.11.2";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.util",
- org.eclipse.jgit.storage.file;version="5.10.1";
+ org.eclipse.jgit.storage.file;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.util",
- org.eclipse.jgit.storage.pack;version="5.10.1";
+ org.eclipse.jgit.storage.pack;version="5.11.2";
   uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.submodule;version="5.10.1";
+ org.eclipse.jgit.submodule;version="5.11.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.diff,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.transport;version="5.10.1";
+ org.eclipse.jgit.transport;version="5.11.2";
   uses:="javax.crypto,
    org.eclipse.jgit.util.io,
    org.eclipse.jgit.lib,
@@ -191,21 +184,21 @@
    org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.storage.pack,
    org.eclipse.jgit.errors",
- org.eclipse.jgit.transport.http;version="5.10.1";
+ org.eclipse.jgit.transport.http;version="5.11.2";
   uses:="javax.net.ssl",
- org.eclipse.jgit.transport.resolver;version="5.10.1";
+ org.eclipse.jgit.transport.resolver;version="5.11.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.lib",
- org.eclipse.jgit.treewalk;version="5.10.1";
+ org.eclipse.jgit.treewalk;version="5.11.2";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.util",
- org.eclipse.jgit.treewalk.filter;version="5.10.1";
+ org.eclipse.jgit.treewalk.filter;version="5.11.2";
   uses:="org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util;version="5.10.1";
+ org.eclipse.jgit.util;version="5.11.2";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.hooks,
    org.eclipse.jgit.revwalk,
@@ -218,12 +211,12 @@
    org.eclipse.jgit.treewalk,
    javax.net.ssl,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.util.io;version="5.10.1";
+ org.eclipse.jgit.util.io;version="5.11.2";
   uses:="org.eclipse.jgit.attributes,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util.sha1;version="5.10.1",
- org.eclipse.jgit.util.time;version="5.10.1"
+ org.eclipse.jgit.util.sha1;version="5.11.2",
+ org.eclipse.jgit.util.time;version="5.11.2"
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
  javax.crypto,
diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
index d6f1f52..7764fe1 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: 5.10.1.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit;version="5.10.1.qualifier";roots="."
+Bundle-Version: 5.11.2.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit;version="5.11.2.qualifier";roots="."
diff --git a/org.eclipse.jgit/findBugs/FindBugsExcludeFilter.xml b/org.eclipse.jgit/findBugs/FindBugsExcludeFilter.xml
index 2efbb9c..73a6685 100644
--- a/org.eclipse.jgit/findBugs/FindBugsExcludeFilter.xml
+++ b/org.eclipse.jgit/findBugs/FindBugsExcludeFilter.xml
@@ -9,6 +9,12 @@
        <Bug pattern="DM_GC" />
      </Match>
 
+     <Match>
+       <Class name="org.eclipse.jgit.internal.storage.pack.PackOutputStream" />
+       <Method name="writeHeader" />
+       <Bug pattern="NS_DANGEROUS_NON_SHORT_CIRCUIT" />
+     </Match>
+
      <!-- Silence ignoring return value of mkdirs -->
      <Match>
        <Class name="org.eclipse.jgit.dircache.DirCacheCheckout" />
@@ -19,8 +25,26 @@
 	   <!-- Silence the construction of our magic String instance.
 	     -->
      <Match>
-	 <Class name="org.eclipse.jgit.lib.Config" />
-	 <Bug pattern="DM_STRING_VOID_CTOR"/>
+       <Class name="org.eclipse.jgit.lib.Config" />
+       <Bug pattern="DM_STRING_VOID_CTOR"/>
+     </Match>
+
+     <Match>
+       <Class name="org.eclipse.jgit.lib.Config" />
+       <Method name="isMissing" />
+       <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ"/>
+     </Match>
+
+     <Match>
+       <Class name="org.eclipse.jgit.transport.PacketLineIn" />
+       <Method name="isDelimiter" />
+       <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ"/>
+     </Match>
+
+     <Match>
+       <Class name="org.eclipse.jgit.transport.PacketLineIn" />
+       <Method name="isEnd" />
+       <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ"/>
      </Match>
 
      <!-- Silence comparison of string by == or !=.  This class is built
@@ -53,6 +77,12 @@
        <Bug pattern="NP_BOOLEAN_RETURN_NULL" />
      </Match>
 
+     <Match>
+       <Class name="org.eclipse.jgit.ignore.IgnoreNode" />
+       <Method name="checkIgnored" />
+       <Bug pattern="NP_BOOLEAN_RETURN_NULL" />
+     </Match>
+
      <!-- Transport initialization works like this -->
      <Match>
        <Class name="org.eclipse.jgit.transport.Transport" />
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index 4f17be5..6a52c27 100644
--- a/org.eclipse.jgit/pom.xml
+++ b/org.eclipse.jgit/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>5.10.1-SNAPSHOT</version>
+    <version>5.11.2-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 861ebca..b6b10ea 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -30,6 +30,8 @@
 badEntryName=Bad entry name: {0}
 badEscape=Bad escape: {0}
 badGroupHeader=Bad group header
+badIgnorePattern=Cannot parse .gitignore pattern ''{0}''
+badIgnorePatternFull=File {0} line {1}: cannot parse pattern ''{2}'': {3}
 badObjectType=Bad object type: {0}
 badRef=Bad ref: {0}: {1}
 badSectionEntry=Bad section entry: {0}
@@ -233,6 +235,7 @@
 downloadCancelledDuringIndexing=Download cancelled during indexing
 duplicateAdvertisementsOf=duplicate advertisements of {0}
 duplicateRef=Duplicate ref: {0}
+duplicateRefAttribute=Duplicate ref attribute: {0}
 duplicateRemoteRefUpdateIsIllegal=Duplicate remote ref update is illegal. Affected remote name: {0}
 duplicateStagesNotAllowed=Duplicate stages not allowed
 eitherGitDirOrWorkTreeRequired=One of setGitDir or setWorkTree must be called.
@@ -310,6 +313,10 @@
 hoursAgo={0} hours ago
 httpConfigCannotNormalizeURL=Cannot normalize URL path {0}: too many .. segments
 httpConfigInvalidURL=Cannot parse URL from subsection http.{0} in git config; ignored.
+httpFactoryInUse=Changing the HTTP connection factory after an HTTP connection has already been opened is not allowed.
+httpPreAuthTooLate=HTTP Basic preemptive authentication cannot be set once an HTTP connection has already been opened.
+httpUserInfoDecodeError=Cannot decode user info from URL {}; ignored.
+httpWrongConnectionType=Wrong connection type: expected {0}, got {1}.
 hugeIndexesAreNotSupportedByJgitYet=Huge indexes are not supported by jgit, yet
 hunkBelongsToAnotherFile=Hunk belongs to another file
 hunkDisconnectedFromFile=Hunk disconnected from file
@@ -526,6 +533,7 @@
 peerDidNotSupplyACompleteObjectGraph=peer did not supply a complete object graph
 personIdentEmailNonNull=E-mail address of PersonIdent must not be null.
 personIdentNameNonNull=Name of PersonIdent must not be null.
+postCommitHookFailed=Execution of post-commit hook failed: {0}.
 prefixRemote=remote:
 problemWithResolvingPushRefSpecsLocally=Problem with resolving push ref specs locally: {0}
 progressMonUploading=Uploading {0}
@@ -567,6 +575,7 @@
 reftableDirExists=reftable dir exists and is nonempty
 reftableRecordsMustIncrease=records must be increasing: last {0}, this {1}
 refUpdateReturnCodeWas=RefUpdate return code was: {0}
+remoteBranchNotFound=Remote branch ''{0}'' not found in upstream origin
 remoteConfigHasNoURIAssociated=Remote config "{0}" has no URIs associated
 remoteDoesNotHaveSpec=Remote does not have {0} available for fetch.
 remoteDoesNotSupportSmartHTTPPush=remote does not support smart HTTP push
@@ -621,7 +630,9 @@
 shortReadOfBlock=Short read of block.
 shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section.
 shortSkipOfBlock=Short skip of block.
-signingNotSupportedOnTag=Signing isn't supported on tag operations yet.
+signatureVerificationError=Signature verification failed
+signatureVerificationUnavailable=No signature verifier registered
+signedTagMessageNoLf=A non-empty message of a signed tag must end in LF.
 signingServiceUnavailable=Signing service is not available
 similarityScoreMustBeWithinBounds=Similarity score must be between 0 and 100.
 skipMustBeNonNegative=skip must be >= 0
@@ -737,6 +748,7 @@
 unmergedPaths=Repository contains unmerged paths
 unpackException=Exception while parsing pack stream
 unreadablePackIndex=Unreadable pack index: {0}
+unrecognizedPackExtension=Unrecognized pack extension: {0}
 unrecognizedRef=Unrecognized ref: {0}
 unsetMark=Mark not set
 unsupportedAlternates=Alternates not supported
@@ -762,6 +774,13 @@
 URINotSupported=URI not supported: {0}
 userConfigInvalid=Git config in the user's home directory {0} is invalid {1}
 validatingGitModules=Validating .gitmodules files
+verifySignatureBad=BAD signature from "{0}"
+verifySignatureExpired=Expired signature from "{0}"
+verifySignatureGood=Good signature from "{0}"
+verifySignatureIssuer=issuer "{0}"
+verifySignatureKey=using key {0}
+verifySignatureMade=Signature made {0}
+verifySignatureTrust=[{0}]
 walkFailure=Walk failure.
 wantNoSpaceWithCapabilities=No space between oid and first capability in first want line
 wantNotValid=want {0} not valid
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties
deleted file mode 100644
index 1fbb7cb..0000000
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties
+++ /dev/null
@@ -1,13 +0,0 @@
-accepted=accepted.
-cannotFetchFromLocalReplica=cannot fetch from LocalReplica
-failed=failed!
-invalidFollowerUri=invalid follower URI
-leaderFailedToStore=leader failed to store
-localReplicaRequired=LocalReplica instance is required
-mismatchedTxnNamespace=mismatched txnNamespace; expected {0} found {1}
-outsideTxnNamespace=ref {0} is outside of txnNamespace {1}
-proposingUpdates=Proposing updates
-queuedProposalFailedToApply=queued proposal failed to apply
-starting=starting!
-unsupportedVoterCount=unsupported voter count {0}, expected one of {1}
-waitingForQueue=Waiting for queue
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
index 2c01c19..fdf8b80 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
@@ -19,7 +19,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
@@ -202,7 +201,7 @@
 	 * Available archival formats (corresponding to values for
 	 * the --format= option)
 	 */
-	private static final ConcurrentMap<String, FormatEntry> formats =
+	private static final Map<String, FormatEntry> formats =
 			new ConcurrentHashMap<>();
 
 	/**
@@ -215,7 +214,7 @@
 	 * @param newValue value to be associated with the key (null to remove).
 	 * @return true if the value was replaced
 	 */
-	private static <K, V> boolean replace(ConcurrentMap<K, V> map,
+	private static <K, V> boolean replace(Map<K, V> map,
 			K key, V oldValue, V newValue) {
 		if (oldValue == null && newValue == null) // Nothing to do.
 			return true;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
index aba86fc..cf7bc1f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
@@ -297,6 +297,7 @@
 			command.setTagOpt(
 					fetchAll ? TagOpt.FETCH_TAGS : TagOpt.AUTO_FOLLOW);
 		}
+		command.setInitialBranch(branch);
 		configure(command);
 
 		return command.call();
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 b4f7175..7ec36af 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -20,6 +20,7 @@
 import java.util.List;
 
 import org.eclipse.jgit.api.errors.AbortedByHookException;
+import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
 import org.eclipse.jgit.api.errors.EmptyCommitException;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -36,6 +37,8 @@
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.UnmergedPathException;
 import org.eclipse.jgit.hooks.CommitMsgHook;
 import org.eclipse.jgit.hooks.Hooks;
@@ -47,6 +50,7 @@
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.GpgConfig;
 import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.GpgObjectSigner;
 import org.eclipse.jgit.lib.GpgSigner;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -66,6 +70,8 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
 import org.eclipse.jgit.util.ChangeIdUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * A class used to execute a {@code Commit} command. It has setters for all
@@ -77,6 +83,9 @@
  *      >Git documentation about Commit</a>
  */
 public class CommitCommand extends GitCommand<RevCommit> {
+	private static final Logger log = LoggerFactory
+			.getLogger(CommitCommand.class);
+
 	private PersonIdent author;
 
 	private PersonIdent committer;
@@ -120,6 +129,8 @@
 
 	private GpgSigner gpgSigner;
 
+	private GpgConfig gpgConfig;
+
 	private CredentialsProvider credentialsProvider;
 
 	/**
@@ -170,8 +181,7 @@
 
 			if (all && !repo.isBare()) {
 				try (Git git = new Git(repo)) {
-					git.add()
-							.addFilepattern(".") //$NON-NLS-1$
+					git.add().addFilepattern(".") //$NON-NLS-1$
 							.setUpdate(true).call();
 				} catch (NoFilepatternException e) {
 					// should really not happen
@@ -209,7 +219,7 @@
 						.setCommitMessage(message).call();
 			}
 
-			// lock the index
+			RevCommit revCommit;
 			DirCache index = repo.lockDirCache();
 			try (ObjectInserter odi = repo.newObjectInserter()) {
 				if (!only.isEmpty())
@@ -223,90 +233,37 @@
 				if (insertChangeId)
 					insertChangeId(indexTreeId);
 
-				// Check for empty commits
-				if (headId != null && !allowEmpty.booleanValue()) {
-					RevCommit headCommit = rw.parseCommit(headId);
-					headCommit.getTree();
-					if (indexTreeId.equals(headCommit.getTree())) {
-						throw new EmptyCommitException(
-								JGitText.get().emptyCommit);
-					}
-				}
+				checkIfEmpty(rw, headId, indexTreeId);
 
 				// Create a Commit object, populate it and write it
 				CommitBuilder commit = new CommitBuilder();
 				commit.setCommitter(committer);
 				commit.setAuthor(author);
 				commit.setMessage(message);
-
 				commit.setParentIds(parents);
 				commit.setTreeId(indexTreeId);
 
 				if (signCommit.booleanValue()) {
-					if (gpgSigner == null) {
-						throw new ServiceUnavailableException(
-								JGitText.get().signingServiceUnavailable);
-					}
-					gpgSigner.sign(commit, signingKey, committer,
-							credentialsProvider);
+					sign(commit);
 				}
 
 				ObjectId commitId = odi.insert(commit);
 				odi.flush();
+				revCommit = rw.parseCommit(commitId);
 
-				RevCommit revCommit = rw.parseCommit(commitId);
-				RefUpdate ru = repo.updateRef(Constants.HEAD);
-				ru.setNewObjectId(commitId);
-				if (!useDefaultReflogMessage) {
-					ru.setRefLogMessage(reflogComment, false);
-				} else {
-					String prefix = amend ? "commit (amend): " //$NON-NLS-1$
-							: parents.isEmpty() ? "commit (initial): " //$NON-NLS-1$
-									: "commit: "; //$NON-NLS-1$
-					ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
-							false);
-				}
-				if (headId != null)
-					ru.setExpectedOldObjectId(headId);
-				else
-					ru.setExpectedOldObjectId(ObjectId.zeroId());
-				Result rc = ru.forceUpdate();
-				switch (rc) {
-				case NEW:
-				case FORCED:
-				case FAST_FORWARD: {
-					setCallable(false);
-					if (state == RepositoryState.MERGING_RESOLVED
-							|| isMergeDuringRebase(state)) {
-						// Commit was successful. Now delete the files
-						// used for merge commits
-						repo.writeMergeCommitMsg(null);
-						repo.writeMergeHeads(null);
-					} else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) {
-						repo.writeMergeCommitMsg(null);
-						repo.writeCherryPickHead(null);
-					} else if (state == RepositoryState.REVERTING_RESOLVED) {
-						repo.writeMergeCommitMsg(null);
-						repo.writeRevertHead(null);
-					}
-					Hooks.postCommit(repo,
-							hookOutRedirect.get(PostCommitHook.NAME),
-							hookErrRedirect.get(PostCommitHook.NAME)).call();
-
-					return revCommit;
-				}
-				case REJECTED:
-				case LOCK_FAILURE:
-					throw new ConcurrentRefUpdateException(
-							JGitText.get().couldNotLockHEAD, ru.getRef(), rc);
-				default:
-					throw new JGitInternalException(MessageFormat.format(
-							JGitText.get().updatingRefFailed, Constants.HEAD,
-							commitId.toString(), rc));
-				}
+				updateRef(state, headId, revCommit, commitId);
 			} finally {
 				index.unlock();
 			}
+			try {
+				Hooks.postCommit(repo, hookOutRedirect.get(PostCommitHook.NAME),
+						hookErrRedirect.get(PostCommitHook.NAME)).call();
+			} catch (Exception e) {
+				log.error(MessageFormat.format(
+						JGitText.get().postCommitHookFailed, e.getMessage()),
+						e);
+			}
+			return revCommit;
 		} catch (UnmergedPathException e) {
 			throw new UnmergedPathsException(e);
 		} catch (IOException e) {
@@ -315,6 +272,89 @@
 		}
 	}
 
+	private void checkIfEmpty(RevWalk rw, ObjectId headId, ObjectId indexTreeId)
+			throws EmptyCommitException, MissingObjectException,
+			IncorrectObjectTypeException, IOException {
+		if (headId != null && !allowEmpty.booleanValue()) {
+			RevCommit headCommit = rw.parseCommit(headId);
+			headCommit.getTree();
+			if (indexTreeId.equals(headCommit.getTree())) {
+				throw new EmptyCommitException(JGitText.get().emptyCommit);
+			}
+		}
+	}
+
+	private void sign(CommitBuilder commit) throws ServiceUnavailableException,
+			CanceledException, UnsupportedSigningFormatException {
+		if (gpgSigner == null) {
+			throw new ServiceUnavailableException(
+					JGitText.get().signingServiceUnavailable);
+		}
+		if (gpgSigner instanceof GpgObjectSigner) {
+			((GpgObjectSigner) gpgSigner).signObject(commit,
+					signingKey, committer, credentialsProvider,
+					gpgConfig);
+		} else {
+			if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) {
+				throw new UnsupportedSigningFormatException(JGitText
+						.get().onlyOpenPgpSupportedForSigning);
+			}
+			gpgSigner.sign(commit, signingKey, committer,
+					credentialsProvider);
+		}
+	}
+
+	private void updateRef(RepositoryState state, ObjectId headId,
+			RevCommit revCommit, ObjectId commitId)
+			throws ConcurrentRefUpdateException, IOException {
+		RefUpdate ru = repo.updateRef(Constants.HEAD);
+		ru.setNewObjectId(commitId);
+		if (!useDefaultReflogMessage) {
+			ru.setRefLogMessage(reflogComment, false);
+		} else {
+			String prefix = amend ? "commit (amend): " //$NON-NLS-1$
+					: parents.isEmpty() ? "commit (initial): " //$NON-NLS-1$
+							: "commit: "; //$NON-NLS-1$
+			ru.setRefLogMessage(prefix + revCommit.getShortMessage(),
+					false);
+		}
+		if (headId != null) {
+			ru.setExpectedOldObjectId(headId);
+		} else {
+			ru.setExpectedOldObjectId(ObjectId.zeroId());
+		}
+		Result rc = ru.forceUpdate();
+		switch (rc) {
+		case NEW:
+		case FORCED:
+		case FAST_FORWARD: {
+			setCallable(false);
+			if (state == RepositoryState.MERGING_RESOLVED
+					|| isMergeDuringRebase(state)) {
+				// Commit was successful. Now delete the files
+				// used for merge commits
+				repo.writeMergeCommitMsg(null);
+				repo.writeMergeHeads(null);
+			} else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) {
+				repo.writeMergeCommitMsg(null);
+				repo.writeCherryPickHead(null);
+			} else if (state == RepositoryState.REVERTING_RESOLVED) {
+				repo.writeMergeCommitMsg(null);
+				repo.writeRevertHead(null);
+			}
+			break;
+		}
+		case REJECTED:
+		case LOCK_FAILURE:
+			throw new ConcurrentRefUpdateException(
+					JGitText.get().couldNotLockHEAD, ru.getRef(), rc);
+		default:
+			throw new JGitInternalException(MessageFormat.format(
+					JGitText.get().updatingRefFailed, Constants.HEAD,
+					commitId.toString(), rc));
+		}
+	}
+
 	private void insertChangeId(ObjectId treeId) {
 		ObjectId firstParentId = null;
 		if (!parents.isEmpty())
@@ -576,7 +616,9 @@
 			// an explicit message
 			throw new NoMessageException(JGitText.get().commitMessageNotSpecified);
 
-		GpgConfig gpgConfig = new GpgConfig(repo.getConfig());
+		if (gpgConfig == null) {
+			gpgConfig = new GpgConfig(repo.getConfig());
+		}
 		if (signCommit == null) {
 			signCommit = gpgConfig.isSignCommits() ? Boolean.TRUE
 					: Boolean.FALSE;
@@ -585,10 +627,6 @@
 			signingKey = gpgConfig.getSigningKey();
 		}
 		if (gpgSigner == null) {
-			if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) {
-				throw new UnsupportedSigningFormatException(
-						JGitText.get().onlyOpenPgpSupportedForSigning);
-			}
 			gpgSigner = GpgSigner.getDefault();
 		}
 	}
@@ -973,6 +1011,36 @@
 	}
 
 	/**
+	 * Sets the {@link GpgSigner} to use if the commit is to be signed.
+	 *
+	 * @param signer
+	 *            to use; if {@code null}, the default signer will be used
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public CommitCommand setGpgSigner(GpgSigner signer) {
+		checkCallable();
+		this.gpgSigner = signer;
+		return this;
+	}
+
+	/**
+	 * Sets an external {@link GpgConfig} to use. Whether it will be used is at
+	 * the discretion of the {@link #setGpgSigner(GpgSigner)}.
+	 *
+	 * @param config
+	 *            to set; if {@code null}, the config will be loaded from the
+	 *            git config of the repository
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public CommitCommand setGpgConfig(GpgConfig config) {
+		checkCallable();
+		this.gpgConfig = config;
+		return this;
+	}
+
+	/**
 	 * Sets a {@link CredentialsProvider}
 	 *
 	 * @param credentialsProvider
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
index 033dd60..90c1515 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
@@ -74,6 +74,8 @@
 
 	private boolean isForceUpdate;
 
+	private String initialBranch;
+
 	/**
 	 * Callback for status of fetch operation.
 	 *
@@ -209,7 +211,7 @@
 			transport.setFetchThin(thin);
 			configure(transport);
 			FetchResult result = transport.fetch(monitor,
-					applyOptions(refSpecs));
+					applyOptions(refSpecs), initialBranch);
 			if (!repo.isBare()) {
 				fetchSubmodules(result);
 			}
@@ -488,6 +490,24 @@
 	}
 
 	/**
+	 * Set the initial branch
+	 *
+	 * @param branch
+	 *            the initial branch to check out when cloning the repository.
+	 *            Can be specified as ref name (<code>refs/heads/master</code>),
+	 *            branch name (<code>master</code>) or tag name
+	 *            (<code>v1.2.3</code>). The default is to use the branch
+	 *            pointed to by the cloned repository's HEAD and can be
+	 *            requested by passing {@code null} or <code>HEAD</code>.
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public FetchCommand setInitialBranch(String branch) {
+		this.initialBranch = branch;
+		return this;
+	}
+
+	/**
 	 * Register a progress callback.
 	 *
 	 * @param callback
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
index 6431477..3b3e10e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
- * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> and others
+ * Copyright (C) 2010, 2021 Chris Aniszczyk <caniszczyk@gmail.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -773,6 +773,16 @@
 	}
 
 	/**
+	 * Return a command to verify signatures of tags or commits.
+	 *
+	 * @return a {@link VerifySignatureCommand}
+	 * @since 5.11
+	 */
+	public VerifySignatureCommand verifySignature() {
+		return new VerifySignatureCommand(repo);
+	}
+
+	/**
 	 * Get repository
 	 *
 	 * @return the git repository this class is interacting with; see
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java
index 41fcf29..240290f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java
@@ -15,12 +15,16 @@
 import java.util.concurrent.Callable;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.InvalidRefNameException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryBuilder;
 import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.SystemReader;
 
 /**
@@ -38,6 +42,8 @@
 
 	private FS fs;
 
+	private String initialBranch;
+
 	/**
 	 * {@inheritDoc}
 	 * <p>
@@ -87,11 +93,16 @@
 					builder.setWorkTree(new File(dStr));
 				}
 			}
+			builder.setInitialBranch(StringUtils.isEmptyOrNull(initialBranch)
+					? SystemReader.getInstance().getUserConfig().getString(
+							ConfigConstants.CONFIG_INIT_SECTION, null,
+							ConfigConstants.CONFIG_KEY_DEFAULT_BRANCH)
+					: initialBranch);
 			Repository repository = builder.build();
 			if (!repository.getObjectDatabase().exists())
 				repository.create(bare);
 			return new Git(repository, true);
-		} catch (IOException e) {
+		} catch (IOException | ConfigInvalidException e) {
 			throw new JGitInternalException(e.getMessage(), e);
 		}
 	}
@@ -184,4 +195,23 @@
 		this.fs = fs;
 		return this;
 	}
+
+	/**
+	 * Set the initial branch of the new repository. If not specified
+	 * ({@code null} or empty), fall back to the default name (currently
+	 * master).
+	 *
+	 * @param branch
+	 *            initial branch name of the new repository
+	 * @return {@code this}
+	 * @throws InvalidRefNameException
+	 *             if the branch name is not valid
+	 *
+	 * @since 5.11
+	 */
+	public InitCommand setInitialBranch(String branch)
+			throws InvalidRefNameException {
+		this.initialBranch = branch;
+		return this;
+	}
 }
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 a4ca309..0c69106 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, Christoph Brill <egore911@egore911.de> and others
+ * Copyright (C) 2011, 2020 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
@@ -164,7 +164,7 @@
 				refSpecs.add(new RefSpec("refs/heads/*:refs/remotes/origin/*")); //$NON-NLS-1$
 			Collection<Ref> refs;
 			Map<String, Ref> refmap = new HashMap<>();
-			try (FetchConnection fc = transport.openFetch()) {
+			try (FetchConnection fc = transport.openFetch(refSpecs)) {
 				refs = fc.getRefs();
 				if (refSpecs.isEmpty())
 					for (Ref r : refs)
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 6678af1..836175d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -67,6 +67,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.revwalk.filter.RevFilter;
 import org.eclipse.jgit.submodule.SubmoduleWalk.IgnoreSubmoduleMode;
@@ -1137,15 +1138,19 @@
 
 	private List<RevCommit> calculatePickList(RevCommit headCommit)
 			throws GitAPIException, NoHeadException, IOException {
-		Iterable<RevCommit> commitsToUse;
-		try (Git git = new Git(repo)) {
-			LogCommand cmd = git.log().addRange(upstreamCommit, headCommit);
-			commitsToUse = cmd.call();
-		}
 		List<RevCommit> cherryPickList = new ArrayList<>();
-		for (RevCommit commit : commitsToUse) {
-			if (preserveMerges || commit.getParentCount() == 1)
-				cherryPickList.add(commit);
+		try (RevWalk r = new RevWalk(repo)) {
+			r.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true);
+			r.sort(RevSort.COMMIT_TIME_DESC, true);
+			r.markUninteresting(r.lookupCommit(upstreamCommit));
+			r.markStart(r.lookupCommit(headCommit));
+			Iterator<RevCommit> commitsToUse = r.iterator();
+			while (commitsToUse.hasNext()) {
+				RevCommit commit = commitsToUse.next();
+				if (preserveMerges || commit.getParentCount() == 1) {
+					cherryPickList.add(commit);
+				}
+			}
 		}
 		Collections.reverse(cherryPickList);
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java
index 9a328a6..58c18b3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, 2013 Chris Aniszczyk <caniszczyk@gmail.com> and others
+ * Copyright (C) 2010, 2020 Chris Aniszczyk <caniszczyk@gmail.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -18,8 +18,14 @@
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.api.errors.NoHeadException;
 import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
+import org.eclipse.jgit.api.errors.ServiceUnavailableException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.GpgObjectSigner;
+import org.eclipse.jgit.lib.GpgSigner;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -31,6 +37,7 @@
 import org.eclipse.jgit.lib.TagBuilder;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.CredentialsProvider;
 
 /**
  * Create/update an annotated tag object or a simple unannotated tag
@@ -56,6 +63,7 @@
  *      >Git documentation about Tag</a>
  */
 public class TagCommand extends GitCommand<Ref> {
+
 	private RevObject id;
 
 	private String name;
@@ -64,11 +72,19 @@
 
 	private PersonIdent tagger;
 
-	private boolean signed;
+	private Boolean signed;
 
 	private boolean forceUpdate;
 
-	private boolean annotated = true;
+	private Boolean annotated;
+
+	private String signingKey;
+
+	private GpgConfig gpgConfig;
+
+	private GpgObjectSigner gpgSigner;
+
+	private CredentialsProvider credentialsProvider;
 
 	/**
 	 * <p>Constructor for TagCommand.</p>
@@ -77,6 +93,7 @@
 	 */
 	protected TagCommand(Repository repo) {
 		super(repo);
+		this.credentialsProvider = CredentialsProvider.getDefault();
 	}
 
 	/**
@@ -108,10 +125,7 @@
 				id = revWalk.parseCommit(objectId);
 			}
 
-			if (!annotated) {
-				if (message != null || tagger != null)
-					throw new JGitInternalException(
-							JGitText.get().messageAndTaggerNotAllowedInUnannotatedTags);
+			if (!isAnnotated()) {
 				return updateTagRef(id, revWalk, name,
 						"SimpleTag[" + name + " : " + id //$NON-NLS-1$ //$NON-NLS-2$
 								+ "]"); //$NON-NLS-1$
@@ -124,6 +138,11 @@
 			newTag.setTagger(tagger);
 			newTag.setObjectId(id);
 
+			if (gpgSigner != null) {
+				gpgSigner.signObject(newTag, signingKey, tagger,
+						credentialsProvider, gpgConfig);
+			}
+
 			// write the tag object
 			try (ObjectInserter inserter = repo.newObjectInserter()) {
 				ObjectId tagId = inserter.insert(newTag);
@@ -158,9 +177,17 @@
 			throw new ConcurrentRefUpdateException(
 					JGitText.get().couldNotLockHEAD, tagRef.getRef(),
 					updateResult);
+		case NO_CHANGE:
+			if (forceUpdate) {
+				return repo.exactRef(refName);
+			}
+			throw new RefAlreadyExistsException(MessageFormat
+					.format(JGitText.get().tagAlreadyExists, newTagToString),
+					updateResult);
 		case REJECTED:
 			throw new RefAlreadyExistsException(MessageFormat.format(
-					JGitText.get().tagAlreadyExists, newTagToString));
+					JGitText.get().tagAlreadyExists, newTagToString),
+					updateResult);
 		default:
 			throw new JGitInternalException(MessageFormat.format(
 					JGitText.get().updatingRefFailed, refName, newTagToString,
@@ -177,20 +204,60 @@
 	 *
 	 * @throws InvalidTagNameException
 	 *             if the tag name is null or invalid
-	 * @throws UnsupportedOperationException
-	 *             if the tag is signed (not supported yet)
+	 * @throws ServiceUnavailableException
+	 *             if the tag should be signed but no signer can be found
+	 * @throws UnsupportedSigningFormatException
+	 *             if the tag should be signed but {@code gpg.format} is not
+	 *             {@link GpgFormat#OPENPGP}
 	 */
 	private void processOptions(RepositoryState state)
-			throws InvalidTagNameException {
-		if (tagger == null && annotated)
-			tagger = new PersonIdent(repo);
-		if (name == null || !Repository.isValidRefName(Constants.R_TAGS + name))
+			throws InvalidTagNameException, ServiceUnavailableException,
+			UnsupportedSigningFormatException {
+		if (name == null
+				|| !Repository.isValidRefName(Constants.R_TAGS + name)) {
 			throw new InvalidTagNameException(
 					MessageFormat.format(JGitText.get().tagNameInvalid,
 							name == null ? "<null>" : name)); //$NON-NLS-1$
-		if (signed)
-			throw new UnsupportedOperationException(
-					JGitText.get().signingNotSupportedOnTag);
+		}
+		if (!isAnnotated()) {
+			if ((message != null && !message.isEmpty()) || tagger != null) {
+				throw new JGitInternalException(JGitText
+						.get().messageAndTaggerNotAllowedInUnannotatedTags);
+			}
+		} else {
+			if (tagger == null) {
+				tagger = new PersonIdent(repo);
+			}
+			// Figure out whether to sign.
+			if (!(Boolean.FALSE.equals(signed) && signingKey == null)) {
+				if (gpgConfig == null) {
+					gpgConfig = new GpgConfig(repo.getConfig());
+				}
+				boolean doSign = isSigned() || gpgConfig.isSignAllTags();
+				if (!Boolean.TRUE.equals(annotated) && !doSign) {
+					doSign = gpgConfig.isSignAnnotated();
+				}
+				if (doSign) {
+					if (signingKey == null) {
+						signingKey = gpgConfig.getSigningKey();
+					}
+					if (gpgSigner == null) {
+						GpgSigner signer = GpgSigner.getDefault();
+						if (!(signer instanceof GpgObjectSigner)) {
+							throw new ServiceUnavailableException(
+									JGitText.get().signingServiceUnavailable);
+						}
+						gpgSigner = (GpgObjectSigner) signer;
+					}
+					// The message of a signed tag must end in a newline because
+					// the signature will be appended.
+					if (message != null && !message.isEmpty()
+							&& !message.endsWith("\n")) { //$NON-NLS-1$
+						message += '\n';
+					}
+				}
+			}
+		}
 	}
 
 	/**
@@ -238,24 +305,61 @@
 	}
 
 	/**
-	 * Whether this tag is signed
+	 * Whether {@link #setSigned(boolean) setSigned(true)} has been called or
+	 * whether a {@link #setSigningKey(String) signing key ID} has been set;
+	 * i.e., whether -s or -u was specified explicitly.
 	 *
 	 * @return whether the tag is signed
 	 */
 	public boolean isSigned() {
-		return signed;
+		return Boolean.TRUE.equals(signed) || signingKey != null;
 	}
 
 	/**
 	 * If set to true the Tag command creates a signed tag object. This
-	 * corresponds to the parameter -s on the command line.
+	 * corresponds to the parameter -s (--sign or --no-sign) on the command
+	 * line.
+	 * <p>
+	 * If {@code true}, the tag will be a signed annotated tag.
+	 * </p>
 	 *
 	 * @param signed
-	 *            a boolean.
+	 *            whether to sign
 	 * @return {@code this}
 	 */
 	public TagCommand setSigned(boolean signed) {
-		this.signed = signed;
+		checkCallable();
+		this.signed = Boolean.valueOf(signed);
+		return this;
+	}
+
+	/**
+	 * Sets the {@link GpgSigner} to use if the commit is to be signed.
+	 *
+	 * @param signer
+	 *            to use; if {@code null}, the default signer will be used
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public TagCommand setGpgSigner(GpgObjectSigner signer) {
+		checkCallable();
+		this.gpgSigner = signer;
+		return this;
+	}
+
+	/**
+	 * Sets an external {@link GpgConfig} to use. Whether it will be used is at
+	 * the discretion of the {@link #setGpgSigner(GpgObjectSigner)}.
+	 *
+	 * @param config
+	 *            to set; if {@code null}, the config will be loaded from the
+	 *            git config of the repository
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public TagCommand setGpgConfig(GpgConfig config) {
+		checkCallable();
+		this.gpgConfig = config;
 		return this;
 	}
 
@@ -268,6 +372,7 @@
 	 * @return {@code this}
 	 */
 	public TagCommand setTagger(PersonIdent tagger) {
+		checkCallable();
 		this.tagger = tagger;
 		return this;
 	}
@@ -291,14 +396,15 @@
 	}
 
 	/**
-	 * Sets the object id of the tag. If the object id is null, the commit
-	 * pointed to from HEAD will be used.
+	 * Sets the object id of the tag. If the object id is {@code null}, the
+	 * commit pointed to from HEAD will be used.
 	 *
 	 * @param id
 	 *            a {@link org.eclipse.jgit.revwalk.RevObject} object.
 	 * @return {@code this}
 	 */
 	public TagCommand setObjectId(RevObject id) {
+		checkCallable();
 		this.id = id;
 		return this;
 	}
@@ -321,6 +427,7 @@
 	 * @return {@code this}
 	 */
 	public TagCommand setForceUpdate(boolean forceUpdate) {
+		checkCallable();
 		this.forceUpdate = forceUpdate;
 		return this;
 	}
@@ -334,18 +441,77 @@
 	 * @since 3.0
 	 */
 	public TagCommand setAnnotated(boolean annotated) {
-		this.annotated = annotated;
+		checkCallable();
+		this.annotated = Boolean.valueOf(annotated);
 		return this;
 	}
 
 	/**
-	 * Whether this will create an annotated command
+	 * Whether this will create an annotated tag.
 	 *
 	 * @return true if this command will create an annotated tag (default is
 	 *         true)
 	 * @since 3.0
 	 */
 	public boolean isAnnotated() {
-		return annotated;
+		boolean setExplicitly = Boolean.TRUE.equals(annotated) || isSigned();
+		if (setExplicitly) {
+			return true;
+		}
+		// Annotated at default (not set explicitly)
+		return annotated == null;
 	}
+
+	/**
+	 * Sets the signing key.
+	 * <p>
+	 * Per spec of {@code user.signingKey}: this will be sent to the GPG program
+	 * as is, i.e. can be anything supported by the GPG program.
+	 * </p>
+	 * <p>
+	 * Note, if none was set or {@code null} is specified a default will be
+	 * obtained from the configuration.
+	 * </p>
+	 * <p>
+	 * If set to a non-{@code null} value, the tag will be a signed annotated
+	 * tag.
+	 * </p>
+	 *
+	 * @param signingKey
+	 *            signing key; {@code null} allowed
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public TagCommand setSigningKey(String signingKey) {
+		checkCallable();
+		this.signingKey = signingKey;
+		return this;
+	}
+
+	/**
+	 * Retrieves the signing key ID.
+	 *
+	 * @return the key ID set, or {@code null} if none is set
+	 * @since 5.11
+	 */
+	public String getSigningKey() {
+		return signingKey;
+	}
+
+	/**
+	 * Sets a {@link CredentialsProvider}
+	 *
+	 * @param credentialsProvider
+	 *            the provider to use when querying for credentials (eg., during
+	 *            signing)
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public TagCommand setCredentialsProvider(
+			CredentialsProvider credentialsProvider) {
+		checkCallable();
+		this.credentialsProvider = credentialsProvider;
+		return this;
+	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java
new file mode 100644
index 0000000..21cddf7
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.api;
+
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.revwalk.RevObject;
+
+/**
+ * A {@code VerificationResult} describes the outcome of a signature
+ * verification.
+ *
+ * @see VerifySignatureCommand
+ *
+ * @since 5.11
+ */
+public interface VerificationResult {
+
+	/**
+	 * If an error occurred during signature verification, this retrieves the
+	 * exception.
+	 *
+	 * @return the exception, or {@code null} if none occurred
+	 */
+	Throwable getException();
+
+	/**
+	 * Retrieves the signature verification result.
+	 *
+	 * @return the result, or {@code null} if none was computed
+	 */
+	GpgSignatureVerifier.SignatureVerification getVerification();
+
+	/**
+	 * Retrieves the git object of which the signature was verified.
+	 *
+	 * @return the git object
+	 */
+	RevObject getObject();
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java
new file mode 100644
index 0000000..6a2a44e
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.api;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.api.errors.ServiceUnavailableException;
+import org.eclipse.jgit.api.errors.WrongObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * A command to verify GPG signatures on tags or commits.
+ *
+ * @since 5.11
+ */
+public class VerifySignatureCommand extends GitCommand<Map<String, VerificationResult>> {
+
+	/**
+	 * Describes what kind of objects shall be handled by a
+	 * {@link VerifySignatureCommand}.
+	 */
+	public enum VerifyMode {
+		/**
+		 * Handle any object type, ignore anything that is not a commit or tag.
+		 */
+		ANY,
+		/**
+		 * Handle only commits; throw a {@link WrongObjectTypeException} for
+		 * anything else.
+		 */
+		COMMITS,
+		/**
+		 * Handle only tags; throw a {@link WrongObjectTypeException} for
+		 * anything else.
+		 */
+		TAGS
+	}
+
+	private final Set<String> namesToCheck = new HashSet<>();
+
+	private VerifyMode mode = VerifyMode.ANY;
+
+	private GpgSignatureVerifier verifier;
+
+	private GpgConfig config;
+
+	private boolean ownVerifier;
+
+	/**
+	 * Creates a new {@link VerifySignatureCommand} for the given {@link Repository}.
+	 *
+	 * @param repo
+	 *            to operate on
+	 */
+	public VerifySignatureCommand(Repository repo) {
+		super(repo);
+	}
+
+	/**
+	 * Add a name of an object (SHA-1, ref name; anything that can be
+	 * {@link Repository#resolve(String) resolved}) to the command to have its
+	 * signature verified.
+	 *
+	 * @param name
+	 *            to add
+	 * @return {@code this}
+	 */
+	public VerifySignatureCommand addName(String name) {
+		checkCallable();
+		namesToCheck.add(name);
+		return this;
+	}
+
+	/**
+	 * Add names of objects (SHA-1, ref name; anything that can be
+	 * {@link Repository#resolve(String) resolved}) to the command to have their
+	 * signatures verified.
+	 *
+	 * @param names
+	 *            to add; duplicates will be ignored
+	 * @return {@code this}
+	 */
+	public VerifySignatureCommand addNames(String... names) {
+		checkCallable();
+		namesToCheck.addAll(Arrays.asList(names));
+		return this;
+	}
+
+	/**
+	 * Add names of objects (SHA-1, ref name; anything that can be
+	 * {@link Repository#resolve(String) resolved}) to the command to have their
+	 * signatures verified.
+	 *
+	 * @param names
+	 *            to add; duplicates will be ignored
+	 * @return {@code this}
+	 */
+	public VerifySignatureCommand addNames(Collection<String> names) {
+		checkCallable();
+		namesToCheck.addAll(names);
+		return this;
+	}
+
+	/**
+	 * Sets the mode of operation for this command.
+	 *
+	 * @param mode
+	 *            the {@link VerifyMode} to set
+	 * @return {@code this}
+	 */
+	public VerifySignatureCommand setMode(@NonNull VerifyMode mode) {
+		checkCallable();
+		this.mode = mode;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link GpgSignatureVerifier} to use.
+	 *
+	 * @param verifier
+	 *            the {@link GpgSignatureVerifier} to use, or {@code null} to
+	 *            use the default verifier
+	 * @return {@code this}
+	 */
+	public VerifySignatureCommand setVerifier(GpgSignatureVerifier verifier) {
+		checkCallable();
+		this.verifier = verifier;
+		return this;
+	}
+
+	/**
+	 * Sets an external {@link GpgConfig} to use. Whether it will be used it at
+	 * the discretion of the {@link #setVerifier(GpgSignatureVerifier)}.
+	 *
+	 * @param config
+	 *            to set; if {@code null}, the config will be loaded from the
+	 *            git config of the repository
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public VerifySignatureCommand setGpgConfig(GpgConfig config) {
+		checkCallable();
+		this.config = config;
+		return this;
+	}
+
+	/**
+	 * Retrieves the currently set {@link GpgSignatureVerifier}. Can be used
+	 * after a successful {@link #call()} to get the verifier that was used.
+	 *
+	 * @return the {@link GpgSignatureVerifier}
+	 */
+	public GpgSignatureVerifier getVerifier() {
+		return verifier;
+	}
+
+	/**
+	 * {@link Repository#resolve(String) Resolves} all names added to the
+	 * command to git objects and verifies their signature. Non-existing objects
+	 * are ignored.
+	 * <p>
+	 * Depending on the {@link #setMode(VerifyMode)}, only tags or commits or
+	 * any kind of objects are allowed.
+	 * </p>
+	 * <p>
+	 * Unsigned objects are silently skipped.
+	 * </p>
+	 *
+	 * @return a map of the given names to the corresponding
+	 *         {@link VerificationResult}, excluding ignored or skipped objects.
+	 * @throws ServiceUnavailableException
+	 *             if no {@link GpgSignatureVerifier} was set and no
+	 *             {@link GpgSignatureVerifierFactory} is available
+	 * @throws WrongObjectTypeException
+	 *             if a name resolves to an object of a type not allowed by the
+	 *             {@link #setMode(VerifyMode)} mode
+	 */
+	@Override
+	@NonNull
+	public Map<String, VerificationResult> call()
+			throws ServiceUnavailableException, WrongObjectTypeException {
+		checkCallable();
+		setCallable(false);
+		Map<String, VerificationResult> result = new HashMap<>();
+		if (verifier == null) {
+			GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory
+					.getDefault();
+			if (factory == null) {
+				throw new ServiceUnavailableException(
+						JGitText.get().signatureVerificationUnavailable);
+			}
+			verifier = factory.getVerifier();
+			ownVerifier = true;
+		}
+		if (config == null) {
+			config = new GpgConfig(repo.getConfig());
+		}
+		try (RevWalk walk = new RevWalk(repo)) {
+			for (String toCheck : namesToCheck) {
+				ObjectId id = repo.resolve(toCheck);
+				if (id != null && !ObjectId.zeroId().equals(id)) {
+					RevObject object;
+					try {
+						object = walk.parseAny(id);
+					} catch (MissingObjectException e) {
+						continue;
+					}
+					VerificationResult verification = verifyOne(object);
+					if (verification != null) {
+						result.put(toCheck, verification);
+					}
+				}
+			}
+		} catch (IOException e) {
+			throw new JGitInternalException(
+					JGitText.get().signatureVerificationError, e);
+		} finally {
+			if (ownVerifier) {
+				verifier.clear();
+			}
+		}
+		return result;
+	}
+
+	private VerificationResult verifyOne(RevObject object)
+			throws WrongObjectTypeException, IOException {
+		int type = object.getType();
+		if (VerifyMode.TAGS.equals(mode) && type != Constants.OBJ_TAG) {
+			throw new WrongObjectTypeException(object, Constants.OBJ_TAG);
+		} else if (VerifyMode.COMMITS.equals(mode)
+				&& type != Constants.OBJ_COMMIT) {
+			throw new WrongObjectTypeException(object, Constants.OBJ_COMMIT);
+		}
+		if (type == Constants.OBJ_COMMIT || type == Constants.OBJ_TAG) {
+			try {
+				GpgSignatureVerifier.SignatureVerification verification = verifier
+						.verifySignature(object, config);
+				if (verification == null) {
+					// Not signed
+					return null;
+				}
+				// Create new result
+				return new Result(object, verification, null);
+			} catch (JGitInternalException e) {
+				return new Result(object, null, e);
+			}
+		}
+		return null;
+	}
+
+	private static class Result implements VerificationResult {
+
+		private final Throwable throwable;
+
+		private final SignatureVerification verification;
+
+		private final RevObject object;
+
+		public Result(RevObject object, SignatureVerification verification,
+				Throwable throwable) {
+			this.object = object;
+			this.verification = verification;
+			this.throwable = throwable;
+		}
+
+		@Override
+		public Throwable getException() {
+			return throwable;
+		}
+
+		@Override
+		public SignatureVerification getVerification() {
+			return verification;
+		}
+
+		@Override
+		public RevObject getObject() {
+			return object;
+		}
+
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RefAlreadyExistsException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RefAlreadyExistsException.java
index 7e39361..81b7bd8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RefAlreadyExistsException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RefAlreadyExistsException.java
@@ -1,42 +1,17 @@
 /*
- * Copyright (C) 2010,Mathias Kinzler <mathias.kinzler@sap.com> and
- * other copyright owners as documented in the project's IP log.
+ * Copyright (C) 2010, 2020 Mathias Kinzler <mathias.kinzler@sap.com> and others
  *
  * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v1.0 which accompanies this
- * distribution, is reproduced below, and is available at
- * http://www.eclipse.org/org/documents/edl-v10.php
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
  *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * - Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the names of its
- * contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
+ * SPDX-License-Identifier: BSD-3-Clause
  */
 package org.eclipse.jgit.api.errors;
 
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.RefUpdate;
+
 /**
  * Thrown when trying to create a {@link org.eclipse.jgit.lib.Ref} with the same
  * name as an existing one
@@ -44,13 +19,43 @@
 public class RefAlreadyExistsException extends GitAPIException {
 	private static final long serialVersionUID = 1L;
 
+	private final RefUpdate.Result updateResult;
+
 	/**
-	 * Constructor for RefAlreadyExistsException
+	 * Creates a new instance with the given message.
 	 *
 	 * @param message
 	 *            error message
 	 */
 	public RefAlreadyExistsException(String message) {
+		this(message, null);
+	}
+
+	/**
+	 * Constructor for RefAlreadyExistsException
+	 *
+	 * @param message
+	 *            error message
+	 * @param updateResult
+	 *            that caused the exception; may be {@code null}
+	 * @since 5.11
+	 */
+	public RefAlreadyExistsException(String message,
+			@Nullable RefUpdate.Result updateResult) {
 		super(message);
+		this.updateResult = updateResult;
+	}
+
+	/**
+	 * Retrieves the {@link org.eclipse.jgit.lib.RefUpdate.Result
+	 * RefUpdate.Result} that caused the exception.
+	 *
+	 * @return the {@link org.eclipse.jgit.lib.RefUpdate.Result
+	 *         RefUpdate.Result} or {@code null} if unknown
+	 * @since 5.11
+	 */
+	@Nullable
+	public RefUpdate.Result getUpdateResult() {
+		return updateResult;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/WrongObjectTypeException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/WrongObjectTypeException.java
new file mode 100644
index 0000000..f639c2f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/WrongObjectTypeException.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.api.errors;
+
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * A given object is not of an expected object type.
+ *
+ * @since 5.11
+ */
+public class WrongObjectTypeException extends GitAPIException {
+
+	private static final long serialVersionUID = 1L;
+
+	private String name;
+
+	private int type;
+
+	/**
+	 * Construct a {@link WrongObjectTypeException} for the specified object id,
+	 * giving the expected type.
+	 *
+	 * @param id
+	 *            {@link ObjectId} of the object with the unexpected type
+	 * @param type
+	 *            expected object type code; see
+	 *            {@link Constants}{@code .OBJ_*}.
+	 */
+	public WrongObjectTypeException(ObjectId id, int type) {
+		super(MessageFormat.format(JGitText.get().objectIsNotA, id.name(),
+				Constants.typeString(type)));
+		this.name = id.name();
+		this.type = type;
+	}
+
+	/**
+	 * Retrieves the name (SHA-1) of the object.
+	 *
+	 * @return the name
+	 */
+	public String getObjectId() {
+		return name;
+	}
+
+	/**
+	 * Retrieves the expected type code. See {@link Constants}{@code .OBJ_*}.
+	 *
+	 * @return the type code
+	 */
+	public int getExpectedType() {
+		return type;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandRegistry.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandRegistry.java
index 2698e23..1c9e9d7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandRegistry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/FilterCommandRegistry.java
@@ -12,6 +12,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -23,7 +24,7 @@
  * @since 4.6
  */
 public class FilterCommandRegistry {
-	private static ConcurrentHashMap<String, FilterCommandFactory> filterCommandRegistry = new ConcurrentHashMap<>();
+	private static Map<String, FilterCommandFactory> filterCommandRegistry = new ConcurrentHashMap<>();
 
 	/**
 	 * Register a {@link org.eclipse.jgit.attributes.FilterCommandFactory}
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 8c51a7a..671475e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -946,12 +946,14 @@
 				// called before). Ignore the cached deletion and use what we
 				// find in Merge. Potentially updates the file.
 				if (equalIdAndMode(hId, hMode, mId, mMode)) {
-					if (initialCheckout)
+					if (initialCheckout || force) {
 						update(name, mId, mMode);
-					else
+					} else {
 						keep(name, dce, f);
-				} else
+					}
+				} else {
 					conflict(name, dce, h, m);
+				}
 			}
 		} else {
 			// Something in Index
@@ -1214,8 +1216,12 @@
 
 	private void keep(String path, DirCacheEntry e, WorkingTreeIterator f)
 			throws IOException {
-		if (e != null && !FileMode.TREE.equals(e.getFileMode()))
+		if (e == null) {
+			return;
+		}
+		if (!FileMode.TREE.equals(e.getFileMode())) {
 			builder.add(e);
+		}
 		if (force) {
 			if (f == null || f.isModified(e, true, walk.getObjectReader())) {
 				kept.add(path);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoPackSignatureException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoPackSignatureException.java
index c3b1df9..a37b8be 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoPackSignatureException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoPackSignatureException.java
@@ -13,8 +13,7 @@
 import java.io.IOException;
 
 /**
- * Thrown when a PackFile is found not to contain the pack signature defined by
- * git.
+ * Thrown when a Pack is found not to contain the pack signature defined by git.
  *
  * @since 4.5
  */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java
index c484984..1fd8086 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java
@@ -17,7 +17,7 @@
 import org.eclipse.jgit.internal.JGitText;
 
 /**
- * Thrown when a PackFile previously failed and is known to be unusable
+ * Thrown when a Pack previously failed and is known to be unusable
  */
 public class PackInvalidException extends IOException {
 	private static final long serialVersionUID = 1L;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java
index ad5664c..44b8e01 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java
@@ -13,7 +13,7 @@
 import java.io.IOException;
 
 /**
- * Thrown when a PackFile no longer matches the PackIndex.
+ * Thrown when a Pack no longer matches the PackIndex.
  */
 public class PackMismatchException extends IOException {
 	private static final long serialVersionUID = 1L;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/UnsupportedPackVersionException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/UnsupportedPackVersionException.java
index 7538229..07aa756 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/UnsupportedPackVersionException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/UnsupportedPackVersionException.java
@@ -16,7 +16,7 @@
 import org.eclipse.jgit.internal.JGitText;
 
 /**
- * Thrown when a PackFile uses a pack version not supported by JGit.
+ * Thrown when a Pack uses a pack version not supported by JGit.
  *
  * @since 4.5
  */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java b/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java
index 32c3a1d..476c37c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/events/ListenerList.java
@@ -11,15 +11,15 @@
 package org.eclipse.jgit.events;
 
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
  * Manages a thread-safe list of {@link org.eclipse.jgit.events.RepositoryListener}s.
  */
 public class ListenerList {
-	private final ConcurrentMap<Class<? extends RepositoryListener>, CopyOnWriteArrayList<ListenerHandle>> lists = new ConcurrentHashMap<>();
+	private final Map<Class<? extends RepositoryListener>, CopyOnWriteArrayList<ListenerHandle>> lists = new ConcurrentHashMap<>();
 
 	/**
 	 * Register a {@link org.eclipse.jgit.events.WorkingTreeModifiedListener}.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java
index 4059b16..ce3ad22 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java
@@ -9,12 +9,10 @@
  */
 package org.eclipse.jgit.hooks;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.PrintStream;
-import java.io.UnsupportedEncodingException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
 import java.util.concurrent.Callable;
 
 import org.eclipse.jgit.api.errors.AbortedByHookException;
@@ -35,21 +33,21 @@
  *            the return type which is expected from {@link #call()}
  * @see <a href="http://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks">Git
  *      Hooks on the git-scm official site</a>
- * @since 4.0
+ * @since 5.11
  */
-abstract class GitHook<T> implements Callable<T> {
+public abstract class GitHook<T> implements Callable<T> {
 
 	private final Repository repo;
 
 	/**
 	 * The output stream to be used by the hook.
 	 */
-	protected final PrintStream outputStream;
+	private final OutputStream outputStream;
 
 	/**
 	 * The error stream to be used by the hook.
 	 */
-	protected final PrintStream errorStream;
+	private final OutputStream errorStream;
 
 	/**
 	 * Constructor for GitHook.
@@ -63,7 +61,7 @@
 	 *            The output stream the hook must use. {@code null} is allowed,
 	 *            in which case the hook will use {@code System.out}.
 	 */
-	protected GitHook(Repository repo, PrintStream outputStream) {
+	protected GitHook(Repository repo, OutputStream outputStream) {
 		this(repo, outputStream, null);
 	}
 
@@ -79,8 +77,8 @@
 	 *            The error stream the hook must use. {@code null} is allowed,
 	 *            in which case the hook will use {@code System.err}.
 	 */
-	protected GitHook(Repository repo, PrintStream outputStream,
-			PrintStream errorStream) {
+	protected GitHook(Repository repo, OutputStream outputStream,
+			OutputStream errorStream) {
 		this.repo = repo;
 		this.outputStream = outputStream;
 		this.errorStream = errorStream;
@@ -137,7 +135,7 @@
 	 * @return The output stream the hook must use. Never {@code null},
 	 *         {@code System.out} is returned by default.
 	 */
-	protected PrintStream getOutputStream() {
+	protected OutputStream getOutputStream() {
 		return outputStream == null ? System.out : outputStream;
 	}
 
@@ -147,7 +145,7 @@
 	 * @return The error stream the hook must use. Never {@code null},
 	 *         {@code System.err} is returned by default.
 	 */
-	protected PrintStream getErrorStream() {
+	protected OutputStream getErrorStream() {
 		return errorStream == null ? System.err : errorStream;
 	}
 
@@ -156,34 +154,48 @@
 	 *
 	 * @throws org.eclipse.jgit.api.errors.AbortedByHookException
 	 *             If the underlying hook script exited with non-zero.
+	 * @throws IOException
+	 *             if an IO error occurred
 	 */
-	protected void doRun() throws AbortedByHookException {
+	protected void doRun() throws AbortedByHookException, IOException {
 		final ByteArrayOutputStream errorByteArray = new ByteArrayOutputStream();
 		final TeeOutputStream stderrStream = new TeeOutputStream(errorByteArray,
 				getErrorStream());
-		PrintStream hookErrRedirect = null;
-		try {
-			hookErrRedirect = new PrintStream(stderrStream, false,
-					UTF_8.name());
-		} catch (UnsupportedEncodingException e) {
-			// UTF-8 is guaranteed to be available
-		}
 		Repository repository = getRepository();
 		FS fs = repository.getFS();
 		if (fs == null) {
 			fs = FS.DETECTED;
 		}
 		ProcessResult result = fs.runHookIfPresent(repository, getHookName(),
-				getParameters(), getOutputStream(), hookErrRedirect,
+				getParameters(), getOutputStream(), stderrStream,
 				getStdinArgs());
 		if (result.isExecutedWithError()) {
-			throw new AbortedByHookException(
-					new String(errorByteArray.toByteArray(), UTF_8),
-					getHookName(), result.getExitCode());
+			handleError(new String(errorByteArray.toByteArray(),
+					Charset.defaultCharset().name()), result);
 		}
 	}
 
 	/**
+	 * Process that the hook exited with an error. This default implementation
+	 * throws an {@link AbortedByHookException }. Hooks which need a different
+	 * behavior can overwrite this method.
+	 *
+	 * @param message
+	 *            error message
+	 * @param result
+	 *            The process result of the hook
+	 * @throws AbortedByHookException
+	 *             When the hook should be aborted
+	 * @since 5.11
+	 */
+	protected void handleError(String message,
+			final ProcessResult result)
+			throws AbortedByHookException {
+		throw new AbortedByHookException(message, getHookName(),
+				result.getExitCode());
+	}
+
+	/**
 	 * Check whether a 'native' (i.e. script) hook is installed in the
 	 * repository.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java
index 0b61ebe..b9dafcc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java
@@ -14,6 +14,7 @@
 
 import org.eclipse.jgit.api.errors.AbortedByHookException;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.ProcessResult;
 
 /**
  * The <code>post-commit</code> hook implementation. This hook is run after the
@@ -73,4 +74,16 @@
 		return NAME;
 	}
 
+
+	/**
+	 * Overwrites the default implementation to never throw an
+	 * {@link AbortedByHookException}, as the commit has already been done and
+	 * the exit code of the post-commit hook has no effect.
+	 */
+	@Override
+	protected void handleError(String message, ProcessResult result)
+			throws AbortedByHookException {
+		// Do nothing as the exit code of the post-commit hook has no effect.
+	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
index d7e4f79..9dd565f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014, Andrey Loskutov <loskutov@gmx.de> and others
+ * Copyright (C) 2014, 2021 Andrey Loskutov <loskutov@gmx.de> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -14,8 +14,11 @@
 import static org.eclipse.jgit.ignore.internal.Strings.stripTrailing;
 import static org.eclipse.jgit.ignore.internal.Strings.stripTrailingWhitespace;
 
+import java.text.MessageFormat;
+
 import org.eclipse.jgit.errors.InvalidPatternException;
 import org.eclipse.jgit.ignore.internal.PathMatcher;
+import org.eclipse.jgit.internal.JGitText;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -36,11 +39,11 @@
 	 */
 	public static final char PATH_SEPARATOR = '/';
 
-	private final IMatcher matcher;
+	private IMatcher matcher;
 
-	private final boolean inverse;
+	private boolean inverse;
 
-	private final boolean dirOnly;
+	private boolean dirOnly;
 
 	/**
 	 * Constructor for FastIgnoreRule
@@ -52,8 +55,23 @@
 	 *            (comment), this rule doesn't match anything.
 	 */
 	public FastIgnoreRule(String pattern) {
-		if (pattern == null)
+		this();
+		try {
+			parse(pattern);
+		} catch (InvalidPatternException e) {
+			LOG.error(MessageFormat.format(JGitText.get().badIgnorePattern,
+					e.getPattern()), e);
+		}
+	}
+
+	FastIgnoreRule() {
+		matcher = IMatcher.NO_MATCH;
+	}
+
+	void parse(String pattern) throws InvalidPatternException {
+		if (pattern == null) {
 			throw new IllegalArgumentException("Pattern must not be null!"); //$NON-NLS-1$
+		}
 		if (pattern.length() == 0) {
 			dirOnly = false;
 			inverse = false;
@@ -90,15 +108,8 @@
 				return;
 			}
 		}
-		IMatcher m;
-		try {
-			m = PathMatcher.createPathMatcher(pattern,
-					Character.valueOf(PATH_SEPARATOR), dirOnly);
-		} catch (InvalidPatternException e) {
-			m = NO_MATCH;
-			LOG.error(e.getMessage(), e);
-		}
-		this.matcher = m;
+		this.matcher = PathMatcher.createPathMatcher(pattern,
+				Character.valueOf(PATH_SEPARATOR), dirOnly);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java
index 0bc6124..4e7f126 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, Red Hat Inc. and others
+ * Copyright (C) 2010, 2021 Red Hat 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
@@ -15,16 +15,26 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.InvalidPatternException;
+import org.eclipse.jgit.internal.JGitText;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 /**
  * Represents a bundle of ignore rules inherited from a base directory.
  *
  * This class is not thread safe, it maintains state about the last match.
  */
 public class IgnoreNode {
+
+	private static final Logger LOG = LoggerFactory.getLogger(IgnoreNode.class);
+
 	/** Result from {@link IgnoreNode#isIgnored(String, boolean)}. */
 	public enum MatchResult {
 		/** The file is not ignored, due to a rule saying its not ignored. */
@@ -52,7 +62,7 @@
 	 * Create an empty ignore node with no rules.
 	 */
 	public IgnoreNode() {
-		rules = new ArrayList<>();
+		this(new ArrayList<>());
 	}
 
 	/**
@@ -75,15 +85,47 @@
 	 *             Error thrown when reading an ignore file.
 	 */
 	public void parse(InputStream in) throws IOException {
+		parse(null, in);
+	}
+
+	/**
+	 * Parse files according to gitignore standards.
+	 *
+	 * @param sourceName
+	 *            identifying the source of the stream
+	 * @param in
+	 *            input stream holding the standard ignore format. The caller is
+	 *            responsible for closing the stream.
+	 * @throws java.io.IOException
+	 *             Error thrown when reading an ignore file.
+	 * @since 5.11
+	 */
+	public void parse(String sourceName, InputStream in) throws IOException {
 		BufferedReader br = asReader(in);
 		String txt;
+		int lineNumber = 1;
 		while ((txt = br.readLine()) != null) {
 			if (txt.length() > 0 && !txt.startsWith("#") && !txt.equals("/")) { //$NON-NLS-1$ //$NON-NLS-2$
-				FastIgnoreRule rule = new FastIgnoreRule(txt);
+				FastIgnoreRule rule = new FastIgnoreRule();
+				try {
+					rule.parse(txt);
+				} catch (InvalidPatternException e) {
+					if (sourceName != null) {
+						LOG.error(MessageFormat.format(
+								JGitText.get().badIgnorePatternFull, sourceName,
+								Integer.toString(lineNumber), e.getPattern(),
+								e.getLocalizedMessage()), e);
+					} else {
+						LOG.error(MessageFormat.format(
+								JGitText.get().badIgnorePattern,
+								e.getPattern()), e);
+					}
+				}
 				if (!rule.isEmpty()) {
 					rules.add(rule);
 				}
 			}
+			lineNumber++;
 		}
 	}
 
@@ -135,7 +177,8 @@
 	 *         undetermined
 	 * @since 4.11
 	 */
-	public Boolean checkIgnored(String entryPath, boolean isDirectory) {
+	public @Nullable Boolean checkIgnored(String entryPath,
+			boolean isDirectory) {
 		// Parse rules in the reverse order that they were read because later
 		// rules have higher priority
 		for (int i = rules.size() - 1; i > -1; i--) {
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 66d54c0..b942c09 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -58,6 +58,8 @@
 	/***/ public String badEntryName;
 	/***/ public String badEscape;
 	/***/ public String badGroupHeader;
+	/***/ public String badIgnorePattern;
+	/***/ public String badIgnorePatternFull;
 	/***/ public String badObjectType;
 	/***/ public String badRef;
 	/***/ public String badSectionEntry;
@@ -261,6 +263,7 @@
 	/***/ public String downloadCancelledDuringIndexing;
 	/***/ public String duplicateAdvertisementsOf;
 	/***/ public String duplicateRef;
+	/***/ public String duplicateRefAttribute;
 	/***/ public String duplicateRemoteRefUpdateIsIllegal;
 	/***/ public String duplicateStagesNotAllowed;
 	/***/ public String eitherGitDirOrWorkTreeRequired;
@@ -338,6 +341,10 @@
 	/***/ public String hoursAgo;
 	/***/ public String httpConfigCannotNormalizeURL;
 	/***/ public String httpConfigInvalidURL;
+	/***/ public String httpFactoryInUse;
+	/***/ public String httpPreAuthTooLate;
+	/***/ public String httpUserInfoDecodeError;
+	/***/ public String httpWrongConnectionType;
 	/***/ public String hugeIndexesAreNotSupportedByJgitYet;
 	/***/ public String hunkBelongsToAnotherFile;
 	/***/ public String hunkDisconnectedFromFile;
@@ -554,6 +561,7 @@
 	/***/ public String peerDidNotSupplyACompleteObjectGraph;
 	/***/ public String personIdentEmailNonNull;
 	/***/ public String personIdentNameNonNull;
+	/***/ public String postCommitHookFailed;
 	/***/ public String prefixRemote;
 	/***/ public String problemWithResolvingPushRefSpecsLocally;
 	/***/ public String progressMonUploading;
@@ -595,6 +603,7 @@
 	/***/ public String reftableDirExists;
 	/***/ public String reftableRecordsMustIncrease;
 	/***/ public String refUpdateReturnCodeWas;
+	/***/ public String remoteBranchNotFound;
 	/***/ public String remoteConfigHasNoURIAssociated;
 	/***/ public String remoteDoesNotHaveSpec;
 	/***/ public String remoteDoesNotSupportSmartHTTPPush;
@@ -649,7 +658,9 @@
 	/***/ public String shortReadOfBlock;
 	/***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes;
 	/***/ public String shortSkipOfBlock;
-	/***/ public String signingNotSupportedOnTag;
+	/***/ public String signatureVerificationError;
+	/***/ public String signatureVerificationUnavailable;
+	/***/ public String signedTagMessageNoLf;
 	/***/ public String signingServiceUnavailable;
 	/***/ public String similarityScoreMustBeWithinBounds;
 	/***/ public String skipMustBeNonNegative;
@@ -765,6 +776,7 @@
 	/***/ public String unmergedPaths;
 	/***/ public String unpackException;
 	/***/ public String unreadablePackIndex;
+	/***/ public String unrecognizedPackExtension;
 	/***/ public String unrecognizedRef;
 	/***/ public String unsetMark;
 	/***/ public String unsupportedAlternates;
@@ -790,6 +802,13 @@
 	/***/ public String URINotSupported;
 	/***/ public String userConfigInvalid;
 	/***/ public String validatingGitModules;
+	/***/ public String verifySignatureBad;
+	/***/ public String verifySignatureExpired;
+	/***/ public String verifySignatureGood;
+	/***/ public String verifySignatureIssuer;
+	/***/ public String verifySignatureKey;
+	/***/ public String verifySignatureMade;
+	/***/ public String verifySignatureTrust;
 	/***/ public String walkFailure;
 	/***/ public String wantNoSpaceWithCapabilities;
 	/***/ public String wantNotValid;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java
deleted file mode 100644
index 5ddbcbd..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.eclipse.jgit.internal.ketch.KetchConstants.TERM;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.TimeoutException;
-
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TreeFormatter;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.time.ProposedTimestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The initial {@link Round} for a leaderless repository, used to establish a
- * leader.
- */
-class ElectionRound extends Round {
-	private static final Logger log = LoggerFactory.getLogger(ElectionRound.class);
-
-	private long term;
-
-	ElectionRound(KetchLeader leader, LogIndex head) {
-		super(leader, head);
-	}
-
-	@Override
-	void start() throws IOException {
-		ObjectId id;
-		try (Repository git = leader.openRepository();
-				ProposedTimestamp ts = getSystem().getClock().propose();
-				ObjectInserter inserter = git.newObjectInserter()) {
-			id = bumpTerm(git, ts, inserter);
-			inserter.flush();
-			blockUntil(ts);
-		}
-		runAsync(id);
-	}
-
-	@Override
-	void success() {
-		// Do nothing upon election, KetchLeader will copy the term.
-	}
-
-	long getTerm() {
-		return term;
-	}
-
-	private ObjectId bumpTerm(Repository git, ProposedTimestamp ts,
-			ObjectInserter inserter) throws IOException {
-		CommitBuilder b = new CommitBuilder();
-		if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
-			try (RevWalk rw = new RevWalk(git)) {
-				RevCommit c = rw.parseCommit(acceptedOldIndex);
-				if (getSystem().requireMonotonicLeaderElections()) {
-					if (ts.read(SECONDS) < c.getCommitTime()) {
-						throw new TimeIsUncertainException();
-					}
-				}
-				b.setTreeId(c.getTree());
-				b.setParentId(acceptedOldIndex);
-				term = parseTerm(c.getFooterLines(TERM)) + 1;
-			}
-		} else {
-			term = 1;
-			b.setTreeId(inserter.insert(new TreeFormatter()));
-		}
-
-		StringBuilder msg = new StringBuilder();
-		msg.append(KetchConstants.TERM.getName())
-				.append(": ") //$NON-NLS-1$
-				.append(term);
-
-		String tag = leader.getSystem().newLeaderTag();
-		if (tag != null && !tag.isEmpty()) {
-			msg.append(' ').append(tag);
-		}
-
-		b.setAuthor(leader.getSystem().newCommitter(ts));
-		b.setCommitter(b.getAuthor());
-		b.setMessage(msg.toString());
-
-		if (log.isDebugEnabled()) {
-			log.debug("Trying to elect myself " + b.getMessage()); //$NON-NLS-1$
-		}
-		return inserter.insert(b);
-	}
-
-	private static long parseTerm(List<String> footer) {
-		if (footer.isEmpty()) {
-			return 0;
-		}
-
-		String s = footer.get(0);
-		int p = s.indexOf(' ');
-		if (p > 0) {
-			s = s.substring(0, p);
-		}
-		return Long.parseLong(s, 10);
-	}
-
-	private void blockUntil(ProposedTimestamp ts) throws IOException {
-		try {
-			ts.blockUntil(getSystem().getMaxWaitForMonotonicClock());
-		} catch (InterruptedException | TimeoutException e) {
-			throw new TimeIsUncertainException(e);
-		}
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java
deleted file mode 100644
index f4a7f59..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import org.eclipse.jgit.revwalk.FooterKey;
-
-/**
- * Frequently used constants in a Ketch system.
- */
-public class KetchConstants {
-	/**
-	 * Default reference namespace holding {@link #ACCEPTED} and
-	 * {@link #COMMITTED} references and the {@link #STAGE} sub-namespace.
-	 */
-	public static final String DEFAULT_TXN_NAMESPACE = "refs/txn/"; //$NON-NLS-1$
-
-	/** Reference name holding the RefTree accepted by a follower. */
-	public static final String ACCEPTED = "accepted"; //$NON-NLS-1$
-
-	/** Reference name holding the RefTree known to be committed. */
-	public static final String COMMITTED = "committed"; //$NON-NLS-1$
-
-	/** Reference subdirectory holding proposed heads. */
-	public static final String STAGE = "stage/"; //$NON-NLS-1$
-
-	/** Footer containing the current term. */
-	public static final FooterKey TERM = new FooterKey("Term"); //$NON-NLS-1$
-
-	/** Section for Ketch configuration ({@code ketch}). */
-	public static final String CONFIG_SECTION_KETCH = "ketch"; //$NON-NLS-1$
-
-	/** Behavior for a replica ({@code remote.$name.ketch-type}) */
-	public static final String CONFIG_KEY_TYPE = "ketch-type"; //$NON-NLS-1$
-
-	/** Behavior for a replica ({@code remote.$name.ketch-commit}) */
-	public static final String CONFIG_KEY_COMMIT = "ketch-commit"; //$NON-NLS-1$
-
-	/** Behavior for a replica ({@code remote.$name.ketch-speed}) */
-	public static final String CONFIG_KEY_SPEED = "ketch-speed"; //$NON-NLS-1$
-
-	private KetchConstants() {
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java
deleted file mode 100644
index 743d193..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java
+++ /dev/null
@@ -1,604 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static org.eclipse.jgit.internal.ketch.KetchLeader.State.CANDIDATE;
-import static org.eclipse.jgit.internal.ketch.KetchLeader.State.LEADER;
-import static org.eclipse.jgit.internal.ketch.KetchLeader.State.SHUTDOWN;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.Participation.FOLLOWER_ONLY;
-import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED;
-
-import java.io.IOException;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-import org.eclipse.jgit.internal.storage.reftree.RefTree;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A leader managing consensus across remote followers.
- * <p>
- * A leader instance starts up in
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader.State#CANDIDATE} and tries
- * to begin a new term by sending an
- * {@link org.eclipse.jgit.internal.ketch.ElectionRound} to all replicas. Its
- * term starts if a majority of replicas have accepted this leader instance for
- * the term.
- * <p>
- * Once elected by a majority the instance enters
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader.State#LEADER} and runs
- * proposals offered to {@link #queueProposal(Proposal)}. This continues until
- * the leader is timed out for inactivity, or is deposed by a competing leader
- * gaining its own majority.
- * <p>
- * Once timed out or deposed this {@code KetchLeader} instance should be
- * discarded, and a new instance takes over.
- * <p>
- * Each leader instance coordinates a group of
- * {@link org.eclipse.jgit.internal.ketch.KetchReplica}s. Replica instances are
- * owned by the leader instance and must be discarded when the leader is
- * discarded.
- * <p>
- * In Ketch all push requests are issued through the leader. The steps are as
- * follows (see {@link org.eclipse.jgit.internal.ketch.KetchPreReceive} for an
- * example):
- * <ul>
- * <li>Create a {@link org.eclipse.jgit.internal.ketch.Proposal} with the
- * {@link org.eclipse.jgit.transport.ReceiveCommand}s that represent the push.
- * <li>Invoke {@link #queueProposal(Proposal)} on the leader instance.
- * <li>Wait for consensus with
- * {@link org.eclipse.jgit.internal.ketch.Proposal#await()}.
- * <li>To examine the status of the push, check
- * {@link org.eclipse.jgit.internal.ketch.Proposal#getCommands()}, looking at
- * {@link org.eclipse.jgit.internal.storage.reftree.Command#getResult()}.
- * </ul>
- * <p>
- * The leader gains consensus by first pushing the needed objects and a
- * {@link org.eclipse.jgit.internal.storage.reftree.RefTree} representing the
- * desired target repository state to the {@code refs/txn/accepted} branch on
- * each of the replicas. Once a majority has succeeded, the leader commits the
- * state by either pushing the {@code refs/txn/accepted} value to
- * {@code refs/txn/committed} (for Ketch-aware replicas) or by pushing updates
- * to {@code refs/heads/master}, etc. for stock Git replicas.
- * <p>
- * Internally, the actual transport to replicas is performed on background
- * threads via the {@link org.eclipse.jgit.internal.ketch.KetchSystem}'s
- * executor service. For performance, the
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader},
- * {@link org.eclipse.jgit.internal.ketch.KetchReplica} and
- * {@link org.eclipse.jgit.internal.ketch.Proposal} objects share some state,
- * and may invoke each other's methods on different threads. This access is
- * protected by the leader's {@link #lock} object. Care must be taken to prevent
- * concurrent access by correctly obtaining the leader's lock.
- */
-public abstract class KetchLeader {
-	private static final Logger log = LoggerFactory.getLogger(KetchLeader.class);
-
-	/** Current state of the leader instance. */
-	public enum State {
-		/** Newly created instance trying to elect itself leader. */
-		CANDIDATE,
-
-		/** Leader instance elected by a majority. */
-		LEADER,
-
-		/** Instance has been deposed by another with a more recent term. */
-		DEPOSED,
-
-		/** Leader has been gracefully shutdown, e.g. due to inactivity. */
-		SHUTDOWN;
-	}
-
-	private final KetchSystem system;
-
-	/** Leader's knowledge of replicas for this repository. */
-	private KetchReplica[] voters;
-	private KetchReplica[] followers;
-	private LocalReplica self;
-
-	/**
-	 * Lock protecting all data within this leader instance.
-	 * <p>
-	 * This lock extends into the {@link KetchReplica} instances used by the
-	 * leader. They share the same lock instance to simplify concurrency.
-	 */
-	final Lock lock;
-
-	private State state = CANDIDATE;
-
-	/** Term of this leader, once elected. */
-	private long term;
-
-	/**
-	 * Pending proposals accepted into the queue in FIFO order.
-	 * <p>
-	 * These proposals were preflighted and do not contain any conflicts with
-	 * each other and their expectations matched the leader's local view of the
-	 * agreed upon {@code refs/txn/accepted} tree.
-	 */
-	private final List<Proposal> queued;
-
-	/**
-	 * State of the repository's RefTree after applying all entries in
-	 * {@link #queued}. New proposals must be consistent with this tree to be
-	 * appended to the end of {@link #queued}.
-	 * <p>
-	 * Must be deep-copied with {@link RefTree#copy()} if
-	 * {@link #roundHoldsReferenceToRefTree} is {@code true}.
-	 */
-	private RefTree refTree;
-
-	/**
-	 * If {@code true} {@link #refTree} must be duplicated before queuing the
-	 * next proposal. The {@link #refTree} was passed into the constructor of a
-	 * {@link ProposalRound}, and that external reference to the {@link RefTree}
-	 * object is held by the proposal until it materializes the tree object in
-	 * the object store. This field is set {@code true} when the proposal begins
-	 * execution and set {@code false} once tree objects are persisted in the
-	 * local repository's object store or {@link #refTree} is replaced with a
-	 * copy to isolate it from any running rounds.
-	 * <p>
-	 * If proposals arrive less frequently than the {@code RefTree} is written
-	 * out to the repository the {@link #roundHoldsReferenceToRefTree} behavior
-	 * avoids duplicating {@link #refTree}, reducing both time and memory used.
-	 * However if proposals arrive more frequently {@link #refTree} must be
-	 * duplicated to prevent newly queued proposals from corrupting the
-	 * {@link #runningRound}.
-	 */
-	volatile boolean roundHoldsReferenceToRefTree;
-
-	/** End of the leader's log. */
-	private LogIndex headIndex;
-
-	/** Leader knows this (and all prior) states are committed. */
-	private LogIndex committedIndex;
-
-	/**
-	 * Is the leader idle with no work pending? If {@code true} there is no work
-	 * for the leader (normal state). This field is {@code false} when the
-	 * leader thread is scheduled for execution, or while {@link #runningRound}
-	 * defines a round in progress.
-	 */
-	private boolean idle;
-
-	/** Current round the leader is preparing and waiting for a vote on. */
-	private Round runningRound;
-
-	/**
-	 * Construct a leader for a Ketch instance.
-	 *
-	 * @param system
-	 *            Ketch system configuration the leader must adhere to.
-	 */
-	protected KetchLeader(KetchSystem system) {
-		this.system = system;
-		this.lock = new ReentrantLock(true /* fair */);
-		this.queued = new ArrayList<>(4);
-		this.idle = true;
-	}
-
-	/** @return system configuration. */
-	KetchSystem getSystem() {
-		return system;
-	}
-
-	/**
-	 * Configure the replicas used by this Ketch instance.
-	 * <p>
-	 * Replicas should be configured once at creation before any proposals are
-	 * executed. Once elections happen, <b>reconfiguration is a complicated
-	 * concept that is not currently supported</b>.
-	 *
-	 * @param replicas
-	 *            members participating with the same repository.
-	 */
-	public void setReplicas(Collection<KetchReplica> replicas) {
-		List<KetchReplica> v = new ArrayList<>(5);
-		List<KetchReplica> f = new ArrayList<>(5);
-		for (KetchReplica r : replicas) {
-			switch (r.getParticipation()) {
-			case FULL:
-				v.add(r);
-				break;
-
-			case FOLLOWER_ONLY:
-				f.add(r);
-				break;
-			}
-		}
-
-		Collection<Integer> validVoters = validVoterCounts();
-		if (!validVoters.contains(Integer.valueOf(v.size()))) {
-			throw new IllegalArgumentException(MessageFormat.format(
-					KetchText.get().unsupportedVoterCount,
-					Integer.valueOf(v.size()),
-					validVoters));
-		}
-
-		LocalReplica me = findLocal(v);
-		if (me == null) {
-			throw new IllegalArgumentException(
-					KetchText.get().localReplicaRequired);
-		}
-
-		lock.lock();
-		try {
-			voters = v.toArray(new KetchReplica[0]);
-			followers = f.toArray(new KetchReplica[0]);
-			self = me;
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	private static Collection<Integer> validVoterCounts() {
-		@SuppressWarnings("boxing")
-		Integer[] valid = {
-				// An odd number of voting replicas is required.
-				1, 3, 5, 7, 9 };
-		return Arrays.asList(valid);
-	}
-
-	private static LocalReplica findLocal(Collection<KetchReplica> voters) {
-		for (KetchReplica r : voters) {
-			if (r instanceof LocalReplica) {
-				return (LocalReplica) r;
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * Get an instance of the repository for use by a leader thread.
-	 * <p>
-	 * The caller will close the repository.
-	 *
-	 * @return opened repository for use by the leader thread.
-	 * @throws java.io.IOException
-	 *             cannot reopen the repository for the leader.
-	 */
-	protected abstract Repository openRepository() throws IOException;
-
-	/**
-	 * Queue a reference update proposal for consensus.
-	 * <p>
-	 * This method does not wait for consensus to be reached. The proposal is
-	 * checked to look for risks of conflicts, and then submitted into the queue
-	 * for distribution as soon as possible.
-	 * <p>
-	 * Callers must use {@link org.eclipse.jgit.internal.ketch.Proposal#await()}
-	 * to see if the proposal is done.
-	 *
-	 * @param proposal
-	 *            the proposed reference updates to queue for consideration.
-	 *            Once execution is complete the individual reference result
-	 *            fields will be populated with the outcome.
-	 * @throws java.lang.InterruptedException
-	 *             current thread was interrupted. The proposal may have been
-	 *             aborted if it was not yet queued for execution.
-	 * @throws java.io.IOException
-	 *             unrecoverable error preventing proposals from being attempted
-	 *             by this leader.
-	 */
-	public void queueProposal(Proposal proposal)
-			throws InterruptedException, IOException {
-		try {
-			lock.lockInterruptibly();
-		} catch (InterruptedException e) {
-			proposal.abort();
-			throw e;
-		}
-		try {
-			if (refTree == null) {
-				initialize();
-				for (Proposal p : queued) {
-					refTree.apply(p.getCommands());
-				}
-			} else if (roundHoldsReferenceToRefTree) {
-				refTree = refTree.copy();
-				roundHoldsReferenceToRefTree = false;
-			}
-
-			if (!refTree.apply(proposal.getCommands())) {
-				// A conflict exists so abort the proposal.
-				proposal.abort();
-				return;
-			}
-
-			queued.add(proposal);
-			proposal.notifyState(QUEUED);
-
-			if (idle) {
-				scheduleLeader();
-			}
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	private void initialize() throws IOException {
-		try (Repository git = openRepository(); RevWalk rw = new RevWalk(git)) {
-			self.initialize(git);
-
-			ObjectId accepted = self.getTxnAccepted();
-			if (!ObjectId.zeroId().equals(accepted)) {
-				RevCommit c = rw.parseCommit(accepted);
-				headIndex = LogIndex.unknown(accepted);
-				refTree = RefTree.read(rw.getObjectReader(), c.getTree());
-			} else {
-				headIndex = LogIndex.unknown(ObjectId.zeroId());
-				refTree = RefTree.newEmptyTree();
-			}
-		}
-	}
-
-	private void scheduleLeader() {
-		idle = false;
-		system.getExecutor().execute(this::runLeader);
-	}
-
-	private void runLeader() {
-		Round round;
-		lock.lock();
-		try {
-			switch (state) {
-			case CANDIDATE:
-				round = new ElectionRound(this, headIndex);
-				break;
-
-			case LEADER:
-				round = newProposalRound();
-				break;
-
-			case DEPOSED:
-			case SHUTDOWN:
-			default:
-				log.warn("Leader cannot run {}", state); //$NON-NLS-1$
-				// TODO(sop): Redirect proposals.
-				return;
-			}
-		} finally {
-			lock.unlock();
-		}
-
-		try {
-			round.start();
-		} catch (IOException e) {
-			// TODO(sop) Depose leader if it cannot use its repository.
-			log.error(KetchText.get().leaderFailedToStore, e);
-			lock.lock();
-			try {
-				nextRound();
-			} finally {
-				lock.unlock();
-			}
-		}
-	}
-
-	private ProposalRound newProposalRound() {
-		List<Proposal> todo = new ArrayList<>(queued);
-		queued.clear();
-		roundHoldsReferenceToRefTree = true;
-		return new ProposalRound(this, headIndex, todo, refTree);
-	}
-
-	/** @return term of this leader's reign. */
-	long getTerm() {
-		return term;
-	}
-
-	/** @return end of the leader's log. */
-	LogIndex getHead() {
-		return headIndex;
-	}
-
-	/**
-	 * @return state leader knows it has committed across a quorum of replicas.
-	 */
-	LogIndex getCommitted() {
-		return committedIndex;
-	}
-
-	boolean isIdle() {
-		return idle;
-	}
-
-	void runAsync(Round round) {
-		lock.lock();
-		try {
-			// End of the log is this round. Once transport begins it is
-			// reasonable to assume at least one replica will eventually get
-			// this, and there is reasonable probability it commits.
-			headIndex = round.acceptedNewIndex;
-			runningRound = round;
-
-			for (KetchReplica replica : voters) {
-				replica.pushTxnAcceptedAsync(round);
-			}
-			for (KetchReplica replica : followers) {
-				replica.pushTxnAcceptedAsync(round);
-			}
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	/**
-	 * Asynchronous signal from a replica after completion.
-	 * <p>
-	 * Must be called while {@link #lock} is held by the replica.
-	 *
-	 * @param replica
-	 *            replica posting a completion event.
-	 */
-	void onReplicaUpdate(KetchReplica replica) {
-		if (log.isDebugEnabled()) {
-			log.debug("Replica {} finished:\n{}", //$NON-NLS-1$
-					replica.describeForLog(), snapshot());
-		}
-
-		if (replica.getParticipation() == FOLLOWER_ONLY) {
-			// Followers cannot vote, so votes haven't changed.
-			return;
-		} else if (runningRound == null) {
-			// No round running, no need to tally votes.
-			return;
-		}
-
-		assert headIndex.equals(runningRound.acceptedNewIndex);
-		int matching = 0;
-		for (KetchReplica r : voters) {
-			if (r.hasAccepted(headIndex)) {
-				matching++;
-			}
-		}
-
-		int quorum = voters.length / 2 + 1;
-		boolean success = matching >= quorum;
-		if (!success) {
-			return;
-		}
-
-		switch (state) {
-		case CANDIDATE:
-			term = ((ElectionRound) runningRound).getTerm();
-			state = LEADER;
-			if (log.isDebugEnabled()) {
-				log.debug("Won election, running term " + term); //$NON-NLS-1$
-			}
-
-			//$FALL-THROUGH$
-		case LEADER:
-			committedIndex = headIndex;
-			if (log.isDebugEnabled()) {
-				log.debug("Committed {} in term {}", //$NON-NLS-1$
-						committedIndex.describeForLog(),
-						Long.valueOf(term));
-			}
-			nextRound();
-			commitAsync(replica);
-			notifySuccess(runningRound);
-			if (log.isDebugEnabled()) {
-				log.debug("Leader state:\n{}", snapshot()); //$NON-NLS-1$
-			}
-			break;
-
-		default:
-			log.debug("Leader ignoring replica while in {}", state); //$NON-NLS-1$
-			break;
-		}
-	}
-
-	private void notifySuccess(Round round) {
-		// Drop the leader lock while notifying Proposal listeners.
-		lock.unlock();
-		try {
-			round.success();
-		} finally {
-			lock.lock();
-		}
-	}
-
-	private void commitAsync(KetchReplica caller) {
-		for (KetchReplica r : voters) {
-			if (r == caller) {
-				continue;
-			}
-			if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) {
-				r.pushCommitAsync(committedIndex);
-			}
-		}
-		for (KetchReplica r : followers) {
-			if (r == caller) {
-				continue;
-			}
-			if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) {
-				r.pushCommitAsync(committedIndex);
-			}
-		}
-	}
-
-	/** Schedule the next round; invoked while {@link #lock} is held. */
-	void nextRound() {
-		runningRound = null;
-
-		if (queued.isEmpty()) {
-			idle = true;
-		} else {
-			// Caller holds lock. Reschedule leader on a new thread so
-			// the call stack can unwind and lock is not held unexpectedly
-			// during prepare for the next round.
-			scheduleLeader();
-		}
-	}
-
-	/**
-	 * Snapshot this leader
-	 *
-	 * @return snapshot of this leader
-	 */
-	public LeaderSnapshot snapshot() {
-		lock.lock();
-		try {
-			LeaderSnapshot s = new LeaderSnapshot();
-			s.state = state;
-			s.term = term;
-			s.headIndex = headIndex;
-			s.committedIndex = committedIndex;
-			s.idle = isIdle();
-			for (KetchReplica r : voters) {
-				s.replicas.add(r.snapshot());
-			}
-			for (KetchReplica r : followers) {
-				s.replicas.add(r.snapshot());
-			}
-			return s;
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	/**
-	 * Gracefully shutdown this leader and cancel outstanding operations.
-	 */
-	public void shutdown() {
-		lock.lock();
-		try {
-			if (state != SHUTDOWN) {
-				state = SHUTDOWN;
-				for (KetchReplica r : voters) {
-					r.shutdown();
-				}
-				for (KetchReplica r : followers) {
-					r.shutdown();
-				}
-			}
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public String toString() {
-		return snapshot().toString();
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java
deleted file mode 100644
index e01fb3a..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import java.net.URISyntaxException;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
-import org.eclipse.jgit.lib.Repository;
-
-/**
- * A cache of live leader instances, keyed by repository.
- * <p>
- * Ketch only assigns a leader to a repository when needed. If
- * {@link #get(Repository)} is called for a repository that does not have a
- * leader, the leader is created and added to the cache.
- */
-public class KetchLeaderCache {
-	private final KetchSystem system;
-	private final ConcurrentMap<String, KetchLeader> leaders;
-	private final Lock startLock;
-
-	/**
-	 * Initialize a new leader cache.
-	 *
-	 * @param system
-	 *            system configuration for the leaders
-	 */
-	public KetchLeaderCache(KetchSystem system) {
-		this.system = system;
-		leaders = new ConcurrentHashMap<>();
-		startLock = new ReentrantLock(true /* fair */);
-	}
-
-	/**
-	 * Lookup the leader instance for a given repository.
-	 *
-	 * @param repo
-	 *            repository to get the leader for.
-	 * @return the leader instance for the repository.
-	 * @throws java.net.URISyntaxException
-	 *             remote configuration contains an invalid URL.
-	 */
-	public KetchLeader get(Repository repo)
-			throws URISyntaxException {
-		String key = computeKey(repo);
-		KetchLeader leader = leaders.get(key);
-		if (leader != null) {
-			return leader;
-		}
-		return startLeader(key, repo);
-	}
-
-	private KetchLeader startLeader(String key, Repository repo)
-			throws URISyntaxException {
-		startLock.lock();
-		try {
-			KetchLeader leader = leaders.get(key);
-			if (leader != null) {
-				return leader;
-			}
-			leader = system.createLeader(repo);
-			leaders.put(key, leader);
-			return leader;
-		} finally {
-			startLock.unlock();
-		}
-	}
-
-	private static String computeKey(Repository repo) {
-		if (repo instanceof DfsRepository) {
-			DfsRepository dfs = (DfsRepository) repo;
-			return dfs.getDescription().getRepositoryName();
-		}
-
-		if (repo.getDirectory() != null) {
-			return repo.getDirectory().toURI().toString();
-		}
-
-		throw new IllegalArgumentException();
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java
deleted file mode 100644
index 1c9535f..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED;
-import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
-
-import java.io.IOException;
-import java.util.Collection;
-
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.transport.PreReceiveHook;
-import org.eclipse.jgit.transport.ProgressSpinner;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceivePack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * PreReceiveHook for handling push traffic in a Ketch system.
- * <p>
- * Install an instance on {@link org.eclipse.jgit.transport.ReceivePack} to
- * capture the commands and other connection state and relay them through the
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader}, allowing the leader to
- * gain consensus about the new reference state.
- */
-public class KetchPreReceive implements PreReceiveHook {
-	private static final Logger log = LoggerFactory.getLogger(KetchPreReceive.class);
-
-	private final KetchLeader leader;
-
-	/**
-	 * Construct a hook executing updates through a
-	 * {@link org.eclipse.jgit.internal.ketch.KetchLeader}.
-	 *
-	 * @param leader
-	 *            leader for this repository.
-	 */
-	public KetchPreReceive(KetchLeader leader) {
-		this.leader = leader;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> cmds) {
-		cmds = ReceiveCommand.filter(cmds, NOT_ATTEMPTED);
-		if (cmds.isEmpty()) {
-			return;
-		}
-
-		try {
-			Proposal proposal = new Proposal(rp.getRevWalk(), cmds)
-				.setPushCertificate(rp.getPushCertificate())
-				.setAuthor(rp.getRefLogIdent())
-				.setMessage("push"); //$NON-NLS-1$
-			leader.queueProposal(proposal);
-			if (proposal.isDone()) {
-				// This failed fast, e.g. conflict or bad precondition.
-				return;
-			}
-
-			ProgressSpinner spinner = new ProgressSpinner(
-					rp.getMessageOutputStream());
-			if (proposal.getState() == QUEUED) {
-				waitForQueue(proposal, spinner);
-			}
-			if (!proposal.isDone()) {
-				waitForPropose(proposal, spinner);
-			}
-		} catch (IOException | InterruptedException e) {
-			String msg = JGitText.get().transactionAborted;
-			for (ReceiveCommand cmd : cmds) {
-				if (cmd.getResult() == NOT_ATTEMPTED) {
-					cmd.setResult(REJECTED_OTHER_REASON, msg);
-				}
-			}
-			log.error(msg, e);
-		}
-	}
-
-	private void waitForQueue(Proposal proposal, ProgressSpinner spinner)
-			throws InterruptedException {
-		spinner.beginTask(KetchText.get().waitingForQueue, 1, SECONDS);
-		while (!proposal.awaitStateChange(QUEUED, 250, MILLISECONDS)) {
-			spinner.update();
-		}
-		switch (proposal.getState()) {
-		case RUNNING:
-		default:
-			spinner.endTask(KetchText.get().starting);
-			break;
-
-		case EXECUTED:
-			spinner.endTask(KetchText.get().accepted);
-			break;
-
-		case ABORTED:
-			spinner.endTask(KetchText.get().failed);
-			break;
-		}
-	}
-
-	private void waitForPropose(Proposal proposal, ProgressSpinner spinner)
-			throws InterruptedException {
-		spinner.beginTask(KetchText.get().proposingUpdates, 2, SECONDS);
-		while (!proposal.await(250, MILLISECONDS)) {
-			spinner.update();
-		}
-		spinner.endTask(proposal.getState() == EXECUTED
-				? KetchText.get().accepted
-				: KetchText.get().failed);
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
deleted file mode 100644
index a9a694a..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
+++ /dev/null
@@ -1,758 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.BATCHED;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.FAST;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.State.CURRENT;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN;
-import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
-import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-
-import org.eclipse.jgit.annotations.NonNull;
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.internal.storage.reftree.RefTree;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.FileUtils;
-import org.eclipse.jgit.util.SystemReader;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A Ketch replica, either {@link org.eclipse.jgit.internal.ketch.LocalReplica}
- * or {@link org.eclipse.jgit.internal.ketch.RemoteGitReplica}.
- * <p>
- * Replicas can be either a stock Git replica, or a Ketch-aware replica.
- * <p>
- * A stock Git replica has no special knowledge of Ketch and simply stores
- * objects and references. Ketch communicates with the stock Git replica using
- * the Git push wire protocol. The
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader} commits an agreed upon
- * state by pushing all references to the Git replica, for example
- * {@code "refs/heads/master"} is pushed during commit. Stock Git replicas use
- * {@link org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod#ALL_REFS} to
- * record the final state.
- * <p>
- * Ketch-aware replicas understand the {@code RefTree} sent during the proposal
- * and during commit are able to update their own reference space to match the
- * state represented by the {@code RefTree}. Ketch-aware replicas typically use
- * a {@link org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase} and
- * {@link org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod#TXN_COMMITTED}
- * to record the final state.
- * <p>
- * KetchReplica instances are tightly coupled with a single
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader}. Some state may be
- * accessed by the leader thread and uses the leader's own
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader#lock} to protect shared
- * data.
- */
-public abstract class KetchReplica {
-	static final Logger log = LoggerFactory.getLogger(KetchReplica.class);
-	private static final byte[] PEEL = { ' ', '^' };
-
-	/** Participation of a replica in establishing consensus. */
-	public enum Participation {
-		/** Replica can vote. */
-		FULL,
-
-		/** Replica does not vote, but tracks leader. */
-		FOLLOWER_ONLY;
-	}
-
-	/** How this replica wants to receive Ketch commit operations. */
-	public enum CommitMethod {
-		/** All references are pushed to the peer as standard Git. */
-		ALL_REFS,
-
-		/** Only {@code refs/txn/committed} is written/updated. */
-		TXN_COMMITTED;
-	}
-
-	/** Delay before committing to a replica. */
-	public enum CommitSpeed {
-		/**
-		 * Send the commit immediately, even if it could be batched with the
-		 * next proposal.
-		 */
-		FAST,
-
-		/**
-		 * If the next proposal is available, batch the commit with it,
-		 * otherwise just send the commit. This generates less network use, but
-		 * may provide slower consistency on the replica.
-		 */
-		BATCHED;
-	}
-
-	/** Current state of a replica. */
-	public enum State {
-		/** Leader has not yet contacted the replica. */
-		UNKNOWN,
-
-		/** Replica is behind the consensus. */
-		LAGGING,
-
-		/** Replica matches the consensus. */
-		CURRENT,
-
-		/** Replica has a different (or unknown) history. */
-		DIVERGENT,
-
-		/** Replica's history contains the leader's history. */
-		AHEAD,
-
-		/** Replica can not be contacted. */
-		OFFLINE;
-	}
-
-	private final KetchLeader leader;
-	private final String replicaName;
-	private final Participation participation;
-	private final CommitMethod commitMethod;
-	private final CommitSpeed commitSpeed;
-	private final long minRetryMillis;
-	private final long maxRetryMillis;
-	private final Map<ObjectId, List<ReceiveCommand>> staged;
-	private final Map<String, ReceiveCommand> running;
-	private final Map<String, ReceiveCommand> waiting;
-	private final List<ReplicaPushRequest> queued;
-
-	/**
-	 * Value known for {@code "refs/txn/accepted"}.
-	 * <p>
-	 * Raft literature refers to this as {@code matchIndex}.
-	 */
-	private ObjectId txnAccepted;
-
-	/**
-	 * Value known for {@code "refs/txn/committed"}.
-	 * <p>
-	 * Raft literature refers to this as {@code commitIndex}. In traditional
-	 * Raft this is a state variable inside the follower implementation, but
-	 * Ketch keeps it in the leader.
-	 */
-	private ObjectId txnCommitted;
-
-	/** What is happening with this replica. */
-	private State state = UNKNOWN;
-	private String error;
-
-	/** Scheduled retry due to communication failure. */
-	private Future<?> retryFuture;
-	private long lastRetryMillis;
-	private long retryAtMillis;
-
-	/**
-	 * Configure a replica representation.
-	 *
-	 * @param leader
-	 *            instance this replica follows.
-	 * @param name
-	 *            unique-ish name identifying this replica for debugging.
-	 * @param cfg
-	 *            how Ketch should treat the replica.
-	 */
-	protected KetchReplica(KetchLeader leader, String name, ReplicaConfig cfg) {
-		this.leader = leader;
-		this.replicaName = name;
-		this.participation = cfg.getParticipation();
-		this.commitMethod = cfg.getCommitMethod();
-		this.commitSpeed = cfg.getCommitSpeed();
-		this.minRetryMillis = cfg.getMinRetry(MILLISECONDS);
-		this.maxRetryMillis = cfg.getMaxRetry(MILLISECONDS);
-		this.staged = new HashMap<>();
-		this.running = new HashMap<>();
-		this.waiting = new HashMap<>();
-		this.queued = new ArrayList<>(4);
-	}
-
-	/**
-	 * Get system configuration.
-	 *
-	 * @return system configuration.
-	 */
-	public KetchSystem getSystem() {
-		return getLeader().getSystem();
-	}
-
-	/**
-	 * Get leader instance this replica follows.
-	 *
-	 * @return leader instance this replica follows.
-	 */
-	public KetchLeader getLeader() {
-		return leader;
-	}
-
-	/**
-	 * Get unique-ish name for debugging.
-	 *
-	 * @return unique-ish name for debugging.
-	 */
-	public String getName() {
-		return replicaName;
-	}
-
-	/**
-	 * Get description of this replica for error/debug logging purposes.
-	 *
-	 * @return description of this replica for error/debug logging purposes.
-	 */
-	protected String describeForLog() {
-		return getName();
-	}
-
-	/**
-	 * Get how the replica participates in this Ketch system.
-	 *
-	 * @return how the replica participates in this Ketch system.
-	 */
-	public Participation getParticipation() {
-		return participation;
-	}
-
-	/**
-	 * Get how Ketch will commit to the repository.
-	 *
-	 * @return how Ketch will commit to the repository.
-	 */
-	public CommitMethod getCommitMethod() {
-		return commitMethod;
-	}
-
-	/**
-	 * Get when Ketch will commit to the repository.
-	 *
-	 * @return when Ketch will commit to the repository.
-	 */
-	public CommitSpeed getCommitSpeed() {
-		return commitSpeed;
-	}
-
-	/**
-	 * Called by leader to perform graceful shutdown.
-	 * <p>
-	 * Default implementation cancels any scheduled retry. Subclasses may add
-	 * additional logic before or after calling {@code super.shutdown()}.
-	 * <p>
-	 * Called with {@link org.eclipse.jgit.internal.ketch.KetchLeader#lock} held
-	 * by caller.
-	 */
-	protected void shutdown() {
-		Future<?> f = retryFuture;
-		if (f != null) {
-			retryFuture = null;
-			f.cancel(true);
-		}
-	}
-
-	ReplicaSnapshot snapshot() {
-		ReplicaSnapshot s = new ReplicaSnapshot(this);
-		s.accepted = txnAccepted;
-		s.committed = txnCommitted;
-		s.state = state;
-		s.error = error;
-		s.retryAtMillis = waitingForRetry() ? retryAtMillis : 0;
-		return s;
-	}
-
-	/**
-	 * Update the leader's view of the replica after a poll.
-	 * <p>
-	 * Called with {@link KetchLeader#lock} held by caller.
-	 *
-	 * @param refs
-	 *            map of refs from the replica.
-	 */
-	void initialize(Map<String, Ref> refs) {
-		if (txnAccepted == null) {
-			txnAccepted = getId(refs.get(getSystem().getTxnAccepted()));
-		}
-		if (txnCommitted == null) {
-			txnCommitted = getId(refs.get(getSystem().getTxnCommitted()));
-		}
-	}
-
-	ObjectId getTxnAccepted() {
-		return txnAccepted;
-	}
-
-	boolean hasAccepted(LogIndex id) {
-		return equals(txnAccepted, id);
-	}
-
-	private static boolean equals(@Nullable ObjectId a, LogIndex b) {
-		return a != null && b != null && AnyObjectId.isEqual(a, b);
-	}
-
-	/**
-	 * Schedule a proposal round with the replica.
-	 * <p>
-	 * Called with {@link KetchLeader#lock} held by caller.
-	 *
-	 * @param round
-	 *            current round being run by the leader.
-	 */
-	void pushTxnAcceptedAsync(Round round) {
-		List<ReceiveCommand> cmds = new ArrayList<>();
-		if (commitSpeed == BATCHED) {
-			LogIndex committedIndex = leader.getCommitted();
-			if (equals(txnAccepted, committedIndex)
-					&& !equals(txnCommitted, committedIndex)) {
-				prepareTxnCommitted(cmds, committedIndex);
-			}
-		}
-
-		// TODO(sop) Lagging replicas should build accept on the fly.
-		if (round.stageCommands != null) {
-			for (ReceiveCommand cmd : round.stageCommands) {
-				// TODO(sop): Do not send certain object graphs to replica.
-				cmds.add(copy(cmd));
-			}
-		}
-		cmds.add(new ReceiveCommand(
-				round.acceptedOldIndex, round.acceptedNewIndex,
-				getSystem().getTxnAccepted()));
-		pushAsync(new ReplicaPushRequest(this, cmds));
-	}
-
-	private static ReceiveCommand copy(ReceiveCommand c) {
-		return new ReceiveCommand(c.getOldId(), c.getNewId(), c.getRefName());
-	}
-
-	boolean shouldPushUnbatchedCommit(LogIndex committed, boolean leaderIdle) {
-		return (leaderIdle || commitSpeed == FAST) && hasAccepted(committed);
-	}
-
-	void pushCommitAsync(LogIndex committed) {
-		List<ReceiveCommand> cmds = new ArrayList<>();
-		prepareTxnCommitted(cmds, committed);
-		pushAsync(new ReplicaPushRequest(this, cmds));
-	}
-
-	private void prepareTxnCommitted(List<ReceiveCommand> cmds,
-			ObjectId committed) {
-		removeStaged(cmds, committed);
-		cmds.add(new ReceiveCommand(
-				txnCommitted, committed,
-				getSystem().getTxnCommitted()));
-	}
-
-	private void removeStaged(List<ReceiveCommand> cmds, ObjectId committed) {
-		List<ReceiveCommand> a = staged.remove(committed);
-		if (a != null) {
-			delete(cmds, a);
-		}
-		if (staged.isEmpty() || !(committed instanceof LogIndex)) {
-			return;
-		}
-
-		LogIndex committedIndex = (LogIndex) committed;
-		Iterator<Map.Entry<ObjectId, List<ReceiveCommand>>> itr = staged
-				.entrySet().iterator();
-		while (itr.hasNext()) {
-			Map.Entry<ObjectId, List<ReceiveCommand>> e = itr.next();
-			if (e.getKey() instanceof LogIndex) {
-				LogIndex stagedIndex = (LogIndex) e.getKey();
-				if (stagedIndex.isBefore(committedIndex)) {
-					delete(cmds, e.getValue());
-					itr.remove();
-				}
-			}
-		}
-	}
-
-	private static void delete(List<ReceiveCommand> cmds,
-			List<ReceiveCommand> createCmds) {
-		for (ReceiveCommand cmd : createCmds) {
-			ObjectId id = cmd.getNewId();
-			String name = cmd.getRefName();
-			cmds.add(new ReceiveCommand(id, ObjectId.zeroId(), name));
-		}
-	}
-
-	/**
-	 * Determine the next push for this replica (if any) and start it.
-	 * <p>
-	 * If the replica has successfully accepted the committed state of the
-	 * leader, this method will push all references to the replica using the
-	 * configured {@link CommitMethod}.
-	 * <p>
-	 * If the replica is {@link State#LAGGING} this method will begin catch up
-	 * by sending a more recent {@code refs/txn/accepted}.
-	 * <p>
-	 * Must be invoked with {@link KetchLeader#lock} held by caller.
-	 */
-	private void runNextPushRequest() {
-		LogIndex committed = leader.getCommitted();
-		if (!equals(txnCommitted, committed)
-				&& shouldPushUnbatchedCommit(committed, leader.isIdle())) {
-			pushCommitAsync(committed);
-		}
-
-		if (queued.isEmpty() || !running.isEmpty() || waitingForRetry()) {
-			return;
-		}
-
-		// Collapse all queued requests into a single request.
-		Map<String, ReceiveCommand> cmdMap = new HashMap<>();
-		for (ReplicaPushRequest req : queued) {
-			for (ReceiveCommand cmd : req.getCommands()) {
-				String name = cmd.getRefName();
-				ReceiveCommand old = cmdMap.remove(name);
-				if (old != null) {
-					cmd = new ReceiveCommand(
-							old.getOldId(), cmd.getNewId(),
-							name);
-				}
-				cmdMap.put(name, cmd);
-			}
-		}
-		queued.clear();
-		waiting.clear();
-
-		List<ReceiveCommand> next = new ArrayList<>(cmdMap.values());
-		for (ReceiveCommand cmd : next) {
-			running.put(cmd.getRefName(), cmd);
-		}
-		startPush(new ReplicaPushRequest(this, next));
-	}
-
-	private void pushAsync(ReplicaPushRequest req) {
-		if (defer(req)) {
-			// TODO(sop) Collapse during long retry outage.
-			for (ReceiveCommand cmd : req.getCommands()) {
-				waiting.put(cmd.getRefName(), cmd);
-			}
-			queued.add(req);
-		} else {
-			for (ReceiveCommand cmd : req.getCommands()) {
-				running.put(cmd.getRefName(), cmd);
-			}
-			startPush(req);
-		}
-	}
-
-	private boolean defer(ReplicaPushRequest req) {
-		if (waitingForRetry()) {
-			// Prior communication failure; everything is deferred.
-			return true;
-		}
-
-		for (ReceiveCommand nextCmd : req.getCommands()) {
-			ReceiveCommand priorCmd = waiting.get(nextCmd.getRefName());
-			if (priorCmd == null) {
-				priorCmd = running.get(nextCmd.getRefName());
-			}
-			if (priorCmd != null) {
-				// Another request pending on same ref; that must go first.
-				// Verify priorCmd.newId == nextCmd.oldId?
-				return true;
-			}
-		}
-		return false;
-	}
-
-	private boolean waitingForRetry() {
-		Future<?> f = retryFuture;
-		return f != null && !f.isDone();
-	}
-
-	private void retryLater(ReplicaPushRequest req) {
-		Collection<ReceiveCommand> cmds = req.getCommands();
-		for (ReceiveCommand cmd : cmds) {
-			cmd.setResult(NOT_ATTEMPTED, null);
-			if (!waiting.containsKey(cmd.getRefName())) {
-				waiting.put(cmd.getRefName(), cmd);
-			}
-		}
-		queued.add(0, new ReplicaPushRequest(this, cmds));
-
-		if (!waitingForRetry()) {
-			long delay = FileUtils
-				.delay(lastRetryMillis, minRetryMillis, maxRetryMillis);
-			if (log.isDebugEnabled()) {
-				log.debug("Retrying {} after {} ms", //$NON-NLS-1$
-						describeForLog(), Long.valueOf(delay));
-			}
-			lastRetryMillis = delay;
-			retryAtMillis = SystemReader.getInstance().getCurrentTime() + delay;
-			retryFuture = getSystem().getExecutor()
-					.schedule(new WeakRetryPush(this), delay, MILLISECONDS);
-		}
-	}
-
-	/** Weakly holds a retrying replica, allowing it to garbage collect. */
-	static class WeakRetryPush extends WeakReference<KetchReplica>
-			implements Callable<Void> {
-		WeakRetryPush(KetchReplica r) {
-			super(r);
-		}
-
-		@Override
-		public Void call() throws Exception {
-			KetchReplica r = get();
-			if (r != null) {
-				r.doRetryPush();
-			}
-			return null;
-		}
-	}
-
-	private void doRetryPush() {
-		leader.lock.lock();
-		try {
-			retryFuture = null;
-			runNextPushRequest();
-		} finally {
-			leader.lock.unlock();
-		}
-	}
-
-	/**
-	 * Begin executing a single push.
-	 * <p>
-	 * This method must move processing onto another thread. Called with
-	 * {@link org.eclipse.jgit.internal.ketch.KetchLeader#lock} held by caller.
-	 *
-	 * @param req
-	 *            the request to send to the replica.
-	 */
-	protected abstract void startPush(ReplicaPushRequest req);
-
-	/**
-	 * Callback from {@link ReplicaPushRequest} upon success or failure.
-	 * <p>
-	 * Acquires the {@link KetchLeader#lock} and updates the leader's internal
-	 * knowledge about this replica to reflect what has been learned during a
-	 * push to the replica. In some cases of divergence this method may take
-	 * some time to determine how the replica has diverged; to reduce contention
-	 * this is evaluated before acquiring the leader lock.
-	 *
-	 * @param repo
-	 *            local repository instance used by the push thread.
-	 * @param req
-	 *            push request just attempted.
-	 */
-	void afterPush(@Nullable Repository repo, ReplicaPushRequest req) {
-		ReceiveCommand acceptCmd = null;
-		ReceiveCommand commitCmd = null;
-		List<ReceiveCommand> stages = null;
-
-		for (ReceiveCommand cmd : req.getCommands()) {
-			String name = cmd.getRefName();
-			if (name.equals(getSystem().getTxnAccepted())) {
-				acceptCmd = cmd;
-			} else if (name.equals(getSystem().getTxnCommitted())) {
-				commitCmd = cmd;
-			} else if (cmd.getResult() == OK && cmd.getType() == CREATE
-					&& name.startsWith(getSystem().getTxnStage())) {
-				if (stages == null) {
-					stages = new ArrayList<>();
-				}
-				stages.add(cmd);
-			}
-		}
-
-		State newState = null;
-		ObjectId acceptId = readId(req, acceptCmd);
-		if (repo != null && acceptCmd != null && acceptCmd.getResult() != OK
-				&& req.getException() == null) {
-			try (LagCheck lag = new LagCheck(this, repo)) {
-				newState = lag.check(acceptId, acceptCmd);
-				acceptId = lag.getRemoteId();
-			}
-		}
-
-		leader.lock.lock();
-		try {
-			for (ReceiveCommand cmd : req.getCommands()) {
-				running.remove(cmd.getRefName());
-			}
-
-			Throwable err = req.getException();
-			if (err != null) {
-				state = OFFLINE;
-				error = err.toString();
-				retryLater(req);
-				leader.onReplicaUpdate(this);
-				return;
-			}
-
-			lastRetryMillis = 0;
-			error = null;
-			updateView(req, acceptId, commitCmd);
-
-			if (acceptCmd != null && acceptCmd.getResult() == OK) {
-				state = hasAccepted(leader.getHead()) ? CURRENT : LAGGING;
-				if (stages != null) {
-					staged.put(acceptCmd.getNewId(), stages);
-				}
-			} else if (newState != null) {
-				state = newState;
-			}
-
-			leader.onReplicaUpdate(this);
-			runNextPushRequest();
-		} finally {
-			leader.lock.unlock();
-		}
-	}
-
-	private void updateView(ReplicaPushRequest req, @Nullable ObjectId acceptId,
-			ReceiveCommand commitCmd) {
-		if (acceptId != null) {
-			txnAccepted = acceptId;
-		}
-
-		ObjectId committed = readId(req, commitCmd);
-		if (committed != null) {
-			txnCommitted = committed;
-		} else if (acceptId != null && txnCommitted == null) {
-			// Initialize during first conversation.
-			Map<String, Ref> adv = req.getRefs();
-			if (adv != null) {
-				Ref refs = adv.get(getSystem().getTxnCommitted());
-				txnCommitted = getId(refs);
-			}
-		}
-	}
-
-	@Nullable
-	private static ObjectId readId(ReplicaPushRequest req,
-			@Nullable ReceiveCommand cmd) {
-		if (cmd == null) {
-			// Ref was not in the command list, do not trust advertisement.
-			return null;
-
-		} else if (cmd.getResult() == OK) {
-			// Currently at newId.
-			return cmd.getNewId();
-		}
-
-		Map<String, Ref> refs = req.getRefs();
-		return refs != null ? getId(refs.get(cmd.getRefName())) : null;
-	}
-
-	/**
-	 * Fetch objects from the remote using the calling thread.
-	 * <p>
-	 * Called without {@link org.eclipse.jgit.internal.ketch.KetchLeader#lock}.
-	 *
-	 * @param repo
-	 *            local repository to fetch objects into.
-	 * @param req
-	 *            the request to fetch from a replica.
-	 * @throws java.io.IOException
-	 *             communication with the replica was not possible.
-	 */
-	protected abstract void blockingFetch(Repository repo,
-			ReplicaFetchRequest req) throws IOException;
-
-	/**
-	 * Build a list of commands to commit
-	 * {@link org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod#ALL_REFS}.
-	 *
-	 * @param git
-	 *            local leader repository to read committed state from.
-	 * @param current
-	 *            all known references in the replica's repository. Typically
-	 *            this comes from a push advertisement.
-	 * @param committed
-	 *            state being pushed to {@code refs/txn/committed}.
-	 * @return commands to update during commit.
-	 * @throws java.io.IOException
-	 *             cannot read the committed state.
-	 */
-	protected Collection<ReceiveCommand> prepareCommit(Repository git,
-			Map<String, Ref> current, ObjectId committed) throws IOException {
-		List<ReceiveCommand> delta = new ArrayList<>();
-		Map<String, Ref> remote = new HashMap<>(current);
-		try (RevWalk rw = new RevWalk(git);
-				TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
-			tw.setRecursive(true);
-			tw.addTree(rw.parseCommit(committed).getTree());
-			while (tw.next()) {
-				if (tw.getRawMode(0) != TYPE_GITLINK
-						|| tw.isPathSuffix(PEEL, 2)) {
-					// Symbolic references cannot be pushed.
-					// Caching peeled values is handled remotely.
-					continue;
-				}
-
-				// TODO(sop) Do not send certain ref names to replica.
-				String name = RefTree.refName(tw.getPathString());
-				Ref oldRef = remote.remove(name);
-				ObjectId oldId = getId(oldRef);
-				ObjectId newId = tw.getObjectId(0);
-				if (!AnyObjectId.isEqual(oldId, newId)) {
-					delta.add(new ReceiveCommand(oldId, newId, name));
-				}
-			}
-		}
-
-		// Delete any extra references not in the committed state.
-		for (Ref ref : remote.values()) {
-			if (canDelete(ref)) {
-				delta.add(new ReceiveCommand(
-					ref.getObjectId(), ObjectId.zeroId(),
-					ref.getName()));
-			}
-		}
-		return delta;
-	}
-
-	boolean canDelete(Ref ref) {
-		String name = ref.getName();
-		if (HEAD.equals(name)) {
-			return false;
-		}
-		if (name.startsWith(getSystem().getTxnNamespace())) {
-			return false;
-		}
-		// TODO(sop) Do not delete precious names from replica.
-		return true;
-	}
-
-	@NonNull
-	static ObjectId getId(@Nullable Ref ref) {
-		if (ref != null) {
-			ObjectId id = ref.getObjectId();
-			if (id != null) {
-				return id;
-			}
-		}
-		return ObjectId.zeroId();
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java
deleted file mode 100644
index 8ad1d60..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java
+++ /dev/null
@@ -1,320 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static org.eclipse.jgit.internal.ketch.KetchConstants.ACCEPTED;
-import static org.eclipse.jgit.internal.ketch.KetchConstants.COMMITTED;
-import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE;
-import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_SECTION_KETCH;
-import static org.eclipse.jgit.internal.ketch.KetchConstants.DEFAULT_TXN_NAMESPACE;
-import static org.eclipse.jgit.internal.ketch.KetchConstants.STAGE;
-import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_NAME;
-import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE;
-
-import java.net.URISyntaxException;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Random;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.URIish;
-import org.eclipse.jgit.util.time.MonotonicClock;
-import org.eclipse.jgit.util.time.MonotonicSystemClock;
-import org.eclipse.jgit.util.time.ProposedTimestamp;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Ketch system-wide configuration.
- * <p>
- * This class provides useful defaults for testing and small proof of concepts.
- * Full scale installations are expected to subclass and override methods to
- * provide consistent configuration across all managed repositories.
- * <p>
- * Servers should configure their own
- * {@link java.util.concurrent.ScheduledExecutorService}.
- */
-public class KetchSystem {
-	private static final Random RNG = new Random();
-
-	/**
-	 * Get default executor, one thread per available processor.
-	 *
-	 * @return default executor, one thread per available processor.
-	 */
-	public static ScheduledExecutorService defaultExecutor() {
-		return DefaultExecutorHolder.I;
-	}
-
-	private final ScheduledExecutorService executor;
-	private final MonotonicClock clock;
-	private final String txnNamespace;
-	private final String txnAccepted;
-	private final String txnCommitted;
-	private final String txnStage;
-
-	/**
-	 * Create a default system with a thread pool of 1 thread per CPU.
-	 */
-	public KetchSystem() {
-		this(defaultExecutor(), new MonotonicSystemClock(), DEFAULT_TXN_NAMESPACE);
-	}
-
-	/**
-	 * Create a Ketch system with the provided executor service.
-	 *
-	 * @param executor
-	 *            thread pool to run background operations.
-	 * @param clock
-	 *            clock to create timestamps.
-	 * @param txnNamespace
-	 *            reference namespace for the RefTree graph and associated
-	 *            transaction state. Must begin with {@code "refs/"} and end
-	 *            with {@code '/'}, for example {@code "refs/txn/"}.
-	 */
-	public KetchSystem(ScheduledExecutorService executor, MonotonicClock clock,
-			String txnNamespace) {
-		this.executor = executor;
-		this.clock = clock;
-		this.txnNamespace = txnNamespace;
-		this.txnAccepted = txnNamespace + ACCEPTED;
-		this.txnCommitted = txnNamespace + COMMITTED;
-		this.txnStage = txnNamespace + STAGE;
-	}
-
-	/**
-	 * Get executor to perform background operations.
-	 *
-	 * @return executor to perform background operations.
-	 */
-	public ScheduledExecutorService getExecutor() {
-		return executor;
-	}
-
-	/**
-	 * Get clock to obtain timestamps from.
-	 *
-	 * @return clock to obtain timestamps from.
-	 */
-	public MonotonicClock getClock() {
-		return clock;
-	}
-
-	/**
-	 * Get how long the leader will wait for the {@link #getClock()}'s
-	 * {@code ProposedTimestamp} used in commits proposed to the RefTree graph
-	 * ({@link #getTxnAccepted()})
-	 *
-	 * @return how long the leader will wait for the {@link #getClock()}'s
-	 *         {@code ProposedTimestamp} used in commits proposed to the RefTree
-	 *         graph ({@link #getTxnAccepted()}). Defaults to 5 seconds.
-	 */
-	public Duration getMaxWaitForMonotonicClock() {
-		return Duration.ofSeconds(5);
-	}
-
-	/**
-	 * Whether elections should require monotonically increasing commit
-	 * timestamps
-	 *
-	 * @return {@code true} if elections should require monotonically increasing
-	 *         commit timestamps. This requires a very good
-	 *         {@link org.eclipse.jgit.util.time.MonotonicClock}.
-	 */
-	public boolean requireMonotonicLeaderElections() {
-		return false;
-	}
-
-	/**
-	 * Get the namespace used for the RefTree graph and transaction management.
-	 *
-	 * @return reference namespace such as {@code "refs/txn/"}.
-	 */
-	public String getTxnNamespace() {
-		return txnNamespace;
-	}
-
-	/**
-	 * Get name of the accepted RefTree graph.
-	 *
-	 * @return name of the accepted RefTree graph.
-	 */
-	public String getTxnAccepted() {
-		return txnAccepted;
-	}
-
-	/**
-	 * Get name of the committed RefTree graph.
-	 *
-	 * @return name of the committed RefTree graph.
-	 */
-	public String getTxnCommitted() {
-		return txnCommitted;
-	}
-
-	/**
-	 * Get prefix for staged objects, e.g. {@code "refs/txn/stage/"}.
-	 *
-	 * @return prefix for staged objects, e.g. {@code "refs/txn/stage/"}.
-	 */
-	public String getTxnStage() {
-		return txnStage;
-	}
-
-	/**
-	 * Create new committer {@code PersonIdent} for ketch system
-	 *
-	 * @param time
-	 *            timestamp for the committer.
-	 * @return identity line for the committer header of a RefTreeGraph.
-	 */
-	public PersonIdent newCommitter(ProposedTimestamp time) {
-		String name = "ketch"; //$NON-NLS-1$
-		String email = "ketch@system"; //$NON-NLS-1$
-		return new PersonIdent(name, email, time);
-	}
-
-	/**
-	 * Construct a random tag to identify a candidate during leader election.
-	 * <p>
-	 * Multiple processes trying to elect themselves leaders at exactly the same
-	 * time (rounded to seconds) using the same
-	 * {@link #newCommitter(ProposedTimestamp)} identity strings, for the same
-	 * term, may generate the same ObjectId for the election commit and falsely
-	 * assume they have both won.
-	 * <p>
-	 * Candidates add this tag to their election ballot commit to disambiguate
-	 * the election. The tag only needs to be unique for a given triplet of
-	 * {@link #newCommitter(ProposedTimestamp)}, system time (rounded to
-	 * seconds), and term. If every replica in the system uses a unique
-	 * {@code newCommitter} (such as including the host name after the
-	 * {@code "@"} in the email address) the tag could be the empty string.
-	 * <p>
-	 * The default implementation generates a few bytes of random data.
-	 *
-	 * @return unique tag; null or empty string if {@code newCommitter()} is
-	 *         sufficiently unique to identify the leader.
-	 */
-	@Nullable
-	public String newLeaderTag() {
-		int n = RNG.nextInt(1 << (6 * 4));
-		return String.format("%06x", Integer.valueOf(n)); //$NON-NLS-1$
-	}
-
-	/**
-	 * Construct the KetchLeader instance of a repository.
-	 *
-	 * @param repo
-	 *            local repository stored by the leader.
-	 * @return leader instance.
-	 * @throws java.net.URISyntaxException
-	 *             a follower configuration contains an unsupported URI.
-	 */
-	public KetchLeader createLeader(Repository repo)
-			throws URISyntaxException {
-		KetchLeader leader = new KetchLeader(this) {
-			@Override
-			protected Repository openRepository() {
-				repo.incrementOpen();
-				return repo;
-			}
-		};
-		leader.setReplicas(createReplicas(leader, repo));
-		return leader;
-	}
-
-	/**
-	 * Get the collection of replicas for a repository.
-	 * <p>
-	 * The collection of replicas must include the local repository.
-	 *
-	 * @param leader
-	 *            the leader driving these replicas.
-	 * @param repo
-	 *            repository to get the replicas of.
-	 * @return collection of replicas for the specified repository.
-	 * @throws java.net.URISyntaxException
-	 *             a configured URI is invalid.
-	 */
-	protected List<KetchReplica> createReplicas(KetchLeader leader,
-			Repository repo) throws URISyntaxException {
-		List<KetchReplica> replicas = new ArrayList<>();
-		Config cfg = repo.getConfig();
-		String localName = getLocalName(cfg);
-		for (String name : cfg.getSubsections(CONFIG_KEY_REMOTE)) {
-			if (!hasParticipation(cfg, name)) {
-				continue;
-			}
-
-			ReplicaConfig kc = ReplicaConfig.newFromConfig(cfg, name);
-			if (name.equals(localName)) {
-				replicas.add(new LocalReplica(leader, name, kc));
-				continue;
-			}
-
-			RemoteConfig rc = new RemoteConfig(cfg, name);
-			List<URIish> uris = rc.getPushURIs();
-			if (uris.isEmpty()) {
-				uris = rc.getURIs();
-			}
-			for (URIish uri : uris) {
-				String n = uris.size() == 1 ? name : uri.getHost();
-				replicas.add(new RemoteGitReplica(leader, n, uri, kc, rc));
-			}
-		}
-		return replicas;
-	}
-
-	private static boolean hasParticipation(Config cfg, String name) {
-		return cfg.getString(CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE) != null;
-	}
-
-	private static String getLocalName(Config cfg) {
-		return cfg.getString(CONFIG_SECTION_KETCH, null, CONFIG_KEY_NAME);
-	}
-
-	static class DefaultExecutorHolder {
-		private static final Logger log = LoggerFactory.getLogger(KetchSystem.class);
-		static final ScheduledExecutorService I = create();
-
-		private static ScheduledExecutorService create() {
-			int cores = Runtime.getRuntime().availableProcessors();
-			int threads = Math.max(5, cores);
-			log.info("Using {} threads", Integer.valueOf(threads)); //$NON-NLS-1$
-			return Executors.newScheduledThreadPool(
-				threads,
-				new ThreadFactory() {
-					private final AtomicInteger threadCnt = new AtomicInteger();
-
-					@Override
-					public Thread newThread(Runnable r) {
-						int id = threadCnt.incrementAndGet();
-						Thread thr = new Thread(r);
-						thr.setName("KetchExecutor-" + id); //$NON-NLS-1$
-						return thr;
-					}
-				});
-		}
-
-		private DefaultExecutorHolder() {
-		}
-	}
-
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java
deleted file mode 100644
index 6f9038b..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import org.eclipse.jgit.nls.NLS;
-import org.eclipse.jgit.nls.TranslationBundle;
-
-/**
- * Translation bundle for the Ketch implementation.
- */
-public class KetchText extends TranslationBundle {
-	/**
-	 * Get an instance of this translation bundle.
-	 *
-	 * @return instance of this translation bundle.
-	 */
-	public static KetchText get() {
-		return NLS.getBundleFor(KetchText.class);
-	}
-
-	// @formatter:off
-	/***/ public String accepted;
-	/***/ public String cannotFetchFromLocalReplica;
-	/***/ public String failed;
-	/***/ public String invalidFollowerUri;
-	/***/ public String leaderFailedToStore;
-	/***/ public String localReplicaRequired;
-	/***/ public String mismatchedTxnNamespace;
-	/***/ public String outsideTxnNamespace;
-	/***/ public String proposingUpdates;
-	/***/ public String queuedProposalFailedToApply;
-	/***/ public String starting;
-	/***/ public String unsupportedVoterCount;
-	/***/ public String waitingForQueue;
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
deleted file mode 100644
index 1f8384f..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static org.eclipse.jgit.internal.ketch.KetchReplica.State.AHEAD;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.State.DIVERGENT;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN;
-import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Map;
-
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-/**
- * A helper to check if a {@link KetchReplica} is ahead or behind the leader.
- */
-class LagCheck implements AutoCloseable {
-	private final KetchReplica replica;
-	private final Repository repo;
-	private RevWalk rw;
-	private ObjectId remoteId;
-
-	LagCheck(KetchReplica replica, Repository repo) {
-		this.replica = replica;
-		this.repo = repo;
-		initRevWalk();
-	}
-
-	private void initRevWalk() {
-		if (rw != null) {
-			rw.close();
-		}
-
-		rw = new RevWalk(repo);
-		rw.setRetainBody(false);
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public void close() {
-		if (rw != null) {
-			rw.close();
-			rw = null;
-		}
-	}
-
-	ObjectId getRemoteId() {
-		return remoteId;
-	}
-
-	KetchReplica.State check(ObjectId acceptId, ReceiveCommand acceptCmd) {
-		remoteId = acceptId;
-		if (remoteId == null) {
-			// Nothing advertised by the replica, value is unknown.
-			return UNKNOWN;
-		}
-
-		if (AnyObjectId.isEqual(remoteId, ObjectId.zeroId())) {
-			// Replica does not have the txnAccepted reference.
-			return LAGGING;
-		}
-
-		try {
-			RevCommit remote;
-			try {
-				remote = parseRemoteCommit(acceptCmd.getRefName());
-			} catch (RefGoneException gone) {
-				// Replica does not have the txnAccepted reference.
-				return LAGGING;
-			} catch (MissingObjectException notFound) {
-				// Local repository does not know this commit so it cannot
-				// be including the replica's log.
-				return DIVERGENT;
-			}
-
-			RevCommit head = rw.parseCommit(acceptCmd.getNewId());
-			if (rw.isMergedInto(remote, head)) {
-				return LAGGING;
-			}
-
-			// TODO(sop) Check term to see if my leader was deposed.
-			if (rw.isMergedInto(head, remote)) {
-				return AHEAD;
-			}
-			return DIVERGENT;
-		} catch (IOException err) {
-			KetchReplica.log.error(String.format(
-					"Cannot compare %s", //$NON-NLS-1$
-					acceptCmd.getRefName()), err);
-			return UNKNOWN;
-		}
-	}
-
-	private RevCommit parseRemoteCommit(String refName)
-			throws IOException, MissingObjectException, RefGoneException {
-		try {
-			return rw.parseCommit(remoteId);
-		} catch (MissingObjectException notLocal) {
-			// Fall through and try to acquire the object by fetching it.
-		}
-
-		ReplicaFetchRequest fetch = new ReplicaFetchRequest(
-				Collections.singleton(refName),
-				Collections.<ObjectId> emptySet());
-		try {
-			replica.blockingFetch(repo, fetch);
-		} catch (IOException fetchErr) {
-			KetchReplica.log.error(String.format(
-					"Cannot fetch %s (%s) from %s", //$NON-NLS-1$
-					remoteId.abbreviate(8).name(), refName,
-					replica.describeForLog()), fetchErr);
-			throw new MissingObjectException(remoteId, OBJ_COMMIT);
-		}
-
-		Map<String, Ref> adv = fetch.getRefs();
-		if (adv == null) {
-			throw new MissingObjectException(remoteId, OBJ_COMMIT);
-		}
-
-		Ref ref = adv.get(refName);
-		if (ref == null || ref.getObjectId() == null) {
-			throw new RefGoneException();
-		}
-
-		initRevWalk();
-		remoteId = ref.getObjectId();
-		return rw.parseCommit(remoteId);
-	}
-
-	private static class RefGoneException extends Exception {
-		private static final long serialVersionUID = 1L;
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java
deleted file mode 100644
index ce0672c..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.lib.ObjectId;
-
-/**
- * A snapshot of a leader and its view of the world.
- */
-public class LeaderSnapshot {
-	final List<ReplicaSnapshot> replicas = new ArrayList<>();
-	KetchLeader.State state;
-	long term;
-	LogIndex headIndex;
-	LogIndex committedIndex;
-	boolean idle;
-
-	LeaderSnapshot() {
-	}
-
-	/**
-	 * Get unmodifiable view of configured replicas.
-	 *
-	 * @return unmodifiable view of configured replicas.
-	 */
-	public Collection<ReplicaSnapshot> getReplicas() {
-		return Collections.unmodifiableList(replicas);
-	}
-
-	/**
-	 * Get current state of the leader.
-	 *
-	 * @return current state of the leader.
-	 */
-	public KetchLeader.State getState() {
-		return state;
-	}
-
-	/**
-	 * Whether the leader is not running a round to reach consensus, and has no
-	 * rounds queued.
-	 *
-	 * @return {@code true} if the leader is not running a round to reach
-	 *         consensus, and has no rounds queued.
-	 */
-	public boolean isIdle() {
-		return idle;
-	}
-
-	/**
-	 * Get term of this leader
-	 *
-	 * @return term of this leader. Valid only if {@link #getState()} is
-	 *         currently
-	 *         {@link org.eclipse.jgit.internal.ketch.KetchLeader.State#LEADER}.
-	 */
-	public long getTerm() {
-		return term;
-	}
-
-	/**
-	 * Get end of the leader's log
-	 *
-	 * @return end of the leader's log; null if leader hasn't started up enough
-	 *         to begin its own election.
-	 */
-	@Nullable
-	public LogIndex getHead() {
-		return headIndex;
-	}
-
-	/**
-	 * Get state the leader knows is committed on a majority of participant
-	 * replicas
-	 *
-	 * @return state the leader knows is committed on a majority of participant
-	 *         replicas. Null until the leader instance has committed a log
-	 *         index within its own term.
-	 */
-	@Nullable
-	public LogIndex getCommitted() {
-		return committedIndex;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public String toString() {
-		StringBuilder s = new StringBuilder();
-		s.append(isIdle() ? "IDLE" : "RUNNING"); //$NON-NLS-1$ //$NON-NLS-2$
-		s.append(" state ").append(getState()); //$NON-NLS-1$
-		if (getTerm() > 0) {
-			s.append(" term ").append(getTerm()); //$NON-NLS-1$
-		}
-		s.append('\n');
-		s.append(String.format(
-				"%-10s %12s %12s\n", //$NON-NLS-1$
-				"Replica", "Accepted", "Committed")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
-		s.append("------------------------------------\n"); //$NON-NLS-1$
-		debug(s, "(leader)", getHead(), getCommitted()); //$NON-NLS-1$
-		s.append('\n');
-		for (ReplicaSnapshot r : getReplicas()) {
-			debug(s, r);
-			s.append('\n');
-		}
-		s.append('\n');
-		return s.toString();
-	}
-
-	private static void debug(StringBuilder b, ReplicaSnapshot s) {
-		KetchReplica replica = s.getReplica();
-		debug(b, replica.getName(), s.getAccepted(), s.getCommitted());
-		b.append(String.format(" %-8s %s", //$NON-NLS-1$
-				replica.getParticipation(), s.getState()));
-		if (s.getState() == OFFLINE) {
-			String err = s.getErrorMessage();
-			if (err != null) {
-				b.append(" (").append(err).append(')'); //$NON-NLS-1$
-			}
-		}
-	}
-
-	private static void debug(StringBuilder s, String name,
-			ObjectId accepted, ObjectId committed) {
-		s.append(String.format(
-				"%-10s %-12s %-12s", //$NON-NLS-1$
-				name, str(accepted), str(committed)));
-	}
-
-	static String str(ObjectId c) {
-		if (c instanceof LogIndex) {
-			return ((LogIndex) c).describeForLog();
-		} else if (c != null) {
-			return c.abbreviate(8).name();
-		}
-		return "-"; //$NON-NLS-1$
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java
deleted file mode 100644
index b2d59d7..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS;
-import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.TXN_COMMITTED;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
-
-import java.io.IOException;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.util.time.MonotonicClock;
-import org.eclipse.jgit.util.time.ProposedTimestamp;
-
-/**
- * Ketch replica running on the same system as the
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader}.
- */
-public class LocalReplica extends KetchReplica {
-	/**
-	 * Configure a local replica.
-	 *
-	 * @param leader
-	 *            instance this replica follows.
-	 * @param name
-	 *            unique-ish name identifying this replica for debugging.
-	 * @param cfg
-	 *            how Ketch should treat the local system.
-	 */
-	public LocalReplica(KetchLeader leader, String name, ReplicaConfig cfg) {
-		super(leader, name, cfg);
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected String describeForLog() {
-		return String.format("%s (leader)", getName()); //$NON-NLS-1$
-	}
-
-	/**
-	 * Initializes local replica by reading accepted and committed references.
-	 * <p>
-	 * Loads accepted and committed references from the reference database of
-	 * the local replica and stores their current ObjectIds in memory.
-	 *
-	 * @param repo
-	 *            repository to initialize state from.
-	 * @throws IOException
-	 *             cannot read repository state.
-	 */
-	void initialize(Repository repo) throws IOException {
-		RefDatabase refdb = repo.getRefDatabase();
-		if (refdb instanceof RefTreeDatabase) {
-			RefTreeDatabase treeDb = (RefTreeDatabase) refdb;
-			String txnNamespace = getSystem().getTxnNamespace();
-			if (!txnNamespace.equals(treeDb.getTxnNamespace())) {
-				throw new IOException(MessageFormat.format(
-						KetchText.get().mismatchedTxnNamespace,
-						txnNamespace, treeDb.getTxnNamespace()));
-			}
-			refdb = treeDb.getBootstrap();
-		}
-		initialize(refdb.exactRef(
-				getSystem().getTxnAccepted(),
-				getSystem().getTxnCommitted()));
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected void startPush(ReplicaPushRequest req) {
-		getSystem().getExecutor().execute(() -> {
-			MonotonicClock clk = getSystem().getClock();
-			try (Repository git = getLeader().openRepository();
-					ProposedTimestamp ts = clk.propose()) {
-				try {
-					update(git, req, ts);
-					req.done(git);
-				} catch (Throwable err) {
-					req.setException(git, err);
-				}
-			} catch (IOException err) {
-				req.setException(null, err);
-			}
-		});
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected void blockingFetch(Repository repo, ReplicaFetchRequest req)
-			throws IOException {
-		throw new IOException(KetchText.get().cannotFetchFromLocalReplica);
-	}
-
-	private void update(Repository git, ReplicaPushRequest req,
-			ProposedTimestamp ts) throws IOException {
-		RefDatabase refdb = git.getRefDatabase();
-		CommitMethod method = getCommitMethod();
-
-		// Local replica probably uses RefTreeDatabase, the request should
-		// be only for the txnNamespace, so drop to the bootstrap layer.
-		if (refdb instanceof RefTreeDatabase) {
-			if (!isOnlyTxnNamespace(req.getCommands())) {
-				return;
-			}
-
-			refdb = ((RefTreeDatabase) refdb).getBootstrap();
-			method = TXN_COMMITTED;
-		}
-
-		BatchRefUpdate batch = refdb.newBatchUpdate();
-		batch.addProposedTimestamp(ts);
-		batch.setRefLogIdent(getSystem().newCommitter(ts));
-		batch.setRefLogMessage("ketch", false); //$NON-NLS-1$
-		batch.setAllowNonFastForwards(true);
-
-		// RefDirectory updates multiple references sequentially.
-		// Run everything else first, then accepted (if present),
-		// then committed (if present). This ensures an earlier
-		// failure will not update these critical references.
-		ReceiveCommand accepted = null;
-		ReceiveCommand committed = null;
-		for (ReceiveCommand cmd : req.getCommands()) {
-			String name = cmd.getRefName();
-			if (name.equals(getSystem().getTxnAccepted())) {
-				accepted = cmd;
-			} else if (name.equals(getSystem().getTxnCommitted())) {
-				committed = cmd;
-			} else {
-				batch.addCommand(cmd);
-			}
-		}
-		if (committed != null && method == ALL_REFS) {
-			Map<String, Ref> refs = refdb.getRefs(ALL);
-			batch.addCommand(prepareCommit(git, refs, committed.getNewId()));
-		}
-		if (accepted != null) {
-			batch.addCommand(accepted);
-		}
-		if (committed != null) {
-			batch.addCommand(committed);
-		}
-
-		try (RevWalk rw = new RevWalk(git)) {
-			batch.execute(rw, NullProgressMonitor.INSTANCE);
-		}
-
-		// KetchReplica only cares about accepted and committed in
-		// advertisement. If they failed, store the current values
-		// back in the ReplicaPushRequest.
-		List<String> failed = new ArrayList<>(2);
-		checkFailed(failed, accepted);
-		checkFailed(failed, committed);
-		if (!failed.isEmpty()) {
-			String[] arr = failed.toArray(new String[0]);
-			req.setRefs(refdb.exactRef(arr));
-		}
-	}
-
-	private static void checkFailed(List<String> failed, ReceiveCommand cmd) {
-		if (cmd != null && cmd.getResult() != OK) {
-			failed.add(cmd.getRefName());
-		}
-	}
-
-	private boolean isOnlyTxnNamespace(Collection<ReceiveCommand> cmdList) {
-		// Be paranoid and reject non txnNamespace names, this
-		// is a programming error in Ketch that should not occur.
-
-		String txnNamespace = getSystem().getTxnNamespace();
-		for (ReceiveCommand cmd : cmdList) {
-			if (!cmd.getRefName().startsWith(txnNamespace)) {
-				cmd.setResult(REJECTED_OTHER_REASON,
-						MessageFormat.format(
-								KetchText.get().outsideTxnNamespace,
-								cmd.getRefName(), txnNamespace));
-				ReceiveCommand.abort(cmdList);
-				return false;
-			}
-		}
-		return true;
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java
deleted file mode 100644
index ed65c06..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
-
-/**
- * An ObjectId for a commit extended with incrementing log index.
- * <p>
- * For any two LogIndex instances, {@code A} is an ancestor of {@code C}
- * reachable through parent edges in the graph if {@code A.index < C.index}.
- * LogIndex provides a performance optimization for Ketch, the same information
- * can be obtained from {@link org.eclipse.jgit.revwalk.RevWalk}.
- * <p>
- * Index values are only valid within a single
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader} instance after it has won
- * an election. By restricting scope to a single leader new leaders do not need
- * to traverse the entire history to determine the next {@code index} for new
- * proposals. This differs from Raft, where leader election uses the log index
- * and the term number to determine which replica holds a sufficiently
- * up-to-date log. Since Ketch uses Git objects for storage of its replicated
- * log, it keeps the term number as Raft does but uses standard Git operations
- * to imply the log index.
- * <p>
- * {@link org.eclipse.jgit.internal.ketch.Round#runAsync(AnyObjectId)} bumps the
- * index as each new round is constructed.
- */
-public class LogIndex extends ObjectId {
-	static LogIndex unknown(AnyObjectId id) {
-		return new LogIndex(id, 0);
-	}
-
-	private final long index;
-
-	private LogIndex(AnyObjectId id, long index) {
-		super(id);
-		this.index = index;
-	}
-
-	LogIndex nextIndex(AnyObjectId id) {
-		return new LogIndex(id, index + 1);
-	}
-
-	/**
-	 * Get index provided by the current leader instance.
-	 *
-	 * @return index provided by the current leader instance.
-	 */
-	public long getIndex() {
-		return index;
-	}
-
-	/**
-	 * Check if this log position committed before another log position.
-	 * <p>
-	 * Only valid for log positions in memory for the current leader.
-	 *
-	 * @param c
-	 *            other (more recent) log position.
-	 * @return true if this log position was before {@code c} or equal to c and
-	 *         therefore any agreement of {@code c} implies agreement on this
-	 *         log position.
-	 */
-	boolean isBefore(LogIndex c) {
-		return index <= c.index;
-	}
-
-	/**
-	 * Create string suitable for debug logging containing the log index and
-	 * abbreviated ObjectId.
-	 *
-	 * @return string suitable for debug logging containing the log index and
-	 *         abbreviated ObjectId.
-	 */
-	@SuppressWarnings("boxing")
-	public String describeForLog() {
-		return String.format("%5d/%s", index, abbreviate(6).name()); //$NON-NLS-1$
-	}
-
-	/** {@inheritDoc} */
-	@SuppressWarnings("boxing")
-	@Override
-	public String toString() {
-		return String.format("LogId[%5d/%s]", index, name()); //$NON-NLS-1$
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java
deleted file mode 100644
index ca27281..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java
+++ /dev/null
@@ -1,415 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static org.eclipse.jgit.internal.ketch.Proposal.State.ABORTED;
-import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED;
-import static org.eclipse.jgit.internal.ketch.Proposal.State.NEW;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.internal.storage.reftree.Command;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.PushCertificate;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.util.time.ProposedTimestamp;
-
-/**
- * A proposal to be applied in a Ketch system.
- * <p>
- * Pushing to a Ketch leader results in the leader making a proposal. The
- * proposal includes the list of reference updates. The leader attempts to send
- * the proposal to a quorum of replicas by pushing the proposal to a "staging"
- * area under the {@code refs/txn/stage/} namespace. If the proposal succeeds
- * then the changes are durable and the leader can commit the proposal.
- * <p>
- * Proposals are executed by
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader#queueProposal(Proposal)},
- * which runs them asynchronously in the background. Proposals are thread-safe
- * futures allowing callers to {@link #await()} for results or be notified by
- * callback using {@link #addListener(Runnable)}.
- */
-public class Proposal {
-	/** Current state of the proposal. */
-	public enum State {
-		/** Proposal has not yet been given to a {@link KetchLeader}. */
-		NEW(false),
-
-		/**
-		 * Proposal was validated and has entered the queue, but a round
-		 * containing this proposal has not started yet.
-		 */
-		QUEUED(false),
-
-		/** Round containing the proposal has begun and is in progress. */
-		RUNNING(false),
-
-		/**
-		 * Proposal was executed through a round. Individual results from
-		 * {@link Proposal#getCommands()}, {@link Command#getResult()} explain
-		 * the success or failure outcome.
-		 */
-		EXECUTED(true),
-
-		/** Proposal was aborted and did not reach consensus. */
-		ABORTED(true);
-
-		private final boolean done;
-
-		private State(boolean done) {
-			this.done = done;
-		}
-
-		/** @return true if this is a terminal state. */
-		public boolean isDone() {
-			return done;
-		}
-	}
-
-	private final List<Command> commands;
-	private PersonIdent author;
-	private String message;
-	private PushCertificate pushCert;
-
-	private List<ProposedTimestamp> timestamps;
-	private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
-	private final AtomicReference<State> state = new AtomicReference<>(NEW);
-
-	/**
-	 * Create a proposal from a list of Ketch commands.
-	 *
-	 * @param cmds
-	 *            prepared list of commands.
-	 */
-	public Proposal(List<Command> cmds) {
-		commands = Collections.unmodifiableList(new ArrayList<>(cmds));
-	}
-
-	/**
-	 * Create a proposal from a collection of received commands.
-	 *
-	 * @param rw
-	 *            walker to assist in preparing commands.
-	 * @param cmds
-	 *            list of pending commands.
-	 * @throws org.eclipse.jgit.errors.MissingObjectException
-	 *             newId of a command is not found locally.
-	 * @throws java.io.IOException
-	 *             local objects cannot be accessed.
-	 */
-	public Proposal(RevWalk rw, Collection<ReceiveCommand> cmds)
-			throws MissingObjectException, IOException {
-		commands = asCommandList(rw, cmds);
-	}
-
-	private static List<Command> asCommandList(RevWalk rw,
-			Collection<ReceiveCommand> cmds)
-					throws MissingObjectException, IOException {
-		List<Command> commands = new ArrayList<>(cmds.size());
-		for (ReceiveCommand cmd : cmds) {
-			commands.add(new Command(rw, cmd));
-		}
-		return Collections.unmodifiableList(commands);
-	}
-
-	/**
-	 * Get commands from this proposal.
-	 *
-	 * @return commands from this proposal.
-	 */
-	public Collection<Command> getCommands() {
-		return commands;
-	}
-
-	/**
-	 * Get optional author of the proposal.
-	 *
-	 * @return optional author of the proposal.
-	 */
-	@Nullable
-	public PersonIdent getAuthor() {
-		return author;
-	}
-
-	/**
-	 * Set the author for the proposal.
-	 *
-	 * @param who
-	 *            optional identity of the author of the proposal.
-	 * @return {@code this}
-	 */
-	public Proposal setAuthor(@Nullable PersonIdent who) {
-		author = who;
-		return this;
-	}
-
-	/**
-	 * Get optional message for the commit log of the RefTree.
-	 *
-	 * @return optional message for the commit log of the RefTree.
-	 */
-	@Nullable
-	public String getMessage() {
-		return message;
-	}
-
-	/**
-	 * Set the message to appear in the commit log of the RefTree.
-	 *
-	 * @param msg
-	 *            message text for the commit.
-	 * @return {@code this}
-	 */
-	public Proposal setMessage(@Nullable String msg) {
-		message = msg != null && !msg.isEmpty() ? msg : null;
-		return this;
-	}
-
-	/**
-	 * Get optional certificate signing the references.
-	 *
-	 * @return optional certificate signing the references.
-	 */
-	@Nullable
-	public PushCertificate getPushCertificate() {
-		return pushCert;
-	}
-
-	/**
-	 * Set the push certificate signing the references.
-	 *
-	 * @param cert
-	 *            certificate, may be null.
-	 * @return {@code this}
-	 */
-	public Proposal setPushCertificate(@Nullable PushCertificate cert) {
-		pushCert = cert;
-		return this;
-	}
-
-	/**
-	 * Get timestamps that Ketch must block for.
-	 *
-	 * @return timestamps that Ketch must block for. These may have been used as
-	 *         commit times inside the objects involved in the proposal.
-	 */
-	public List<ProposedTimestamp> getProposedTimestamps() {
-		if (timestamps != null) {
-			return timestamps;
-		}
-		return Collections.emptyList();
-	}
-
-	/**
-	 * Request the proposal to wait for the affected timestamps to resolve.
-	 *
-	 * @param ts
-	 *            a {@link org.eclipse.jgit.util.time.ProposedTimestamp} object.
-	 * @return {@code this}.
-	 */
-	public Proposal addProposedTimestamp(ProposedTimestamp ts) {
-		if (timestamps == null) {
-			timestamps = new ArrayList<>(4);
-		}
-		timestamps.add(ts);
-		return this;
-	}
-
-	/**
-	 * Add a callback to be invoked when the proposal is done.
-	 * <p>
-	 * A proposal is done when it has entered either
-	 * {@link org.eclipse.jgit.internal.ketch.Proposal.State#EXECUTED} or
-	 * {@link org.eclipse.jgit.internal.ketch.Proposal.State#ABORTED} state. If
-	 * the proposal is already done {@code callback.run()} is immediately
-	 * invoked on the caller's thread.
-	 *
-	 * @param callback
-	 *            method to run after the proposal is done. The callback may be
-	 *            run on a Ketch system thread and should be completed quickly.
-	 */
-	public void addListener(Runnable callback) {
-		boolean runNow = false;
-		synchronized (state) {
-			if (state.get().isDone()) {
-				runNow = true;
-			} else {
-				listeners.add(callback);
-			}
-		}
-		if (runNow) {
-			callback.run();
-		}
-	}
-
-	/** Set command result as OK. */
-	void success() {
-		for (Command c : commands) {
-			if (c.getResult() == NOT_ATTEMPTED) {
-				c.setResult(OK);
-			}
-		}
-		notifyState(EXECUTED);
-	}
-
-	/** Mark commands as "transaction aborted". */
-	void abort() {
-		Command.abort(commands, null);
-		notifyState(ABORTED);
-	}
-
-	/**
-	 * Read the current state of the proposal.
-	 *
-	 * @return read the current state of the proposal.
-	 */
-	public State getState() {
-		return state.get();
-	}
-
-	/**
-	 * Whether the proposal was attempted
-	 *
-	 * @return {@code true} if the proposal was attempted. A true value does not
-	 *         mean consensus was reached, only that the proposal was considered
-	 *         and will not be making any more progress beyond its current
-	 *         state.
-	 */
-	public boolean isDone() {
-		return state.get().isDone();
-	}
-
-	/**
-	 * Wait for the proposal to be attempted and {@link #isDone()} to be true.
-	 *
-	 * @throws java.lang.InterruptedException
-	 *             caller was interrupted before proposal executed.
-	 */
-	public void await() throws InterruptedException {
-		synchronized (state) {
-			while (!state.get().isDone()) {
-				state.wait();
-			}
-		}
-	}
-
-	/**
-	 * Wait for the proposal to be attempted and {@link #isDone()} to be true.
-	 *
-	 * @param wait
-	 *            how long to wait.
-	 * @param unit
-	 *            unit describing the wait time.
-	 * @return true if the proposal is done; false if the method timed out.
-	 * @throws java.lang.InterruptedException
-	 *             caller was interrupted before proposal executed.
-	 */
-	public boolean await(long wait, TimeUnit unit) throws InterruptedException {
-		synchronized (state) {
-			if (state.get().isDone()) {
-				return true;
-			}
-			state.wait(unit.toMillis(wait));
-			return state.get().isDone();
-		}
-	}
-
-	/**
-	 * Wait for the proposal to exit a state.
-	 *
-	 * @param notIn
-	 *            state the proposal should not be in to return.
-	 * @param wait
-	 *            how long to wait.
-	 * @param unit
-	 *            unit describing the wait time.
-	 * @return true if the proposal exited the state; false on time out.
-	 * @throws java.lang.InterruptedException
-	 *             caller was interrupted before proposal executed.
-	 */
-	public boolean awaitStateChange(State notIn, long wait, TimeUnit unit)
-			throws InterruptedException {
-		synchronized (state) {
-			if (state.get() != notIn) {
-				return true;
-			}
-			state.wait(unit.toMillis(wait));
-			return state.get() != notIn;
-		}
-	}
-
-	void notifyState(State s) {
-		synchronized (state) {
-			state.set(s);
-			state.notifyAll();
-		}
-		if (s.isDone()) {
-			for (Runnable callback : listeners) {
-				callback.run();
-			}
-			listeners.clear();
-		}
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public String toString() {
-		StringBuilder s = new StringBuilder();
-		s.append("Ketch Proposal {\n"); //$NON-NLS-1$
-		s.append("  ").append(state.get()).append('\n'); //$NON-NLS-1$
-		if (author != null) {
-			s.append("  author ").append(author).append('\n'); //$NON-NLS-1$
-		}
-		if (message != null) {
-			s.append("  message ").append(message).append('\n'); //$NON-NLS-1$
-		}
-		for (Command c : commands) {
-			s.append("  "); //$NON-NLS-1$
-			format(s, c.getOldRef(), "CREATE"); //$NON-NLS-1$
-			s.append(' ');
-			format(s, c.getNewRef(), "DELETE"); //$NON-NLS-1$
-			s.append(' ').append(c.getRefName());
-			if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
-				s.append(' ').append(c.getResult()); // $NON-NLS-1$
-			}
-			s.append('\n');
-		}
-		s.append('}');
-		return s.toString();
-	}
-
-	private static void format(StringBuilder s, @Nullable Ref r, String n) {
-		if (r == null) {
-			s.append(n);
-		} else if (r.isSymbolic()) {
-			s.append(r.getTarget().getName());
-		} else {
-			ObjectId id = r.getObjectId();
-			if (id != null) {
-				s.append(id.abbreviate(8).name());
-			}
-		}
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java
deleted file mode 100644
index b73183a..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static org.eclipse.jgit.internal.ketch.Proposal.State.RUNNING;
-
-import java.io.IOException;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeoutException;
-import java.util.stream.Collectors;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.internal.storage.reftree.Command;
-import org.eclipse.jgit.internal.storage.reftree.RefTree;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.util.time.ProposedTimestamp;
-
-/** A {@link Round} that aggregates and sends user {@link Proposal}s. */
-class ProposalRound extends Round {
-	private final List<Proposal> todo;
-	private RefTree queuedTree;
-
-	ProposalRound(KetchLeader leader, LogIndex head, List<Proposal> todo,
-			@Nullable RefTree tree) {
-		super(leader, head);
-		this.todo = todo;
-
-		if (tree != null && canCombine(todo)) {
-			this.queuedTree = tree;
-		} else {
-			leader.roundHoldsReferenceToRefTree = false;
-		}
-	}
-
-	private static boolean canCombine(List<Proposal> todo) {
-		Proposal first = todo.get(0);
-		for (int i = 1; i < todo.size(); i++) {
-			if (!canCombine(first, todo.get(i))) {
-				return false;
-			}
-		}
-		return true;
-	}
-
-	private static boolean canCombine(Proposal a, Proposal b) {
-		String aMsg = nullToEmpty(a.getMessage());
-		String bMsg = nullToEmpty(b.getMessage());
-		return aMsg.equals(bMsg) && canCombine(a.getAuthor(), b.getAuthor());
-	}
-
-	private static String nullToEmpty(@Nullable String str) {
-		return str != null ? str : ""; //$NON-NLS-1$
-	}
-
-	private static boolean canCombine(@Nullable PersonIdent a,
-			@Nullable PersonIdent b) {
-		if (a != null && b != null) {
-			// Same name and email address. Combine timestamp as the two
-			// proposals are running concurrently and appear together or
-			// not at all from the point of view of an outside reader.
-			return a.getName().equals(b.getName())
-					&& a.getEmailAddress().equals(b.getEmailAddress());
-		}
-
-		// If a and b are null, both will be the system identity.
-		return a == null && b == null;
-	}
-
-	@Override
-	void start() throws IOException {
-		for (Proposal p : todo) {
-			p.notifyState(RUNNING);
-		}
-		try {
-			ObjectId id;
-			try (Repository git = leader.openRepository();
-					ProposedTimestamp ts = getSystem().getClock().propose()) {
-				id = insertProposals(git, ts);
-				blockUntil(ts);
-			}
-			runAsync(id);
-		} catch (NoOp e) {
-			for (Proposal p : todo) {
-				p.success();
-			}
-			leader.lock.lock();
-			try {
-				leader.nextRound();
-			} finally {
-				leader.lock.unlock();
-			}
-		} catch (IOException e) {
-			abort();
-			throw e;
-		}
-	}
-
-	private ObjectId insertProposals(Repository git, ProposedTimestamp ts)
-			throws IOException, NoOp {
-		ObjectId id;
-		try (ObjectInserter inserter = git.newObjectInserter()) {
-			// TODO(sop) Process signed push certificates.
-
-			if (queuedTree != null) {
-				id = insertSingleProposal(git, ts, inserter);
-			} else {
-				id = insertMultiProposal(git, ts, inserter);
-			}
-
-			stageCommands = makeStageList(git, inserter);
-			inserter.flush();
-		}
-		return id;
-	}
-
-	private ObjectId insertSingleProposal(Repository git, ProposedTimestamp ts,
-			ObjectInserter inserter) throws IOException, NoOp {
-		// Fast path: tree is passed in with all proposals applied.
-		ObjectId treeId = queuedTree.writeTree(inserter);
-		queuedTree = null;
-		leader.roundHoldsReferenceToRefTree = false;
-
-		if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
-			try (RevWalk rw = new RevWalk(git)) {
-				RevCommit c = rw.parseCommit(acceptedOldIndex);
-				if (treeId.equals(c.getTree())) {
-					throw new NoOp();
-				}
-			}
-		}
-
-		Proposal p = todo.get(0);
-		CommitBuilder b = new CommitBuilder();
-		b.setTreeId(treeId);
-		if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
-			b.setParentId(acceptedOldIndex);
-		}
-		b.setCommitter(leader.getSystem().newCommitter(ts));
-		b.setAuthor(p.getAuthor() != null ? p.getAuthor() : b.getCommitter());
-		b.setMessage(message(p));
-		return inserter.insert(b);
-	}
-
-	private ObjectId insertMultiProposal(Repository git, ProposedTimestamp ts,
-			ObjectInserter inserter) throws IOException, NoOp {
-		// The tree was not passed in, or there are multiple proposals
-		// each needing their own commit. Reset the tree and replay each
-		// proposal in order as individual commits.
-		ObjectId lastIndex = acceptedOldIndex;
-		ObjectId oldTreeId;
-		RefTree tree;
-		if (ObjectId.zeroId().equals(lastIndex)) {
-			oldTreeId = ObjectId.zeroId();
-			tree = RefTree.newEmptyTree();
-		} else {
-			try (RevWalk rw = new RevWalk(git)) {
-				RevCommit c = rw.parseCommit(lastIndex);
-				oldTreeId = c.getTree();
-				tree = RefTree.read(rw.getObjectReader(), c.getTree());
-			}
-		}
-
-		PersonIdent committer = leader.getSystem().newCommitter(ts);
-		for (Proposal p : todo) {
-			if (!tree.apply(p.getCommands())) {
-				// This should not occur, previously during queuing the
-				// commands were successfully applied to the pending tree.
-				// Abort the entire round.
-				throw new IOException(
-						KetchText.get().queuedProposalFailedToApply);
-			}
-
-			ObjectId treeId = tree.writeTree(inserter);
-			if (treeId.equals(oldTreeId)) {
-				continue;
-			}
-
-			CommitBuilder b = new CommitBuilder();
-			b.setTreeId(treeId);
-			if (!ObjectId.zeroId().equals(lastIndex)) {
-				b.setParentId(lastIndex);
-			}
-			b.setAuthor(p.getAuthor() != null ? p.getAuthor() : committer);
-			b.setCommitter(committer);
-			b.setMessage(message(p));
-			lastIndex = inserter.insert(b);
-		}
-		if (lastIndex.equals(acceptedOldIndex)) {
-			throw new NoOp();
-		}
-		return lastIndex;
-	}
-
-	private String message(Proposal p) {
-		StringBuilder m = new StringBuilder();
-		String msg = p.getMessage();
-		if (msg != null && !msg.isEmpty()) {
-			m.append(msg);
-			while (m.length() < 2 || m.charAt(m.length() - 2) != '\n'
-					|| m.charAt(m.length() - 1) != '\n') {
-				m.append('\n');
-			}
-		}
-		m.append(KetchConstants.TERM.getName())
-				.append(": ") //$NON-NLS-1$
-				.append(leader.getTerm());
-		return m.toString();
-	}
-
-	void abort() {
-		for (Proposal p : todo) {
-			p.abort();
-		}
-	}
-
-	@Override
-	void success() {
-		for (Proposal p : todo) {
-			p.success();
-		}
-	}
-
-	private List<ReceiveCommand> makeStageList(Repository git,
-			ObjectInserter inserter) throws IOException {
-		// For each branch, collapse consecutive updates to only most recent,
-		// avoiding sending multiple objects in a rapid fast-forward chain, or
-		// rewritten content.
-		Map<String, ObjectId> byRef = new HashMap<>();
-		for (Proposal p : todo) {
-			for (Command c : p.getCommands()) {
-				Ref n = c.getNewRef();
-				if (n != null && !n.isSymbolic()) {
-					byRef.put(n.getName(), n.getObjectId());
-				}
-			}
-		}
-		if (byRef.isEmpty()) {
-			return Collections.emptyList();
-		}
-
-		Set<ObjectId> newObjs = new HashSet<>(byRef.values());
-		StageBuilder b = new StageBuilder(
-				leader.getSystem().getTxnStage(),
-				acceptedNewIndex);
-		return b.makeStageList(newObjs, git, inserter);
-	}
-
-	private void blockUntil(ProposedTimestamp ts)
-			throws TimeIsUncertainException {
-		List<ProposedTimestamp> times = todo.stream()
-				.flatMap(p -> p.getProposedTimestamps().stream())
-				.collect(Collectors.toCollection(ArrayList::new));
-		times.add(ts);
-
-		try {
-			Duration maxWait = getSystem().getMaxWaitForMonotonicClock();
-			ProposedTimestamp.blockUntil(times, maxWait);
-		} catch (InterruptedException | TimeoutException e) {
-			throw new TimeIsUncertainException(e);
-		}
-	}
-
-	private static class NoOp extends Exception {
-		private static final long serialVersionUID = 1L;
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
deleted file mode 100644
index fac93c8..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS;
-import static org.eclipse.jgit.lib.Ref.Storage.NETWORK;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NODELETE;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.errors.NotSupportedException;
-import org.eclipse.jgit.errors.TransportException;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.FetchConnection;
-import org.eclipse.jgit.transport.PushConnection;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.transport.URIish;
-
-/**
- * Representation of a Git repository on a remote replica system.
- * <p>
- * {@link org.eclipse.jgit.internal.ketch.KetchLeader} will contact the replica
- * using the Git wire protocol.
- * <p>
- * The remote replica may be fully Ketch-aware, or a standard Git server.
- */
-public class RemoteGitReplica extends KetchReplica {
-	private final URIish uri;
-	private final RemoteConfig remoteConfig;
-
-	/**
-	 * Configure a new remote.
-	 *
-	 * @param leader
-	 *            instance this replica follows.
-	 * @param name
-	 *            unique-ish name identifying this remote for debugging.
-	 * @param uri
-	 *            URI to connect to the follower's repository.
-	 * @param cfg
-	 *            how Ketch should treat the remote system.
-	 * @param rc
-	 *            optional remote configuration describing how to contact the
-	 *            peer repository.
-	 */
-	public RemoteGitReplica(KetchLeader leader, String name, URIish uri,
-			ReplicaConfig cfg, @Nullable RemoteConfig rc) {
-		super(leader, name, cfg);
-		this.uri = uri;
-		this.remoteConfig = rc;
-	}
-
-	/**
-	 * Get URI to contact the remote peer repository.
-	 *
-	 * @return URI to contact the remote peer repository.
-	 */
-	public URIish getURI() {
-		return uri;
-	}
-
-	/**
-	 * Get optional configuration describing how to contact the peer.
-	 *
-	 * @return optional configuration describing how to contact the peer.
-	 */
-	@Nullable
-	protected RemoteConfig getRemoteConfig() {
-		return remoteConfig;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected String describeForLog() {
-		return String.format("%s @ %s", getName(), getURI()); //$NON-NLS-1$
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected void startPush(ReplicaPushRequest req) {
-		getSystem().getExecutor().execute(() -> {
-			try (Repository git = getLeader().openRepository()) {
-				try {
-					push(git, req);
-					req.done(git);
-				} catch (Throwable err) {
-					req.setException(git, err);
-				}
-			} catch (IOException err) {
-				req.setException(null, err);
-			}
-		});
-	}
-
-	private void push(Repository repo, ReplicaPushRequest req)
-			throws NotSupportedException, TransportException, IOException {
-		Map<String, Ref> adv;
-		List<RemoteCommand> cmds = asUpdateList(req.getCommands());
-		try (Transport transport = Transport.open(repo, uri)) {
-			RemoteConfig rc = getRemoteConfig();
-			if (rc != null) {
-				transport.applyConfig(rc);
-			}
-			transport.setPushAtomic(true);
-			adv = push(repo, transport, cmds);
-		}
-		for (RemoteCommand c : cmds) {
-			c.copyStatusToResult();
-		}
-		req.setRefs(adv);
-	}
-
-	private Map<String, Ref> push(Repository git, Transport transport,
-			List<RemoteCommand> cmds) throws IOException {
-		Map<String, RemoteRefUpdate> updates = asUpdateMap(cmds);
-		try (PushConnection connection = transport.openPush()) {
-			Map<String, Ref> adv = connection.getRefsMap();
-			RemoteRefUpdate accepted = updates.get(getSystem().getTxnAccepted());
-			if (accepted != null && !isExpectedValue(adv, accepted)) {
-				abort(cmds);
-				return adv;
-			}
-
-			RemoteRefUpdate committed = updates.get(getSystem().getTxnCommitted());
-			if (committed != null && !isExpectedValue(adv, committed)) {
-				abort(cmds);
-				return adv;
-			}
-			if (committed != null && getCommitMethod() == ALL_REFS) {
-				prepareCommit(git, cmds, updates, adv,
-						committed.getNewObjectId());
-			}
-
-			connection.push(NullProgressMonitor.INSTANCE, updates);
-			return adv;
-		}
-	}
-
-	private static boolean isExpectedValue(Map<String, Ref> adv,
-			RemoteRefUpdate u) {
-		Ref r = adv.get(u.getRemoteName());
-		if (!AnyObjectId.isEqual(getId(r), u.getExpectedOldObjectId())) {
-			((RemoteCommand) u).cmd.setResult(LOCK_FAILURE);
-			return false;
-		}
-		return true;
-	}
-
-	private void prepareCommit(Repository git, List<RemoteCommand> cmds,
-			Map<String, RemoteRefUpdate> updates, Map<String, Ref> adv,
-			ObjectId committed) throws IOException {
-		for (ReceiveCommand cmd : prepareCommit(git, adv, committed)) {
-			RemoteCommand c = new RemoteCommand(cmd);
-			cmds.add(c);
-			updates.put(c.getRemoteName(), c);
-		}
-	}
-
-	private static List<RemoteCommand> asUpdateList(
-			Collection<ReceiveCommand> cmds) {
-		try {
-			List<RemoteCommand> toPush = new ArrayList<>(cmds.size());
-			for (ReceiveCommand cmd : cmds) {
-				toPush.add(new RemoteCommand(cmd));
-			}
-			return toPush;
-		} catch (IOException e) {
-			// Cannot occur as no IO was required to build the command.
-			throw new IllegalStateException(e);
-		}
-	}
-
-	private static Map<String, RemoteRefUpdate> asUpdateMap(
-			List<RemoteCommand> cmds) {
-		Map<String, RemoteRefUpdate> m = new LinkedHashMap<>();
-		for (RemoteCommand cmd : cmds) {
-			m.put(cmd.getRemoteName(), cmd);
-		}
-		return m;
-	}
-
-	private static void abort(List<RemoteCommand> cmds) {
-		List<ReceiveCommand> tmp = new ArrayList<>(cmds.size());
-		for (RemoteCommand cmd : cmds) {
-			tmp.add(cmd.cmd);
-		}
-		ReceiveCommand.abort(tmp);
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected void blockingFetch(Repository repo, ReplicaFetchRequest req)
-			throws NotSupportedException, TransportException {
-		try (Transport transport = Transport.open(repo, uri)) {
-			RemoteConfig rc = getRemoteConfig();
-			if (rc != null) {
-				transport.applyConfig(rc);
-			}
-			fetch(transport, req);
-		}
-	}
-
-	private void fetch(Transport transport, ReplicaFetchRequest req)
-			throws NotSupportedException, TransportException {
-		try (FetchConnection conn = transport.openFetch()) {
-			Map<String, Ref> remoteRefs = conn.getRefsMap();
-			req.setRefs(remoteRefs);
-
-			List<Ref> want = new ArrayList<>();
-			for (String name : req.getWantRefs()) {
-				Ref ref = remoteRefs.get(name);
-				if (ref != null && ref.getObjectId() != null) {
-					want.add(ref);
-				}
-			}
-			for (ObjectId id : req.getWantObjects()) {
-				want.add(new ObjectIdRef.Unpeeled(NETWORK, id.name(), id));
-			}
-
-			conn.fetch(NullProgressMonitor.INSTANCE, want,
-					Collections.<ObjectId> emptySet());
-		}
-	}
-
-	static class RemoteCommand extends RemoteRefUpdate {
-		final ReceiveCommand cmd;
-
-		RemoteCommand(ReceiveCommand cmd) throws IOException {
-			super(null, null,
-					cmd.getNewId(), cmd.getRefName(),
-					true /* force update */,
-					null /* no local tracking ref */,
-					cmd.getOldId());
-			this.cmd = cmd;
-		}
-
-		void copyStatusToResult() {
-			if (cmd.getResult() == NOT_ATTEMPTED) {
-				switch (getStatus()) {
-				case OK:
-				case UP_TO_DATE:
-				case NON_EXISTING:
-					cmd.setResult(OK);
-					break;
-
-				case REJECTED_NODELETE:
-					cmd.setResult(REJECTED_NODELETE);
-					break;
-
-				case REJECTED_NONFASTFORWARD:
-					cmd.setResult(REJECTED_NONFASTFORWARD);
-					break;
-
-				case REJECTED_OTHER_REASON:
-					cmd.setResult(REJECTED_OTHER_REASON, getMessage());
-					break;
-
-				default:
-					cmd.setResult(REJECTED_OTHER_REASON, getStatus().name());
-					break;
-				}
-			}
-		}
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java
deleted file mode 100644
index 1d323b8..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static java.util.concurrent.TimeUnit.DAYS;
-import static java.util.concurrent.TimeUnit.HOURS;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_COMMIT;
-import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_SPEED;
-import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE;
-import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod;
-import org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed;
-import org.eclipse.jgit.internal.ketch.KetchReplica.Participation;
-import org.eclipse.jgit.lib.Config;
-
-/**
- * Configures a {@link org.eclipse.jgit.internal.ketch.KetchReplica}.
- */
-public class ReplicaConfig {
-	/**
-	 * Read a configuration from a config block.
-	 *
-	 * @param cfg
-	 *            configuration to read.
-	 * @param name
-	 *            of the replica being configured.
-	 * @return replica configuration for {@code name}.
-	 */
-	public static ReplicaConfig newFromConfig(Config cfg, String name) {
-		return new ReplicaConfig().fromConfig(cfg, name);
-	}
-
-	private Participation participation = Participation.FULL;
-	private CommitMethod commitMethod = CommitMethod.ALL_REFS;
-	private CommitSpeed commitSpeed = CommitSpeed.BATCHED;
-	private long minRetry = SECONDS.toMillis(5);
-	private long maxRetry = MINUTES.toMillis(1);
-
-	/**
-	 * Get participation of the replica in the system.
-	 *
-	 * @return participation of the replica in the system.
-	 */
-	public Participation getParticipation() {
-		return participation;
-	}
-
-	/**
-	 * Get how Ketch should apply committed changes.
-	 *
-	 * @return how Ketch should apply committed changes.
-	 */
-	public CommitMethod getCommitMethod() {
-		return commitMethod;
-	}
-
-	/**
-	 * Get how quickly should Ketch commit.
-	 *
-	 * @return how quickly should Ketch commit.
-	 */
-	public CommitSpeed getCommitSpeed() {
-		return commitSpeed;
-	}
-
-	/**
-	 * Returns the minimum wait delay before retrying a failure.
-	 *
-	 * @param unit
-	 *            to get retry delay in.
-	 * @return minimum delay before retrying a failure.
-	 */
-	public long getMinRetry(TimeUnit unit) {
-		return unit.convert(minRetry, MILLISECONDS);
-	}
-
-	/**
-	 * Returns the maximum wait delay before retrying a failure.
-	 *
-	 * @param unit
-	 *            to get retry delay in.
-	 * @return maximum delay before retrying a failure.
-	 */
-	public long getMaxRetry(TimeUnit unit) {
-		return unit.convert(maxRetry, MILLISECONDS);
-	}
-
-	/**
-	 * Update the configuration from a config block.
-	 *
-	 * @param cfg
-	 *            configuration to read.
-	 * @param name
-	 *            of the replica being configured.
-	 * @return {@code this}
-	 */
-	public ReplicaConfig fromConfig(Config cfg, String name) {
-		participation = cfg.getEnum(
-				CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE,
-				participation);
-		commitMethod = cfg.getEnum(
-				CONFIG_KEY_REMOTE, name, CONFIG_KEY_COMMIT,
-				commitMethod);
-		commitSpeed = cfg.getEnum(
-				CONFIG_KEY_REMOTE, name, CONFIG_KEY_SPEED,
-				commitSpeed);
-		minRetry = getMillis(cfg, name, "ketch-minRetry", minRetry); //$NON-NLS-1$
-		maxRetry = getMillis(cfg, name, "ketch-maxRetry", maxRetry); //$NON-NLS-1$
-		return this;
-	}
-
-	private static long getMillis(Config cfg, String name, String key,
-			long defaultValue) {
-		String valStr = cfg.getString(CONFIG_KEY_REMOTE, name, key);
-		if (valStr == null) {
-			return defaultValue;
-		}
-
-		valStr = valStr.trim();
-		if (valStr.isEmpty()) {
-			return defaultValue;
-		}
-
-		Matcher m = UnitMap.PATTERN.matcher(valStr);
-		if (!m.matches()) {
-			return defaultValue;
-		}
-
-		String digits = m.group(1);
-		String unitName = m.group(2).trim();
-		TimeUnit unit = UnitMap.UNITS.get(unitName);
-		if (unit == null) {
-			return defaultValue;
-		}
-
-		try {
-			if (digits.indexOf('.') == -1) {
-				return unit.toMillis(Long.parseLong(digits));
-			}
-
-			double val = Double.parseDouble(digits);
-			return (long) (val * unit.toMillis(1));
-		} catch (NumberFormatException nfe) {
-			return defaultValue;
-		}
-	}
-
-	static class UnitMap {
-		static final Pattern PATTERN = Pattern
-				.compile("^([1-9][0-9]*(?:\\.[0-9]*)?)\\s*(.*)$"); //$NON-NLS-1$
-
-		static final Map<String, TimeUnit> UNITS;
-
-		static {
-			Map<String, TimeUnit> m = new HashMap<>();
-			TimeUnit u = MILLISECONDS;
-			m.put("", u); //$NON-NLS-1$
-			m.put("ms", u); //$NON-NLS-1$
-			m.put("millis", u); //$NON-NLS-1$
-			m.put("millisecond", u); //$NON-NLS-1$
-			m.put("milliseconds", u); //$NON-NLS-1$
-
-			u = SECONDS;
-			m.put("s", u); //$NON-NLS-1$
-			m.put("sec", u); //$NON-NLS-1$
-			m.put("secs", u); //$NON-NLS-1$
-			m.put("second", u); //$NON-NLS-1$
-			m.put("seconds", u); //$NON-NLS-1$
-
-			u = MINUTES;
-			m.put("m", u); //$NON-NLS-1$
-			m.put("min", u); //$NON-NLS-1$
-			m.put("mins", u); //$NON-NLS-1$
-			m.put("minute", u); //$NON-NLS-1$
-			m.put("minutes", u); //$NON-NLS-1$
-
-			u = HOURS;
-			m.put("h", u); //$NON-NLS-1$
-			m.put("hr", u); //$NON-NLS-1$
-			m.put("hrs", u); //$NON-NLS-1$
-			m.put("hour", u); //$NON-NLS-1$
-			m.put("hours", u); //$NON-NLS-1$
-
-			u = DAYS;
-			m.put("d", u); //$NON-NLS-1$
-			m.put("day", u); //$NON-NLS-1$
-			m.put("days", u); //$NON-NLS-1$
-
-			UNITS = Collections.unmodifiableMap(m);
-		}
-
-		private UnitMap() {
-		}
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java
deleted file mode 100644
index f50ad62..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import java.util.Map;
-import java.util.Set;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-
-/**
- * A fetch request to obtain objects from a replica, and its result.
- */
-public class ReplicaFetchRequest {
-	private final Set<String> wantRefs;
-	private final Set<ObjectId> wantObjects;
-	private Map<String, Ref> refs;
-
-	/**
-	 * Construct a new fetch request for a replica.
-	 *
-	 * @param wantRefs
-	 *            named references to be fetched.
-	 * @param wantObjects
-	 *            specific objects to be fetched.
-	 */
-	public ReplicaFetchRequest(Set<String> wantRefs,
-			Set<ObjectId> wantObjects) {
-		this.wantRefs = wantRefs;
-		this.wantObjects = wantObjects;
-	}
-
-	/**
-	 * Get references to be fetched.
-	 *
-	 * @return references to be fetched.
-	 */
-	public Set<String> getWantRefs() {
-		return wantRefs;
-	}
-
-	/**
-	 * Get objects to be fetched.
-	 *
-	 * @return objects to be fetched.
-	 */
-	public Set<ObjectId> getWantObjects() {
-		return wantObjects;
-	}
-
-	/**
-	 * Get remote references, usually from the advertisement.
-	 *
-	 * @return remote references, usually from the advertisement.
-	 */
-	@Nullable
-	public Map<String, Ref> getRefs() {
-		return refs;
-	}
-
-	/**
-	 * Set references observed from the replica.
-	 *
-	 * @param refs
-	 *            references observed from the replica.
-	 */
-	public void setRefs(Map<String, Ref> refs) {
-		this.refs = refs;
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java
deleted file mode 100644
index 273760b..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import java.util.Collection;
-import java.util.Map;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-/**
- * A push request sending objects to a replica, and its result.
- * <p>
- * Implementors of {@link org.eclipse.jgit.internal.ketch.KetchReplica} must
- * populate the command result fields, {@link #setRefs(Map)}, and call one of
- * {@link #setException(Repository, Throwable)} or {@link #done(Repository)} to
- * finish processing.
- */
-public class ReplicaPushRequest {
-	private final KetchReplica replica;
-	private final Collection<ReceiveCommand> commands;
-	private Map<String, Ref> refs;
-	private Throwable exception;
-	private boolean notified;
-
-	/**
-	 * Construct a new push request for a replica.
-	 *
-	 * @param replica
-	 *            the replica being pushed to.
-	 * @param commands
-	 *            commands to be executed.
-	 */
-	public ReplicaPushRequest(KetchReplica replica,
-			Collection<ReceiveCommand> commands) {
-		this.replica = replica;
-		this.commands = commands;
-	}
-
-	/**
-	 * Get commands to be executed, and their results.
-	 *
-	 * @return commands to be executed, and their results.
-	 */
-	public Collection<ReceiveCommand> getCommands() {
-		return commands;
-	}
-
-	/**
-	 * Get remote references, usually from the advertisement.
-	 *
-	 * @return remote references, usually from the advertisement.
-	 */
-	@Nullable
-	public Map<String, Ref> getRefs() {
-		return refs;
-	}
-
-	/**
-	 * Set references observed from the replica.
-	 *
-	 * @param refs
-	 *            references observed from the replica.
-	 */
-	public void setRefs(Map<String, Ref> refs) {
-		this.refs = refs;
-	}
-
-	/**
-	 * Get exception thrown, if any.
-	 *
-	 * @return exception thrown, if any.
-	 */
-	@Nullable
-	public Throwable getException() {
-		return exception;
-	}
-
-	/**
-	 * Mark the request as crashing with a communication error.
-	 * <p>
-	 * This method may take significant time acquiring the leader lock and
-	 * updating the Ketch state machine with the failure.
-	 *
-	 * @param repo
-	 *            local repository reference used by the push attempt.
-	 * @param err
-	 *            exception thrown during communication.
-	 */
-	public void setException(@Nullable Repository repo, Throwable err) {
-		if (KetchReplica.log.isErrorEnabled()) {
-			KetchReplica.log.error(describe("failed"), err); //$NON-NLS-1$
-		}
-		if (!notified) {
-			notified = true;
-			exception = err;
-			replica.afterPush(repo, this);
-		}
-	}
-
-	/**
-	 * Mark the request as completed without exception.
-	 * <p>
-	 * This method may take significant time acquiring the leader lock and
-	 * updating the Ketch state machine with results from this replica.
-	 *
-	 * @param repo
-	 *            local repository reference used by the push attempt.
-	 */
-	public void done(Repository repo) {
-		if (KetchReplica.log.isDebugEnabled()) {
-			KetchReplica.log.debug(describe("completed")); //$NON-NLS-1$
-		}
-		if (!notified) {
-			notified = true;
-			replica.afterPush(repo, this);
-		}
-	}
-
-	private String describe(String heading) {
-		StringBuilder b = new StringBuilder();
-		b.append("push to "); //$NON-NLS-1$
-		b.append(replica.describeForLog());
-		b.append(' ').append(heading).append(":\n"); //$NON-NLS-1$
-		for (ReceiveCommand cmd : commands) {
-			b.append(String.format(
-					"  %-12s %-12s %s %s", //$NON-NLS-1$
-					LeaderSnapshot.str(cmd.getOldId()),
-					LeaderSnapshot.str(cmd.getNewId()),
-					cmd.getRefName(),
-					cmd.getResult()));
-			if (cmd.getMessage() != null) {
-				b.append(' ').append(cmd.getMessage());
-			}
-			b.append('\n');
-		}
-		return b.toString();
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java
deleted file mode 100644
index 05e4ed6..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import java.util.Date;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.lib.ObjectId;
-
-/**
- * A snapshot of a replica.
- *
- * @see LeaderSnapshot
- */
-public class ReplicaSnapshot {
-	final KetchReplica replica;
-	ObjectId accepted;
-	ObjectId committed;
-	KetchReplica.State state;
-	String error;
-	long retryAtMillis;
-
-	ReplicaSnapshot(KetchReplica replica) {
-		this.replica = replica;
-	}
-
-	/**
-	 * Get the replica this snapshot describes the state of
-	 *
-	 * @return the replica this snapshot describes the state of
-	 */
-	public KetchReplica getReplica() {
-		return replica;
-	}
-
-	/**
-	 * Get current state of the replica
-	 *
-	 * @return current state of the replica
-	 */
-	public KetchReplica.State getState() {
-		return state;
-	}
-
-	/**
-	 * Get last known Git commit at {@code refs/txn/accepted}
-	 *
-	 * @return last known Git commit at {@code refs/txn/accepted}
-	 */
-	@Nullable
-	public ObjectId getAccepted() {
-		return accepted;
-	}
-
-	/**
-	 * Get last known Git commit at {@code refs/txn/committed}
-	 *
-	 * @return last known Git commit at {@code refs/txn/committed}
-	 */
-	@Nullable
-	public ObjectId getCommitted() {
-		return committed;
-	}
-
-	/**
-	 * Get error message
-	 *
-	 * @return if {@link #getState()} ==
-	 *         {@link org.eclipse.jgit.internal.ketch.KetchReplica.State#OFFLINE}
-	 *         an optional human-readable message from the transport system
-	 *         explaining the failure.
-	 */
-	@Nullable
-	public String getErrorMessage() {
-		return error;
-	}
-
-	/**
-	 * Get when the leader will retry communication with the offline or lagging
-	 * replica
-	 *
-	 * @return time (usually in the future) when the leader will retry
-	 *         communication with the offline or lagging replica; null if no
-	 *         retry is scheduled or necessary.
-	 */
-	@Nullable
-	public Date getRetryAt() {
-		return retryAtMillis > 0 ? new Date(retryAtMillis) : null;
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java
deleted file mode 100644
index 05da5be..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import java.io.IOException;
-import java.util.List;
-
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-/**
- * One round-trip to all replicas proposing a log entry.
- * <p>
- * In Raft a log entry represents a state transition at a specific index in the
- * replicated log. The leader can only append log entries to the log.
- * <p>
- * In Ketch a log entry is recorded under the {@code refs/txn} namespace. This
- * occurs when:
- * <ul>
- * <li>a replica wants to establish itself as a new leader by proposing a new
- * term (see {@link ElectionRound})
- * <li>an established leader wants to gain consensus on new {@link Proposal}s
- * (see {@link ProposalRound})
- * </ul>
- */
-abstract class Round {
-	final KetchLeader leader;
-	final LogIndex acceptedOldIndex;
-	LogIndex acceptedNewIndex;
-	List<ReceiveCommand> stageCommands;
-
-	Round(KetchLeader leader, LogIndex head) {
-		this.leader = leader;
-		this.acceptedOldIndex = head;
-	}
-
-	KetchSystem getSystem() {
-		return leader.getSystem();
-	}
-
-	/**
-	 * Creates a commit for {@code refs/txn/accepted} and calls
-	 * {@link #runAsync(AnyObjectId)} to begin execution of the round across
-	 * the system.
-	 * <p>
-	 * If references are being updated (such as in a {@link ProposalRound}) the
-	 * RefTree may be modified.
-	 * <p>
-	 * Invoked without {@link KetchLeader#lock} to build objects.
-	 *
-	 * @throws IOException
-	 *             the round cannot build new objects within the leader's
-	 *             repository. The leader may be unable to execute.
-	 */
-	abstract void start() throws IOException;
-
-	/**
-	 * Asynchronously distribute the round's new value for
-	 * {@code refs/txn/accepted} to all replicas.
-	 * <p>
-	 * Invoked by {@link #start()} after new commits have been created for the
-	 * log. The method passes {@code newId} to {@link KetchLeader} to be
-	 * distributed to all known replicas.
-	 *
-	 * @param newId
-	 *            new value for {@code refs/txn/accepted}.
-	 */
-	void runAsync(AnyObjectId newId) {
-		acceptedNewIndex = acceptedOldIndex.nextIndex(newId);
-		leader.runAsync(this);
-	}
-
-	/**
-	 * Notify the round it was accepted by a majority of the system.
-	 * <p>
-	 * Invoked by the leader with {@link KetchLeader#lock} held by the caller.
-	 */
-	abstract void success();
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
deleted file mode 100644
index 40d86e1..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.treewalk.EmptyTreeIterator;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.filter.TreeFilter;
-
-/**
- * Constructs a set of commands to stage content during a proposal.
- */
-public class StageBuilder {
-	/**
-	 * Acceptable number of references to send in a single stage transaction.
-	 * <p>
-	 * If the number of unique objects exceeds this amount the builder will
-	 * attempt to decrease the reference count by chaining commits..
-	 */
-	private static final int SMALL_BATCH_SIZE = 5;
-
-	/**
-	 * Acceptable number of commits to chain together using parent pointers.
-	 * <p>
-	 * When staging many unique commits the {@link StageBuilder} batches
-	 * together unrelated commits as parents of a temporary commit. After the
-	 * proposal completes the temporary commit is discarded and can be garbage
-	 * collected by all replicas.
-	 */
-	private static final int TEMP_PARENT_BATCH_SIZE = 128;
-
-	private static final byte[] PEEL = { ' ', '^' };
-
-	private final String txnStage;
-	private final String txnId;
-
-	/**
-	 * Construct a stage builder for a transaction.
-	 *
-	 * @param txnStageNamespace
-	 *            namespace for transaction references to build
-	 *            {@code "txnStageNamespace/txnId.n"} style names.
-	 * @param txnId
-	 *            identifier used to name temporary staging refs.
-	 */
-	public StageBuilder(String txnStageNamespace, ObjectId txnId) {
-		this.txnStage = txnStageNamespace;
-		this.txnId = txnId.name();
-	}
-
-	/**
-	 * Compare two RefTrees and return commands to stage new objects.
-	 * <p>
-	 * This method ignores the lineage between the two RefTrees and does a
-	 * straight diff on the two trees. New objects will be staged. The diff
-	 * strategy is useful to catch-up a lagging replica, without sending every
-	 * intermediate step. This may mean the replica does not have the same
-	 * object set as other replicas if there are rewinds or branch deletes.
-	 *
-	 * @param git
-	 *            source repository to read {@code oldTree} and {@code newTree}
-	 *            from.
-	 * @param oldTree
-	 *            accepted RefTree on the replica ({@code refs/txn/accepted}).
-	 *            Use {@link org.eclipse.jgit.lib.ObjectId#zeroId()} if the
-	 *            remote does not have any ref tree, e.g. a new replica catching
-	 *            up.
-	 * @param newTree
-	 *            RefTree being sent to the replica. The trees will be compared.
-	 * @return list of commands to create {@code "refs/txn/stage/..."}
-	 *         references on replicas anchoring new objects into the repository
-	 *         while a transaction gains consensus.
-	 * @throws java.io.IOException
-	 *             {@code git} cannot be accessed to compare {@code oldTree} and
-	 *             {@code newTree} to build the object set.
-	 */
-	public List<ReceiveCommand> makeStageList(Repository git, ObjectId oldTree,
-			ObjectId newTree) throws IOException {
-		try (RevWalk rw = new RevWalk(git);
-				TreeWalk tw = new TreeWalk(rw.getObjectReader());
-				ObjectInserter ins = git.newObjectInserter()) {
-			if (AnyObjectId.isEqual(oldTree, ObjectId.zeroId())) {
-				tw.addTree(new EmptyTreeIterator());
-			} else {
-				tw.addTree(rw.parseTree(oldTree));
-			}
-			tw.addTree(rw.parseTree(newTree));
-			tw.setFilter(TreeFilter.ANY_DIFF);
-			tw.setRecursive(true);
-
-			Set<ObjectId> newObjs = new HashSet<>();
-			while (tw.next()) {
-				if (tw.getRawMode(1) == TYPE_GITLINK
-						&& !tw.isPathSuffix(PEEL, 2)) {
-					newObjs.add(tw.getObjectId(1));
-				}
-			}
-
-			List<ReceiveCommand> cmds = makeStageList(newObjs, git, ins);
-			ins.flush();
-			return cmds;
-		}
-	}
-
-	/**
-	 * Construct a set of commands to stage objects on a replica.
-	 *
-	 * @param newObjs
-	 *            objects to send to a replica.
-	 * @param git
-	 *            local repository to read source objects from. Required to
-	 *            perform minification of {@code newObjs}.
-	 * @param inserter
-	 *            inserter to write temporary commit objects during minification
-	 *            if many new branches are created by {@code newObjs}.
-	 * @return list of commands to create {@code "refs/txn/stage/..."}
-	 *         references on replicas anchoring {@code newObjs} into the
-	 *         repository while a transaction gains consensus.
-	 * @throws java.io.IOException
-	 *             {@code git} cannot be accessed to perform minification of
-	 *             {@code newObjs}.
-	 */
-	public List<ReceiveCommand> makeStageList(Set<ObjectId> newObjs,
-			@Nullable Repository git, @Nullable ObjectInserter inserter)
-					throws IOException {
-		if (git == null || newObjs.size() <= SMALL_BATCH_SIZE) {
-			// Without a source repository can only construct unique set.
-			List<ReceiveCommand> cmds = new ArrayList<>(newObjs.size());
-			for (ObjectId id : newObjs) {
-				stage(cmds, id);
-			}
-			return cmds;
-		}
-
-		List<ReceiveCommand> cmds = new ArrayList<>();
-		List<RevCommit> commits = new ArrayList<>();
-		reduceObjects(cmds, commits, git, newObjs);
-
-		if (inserter == null || commits.size() <= 1
-				|| (cmds.size() + commits.size()) <= SMALL_BATCH_SIZE) {
-			// Without an inserter to aggregate commits, or for a small set of
-			// commits just send one stage ref per commit.
-			for (RevCommit c : commits) {
-				stage(cmds, c.copy());
-			}
-			return cmds;
-		}
-
-		// 'commits' is sorted most recent to least recent commit.
-		// Group batches of commits and build a chain.
-		// TODO(sop) Cluster by restricted graphs to support filtering.
-		ObjectId tip = null;
-		for (int end = commits.size(); end > 0;) {
-			int start = Math.max(0, end - TEMP_PARENT_BATCH_SIZE);
-			List<RevCommit> batch = commits.subList(start, end);
-			List<ObjectId> parents = new ArrayList<>(1 + batch.size());
-			if (tip != null) {
-				parents.add(tip);
-			}
-			parents.addAll(batch);
-
-			CommitBuilder b = new CommitBuilder();
-			b.setTreeId(batch.get(0).getTree());
-			b.setParentIds(parents);
-			b.setAuthor(tmpAuthor(batch));
-			b.setCommitter(b.getAuthor());
-			tip = inserter.insert(b);
-			end = start;
-		}
-		stage(cmds, tip);
-		return cmds;
-	}
-
-	private static PersonIdent tmpAuthor(List<RevCommit> commits) {
-		// Construct a predictable author using most recent commit time.
-		int t = 0;
-		for (int i = 0; i < commits.size();) {
-			t = Math.max(t, commits.get(i).getCommitTime());
-		}
-		String name = "Ketch Stage"; //$NON-NLS-1$
-		String email = "tmp@tmp"; //$NON-NLS-1$
-		return new PersonIdent(name, email, t * 1000L, 0);
-	}
-
-	private void reduceObjects(List<ReceiveCommand> cmds,
-			List<RevCommit> commits, Repository git,
-			Set<ObjectId> newObjs) throws IOException {
-		try (RevWalk rw = new RevWalk(git)) {
-			rw.setRetainBody(false);
-
-			for (ObjectId id : newObjs) {
-				RevObject obj = rw.parseAny(id);
-				if (obj instanceof RevCommit) {
-					rw.markStart((RevCommit) obj);
-				} else {
-					stage(cmds, id);
-				}
-			}
-
-			for (RevCommit c; (c = rw.next()) != null;) {
-				commits.add(c);
-				rw.markUninteresting(c);
-			}
-		}
-	}
-
-	private void stage(List<ReceiveCommand> cmds, ObjectId id) {
-		int estLen = txnStage.length() + txnId.length() + 5;
-		StringBuilder n = new StringBuilder(estLen);
-		n.append(txnStage).append(txnId).append('.');
-		n.append(Integer.toHexString(cmds.size()));
-		cmds.add(new ReceiveCommand(ObjectId.zeroId(), id, n.toString()));
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/TimeIsUncertainException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/TimeIsUncertainException.java
deleted file mode 100644
index f665e6a..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/TimeIsUncertainException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.ketch;
-
-import java.io.IOException;
-
-import org.eclipse.jgit.internal.JGitText;
-
-class TimeIsUncertainException extends IOException {
-	private static final long serialVersionUID = 1L;
-
-	TimeIsUncertainException() {
-		super(JGitText.get().timeIsUncertain);
-	}
-
-	TimeIsUncertainException(Exception e) {
-		super(JGitText.get().timeIsUncertain, e);
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java
deleted file mode 100644
index dfe0375..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * Distributed consensus system built on Git.
- */
-package org.eclipse.jgit.internal.ketch;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedObjectReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedObjectReachabilityChecker.java
similarity index 88%
rename from org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedObjectReachabilityChecker.java
rename to org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedObjectReachabilityChecker.java
index 89aef7d..d805649 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedObjectReachabilityChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedObjectReachabilityChecker.java
@@ -7,7 +7,7 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -21,12 +21,16 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder;
+import org.eclipse.jgit.revwalk.BitmapWalker;
+import org.eclipse.jgit.revwalk.ObjectReachabilityChecker;
+import org.eclipse.jgit.revwalk.ObjectWalk;
+import org.eclipse.jgit.revwalk.RevObject;
 
 /**
  * Checks if all objects are reachable from certain starting points using
  * bitmaps.
  */
-class BitmappedObjectReachabilityChecker
+public class BitmappedObjectReachabilityChecker
 		implements ObjectReachabilityChecker {
 
 	private final ObjectWalk walk;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityChecker.java
similarity index 91%
rename from org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java
rename to org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityChecker.java
index 0d9c459..37721ad 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/BitmappedReachabilityChecker.java
@@ -7,7 +7,7 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -23,12 +23,17 @@
 import org.eclipse.jgit.lib.BitmapIndex.Bitmap;
 import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.revwalk.ReachabilityChecker;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.revwalk.filter.RevFilter;
 
 /**
  * Checks the reachability using bitmaps.
  */
-class BitmappedReachabilityChecker implements ReachabilityChecker {
+public class BitmappedReachabilityChecker implements ReachabilityChecker {
 
 	private final RevWalk walk;
 
@@ -42,7 +47,7 @@
 	 * @throws IOException
 	 *             if the index or the object reader cannot be opened.
 	 */
-	BitmappedReachabilityChecker(RevWalk walk)
+	public BitmappedReachabilityChecker(RevWalk walk)
 			throws IOException {
 		this.walk = walk;
 		if (walk.getObjectReader().getBitmapIndex() == null) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianObjectReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/PedestrianObjectReachabilityChecker.java
similarity index 82%
rename from org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianObjectReachabilityChecker.java
rename to org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/PedestrianObjectReachabilityChecker.java
index df5d68a..1d1f5fd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianObjectReachabilityChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/PedestrianObjectReachabilityChecker.java
@@ -7,7 +7,7 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import java.io.IOException;
 import java.io.InvalidObjectException;
@@ -17,12 +17,18 @@
 import java.util.stream.Stream;
 
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.revwalk.ObjectReachabilityChecker;
+import org.eclipse.jgit.revwalk.ObjectWalk;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevSort;
 
 /**
  * Checks if all objects are reachable from certain starting points doing a
  * walk.
  */
-class PedestrianObjectReachabilityChecker implements ObjectReachabilityChecker {
+public class PedestrianObjectReachabilityChecker
+		implements ObjectReachabilityChecker {
 	private final ObjectWalk walk;
 
 	/**
@@ -31,7 +37,7 @@
 	 * @param walk
 	 *            ObjectWalk instance to reuse. Caller retains ownership.
 	 */
-	PedestrianObjectReachabilityChecker(ObjectWalk walk) {
+	public PedestrianObjectReachabilityChecker(ObjectWalk walk) {
 		this.walk = walk;
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/PedestrianReachabilityChecker.java
similarity index 84%
rename from org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java
rename to org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/PedestrianReachabilityChecker.java
index 5dc0377..a03306b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/revwalk/PedestrianReachabilityChecker.java
@@ -7,7 +7,7 @@
  *
  * SPDX-License-Identifier: BSD-3-Clause
  */
-package org.eclipse.jgit.revwalk;
+package org.eclipse.jgit.internal.revwalk;
 
 import java.io.IOException;
 import java.util.Collection;
@@ -17,12 +17,16 @@
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.revwalk.ReachabilityChecker;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 /**
  * Checks the reachability walking the graph from the starters towards the
  * target.
  */
-class PedestrianReachabilityChecker implements ReachabilityChecker {
+public class PedestrianReachabilityChecker implements ReachabilityChecker {
 
 	private final boolean topoSort;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
index 876cbec..26d5b5b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
@@ -13,7 +13,6 @@
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.COMPACT;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_REST;
-import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_TXN;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.RECEIVE;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
@@ -45,7 +44,6 @@
 import org.eclipse.jgit.internal.storage.reftable.ReftableCompactor;
 import org.eclipse.jgit.internal.storage.reftable.ReftableConfig;
 import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
-import org.eclipse.jgit.internal.storage.reftree.RefTreeNames;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -95,7 +93,6 @@
 	private Set<ObjectId> allHeadsAndTags;
 	private Set<ObjectId> allTags;
 	private Set<ObjectId> nonHeads;
-	private Set<ObjectId> txnHeads;
 	private Set<ObjectId> tagTargets;
 
 	/**
@@ -318,7 +315,6 @@
 			allHeadsAndTags = new HashSet<>();
 			allTags = new HashSet<>();
 			nonHeads = new HashSet<>();
-			txnHeads = new HashSet<>();
 			tagTargets = new HashSet<>();
 			for (Ref ref : refsBefore) {
 				if (ref.isSymbolic() || ref.getObjectId() == null) {
@@ -328,8 +324,6 @@
 					allHeads.add(ref.getObjectId());
 				} else if (isTag(ref)) {
 					allTags.add(ref.getObjectId());
-				} else if (RefTreeNames.isRefTree(refdb, ref.getName())) {
-					txnHeads.add(ref.getObjectId());
 				} else {
 					nonHeads.add(ref.getObjectId());
 				}
@@ -355,7 +349,6 @@
 			try {
 				packHeads(pm);
 				packRest(pm);
-				packRefTreeGraph(pm);
 				packGarbage(pm);
 				objdb.commitPack(newPackDesc, toPrune());
 				rollback = false;
@@ -559,19 +552,6 @@
 		}
 	}
 
-	private void packRefTreeGraph(ProgressMonitor pm) throws IOException {
-		if (txnHeads.isEmpty())
-			return;
-
-		try (PackWriter pw = newPackWriter()) {
-			for (ObjectIdSet packedObjs : newPackObj)
-				pw.excludeObjects(packedObjs);
-			pw.preparePack(pm, txnHeads, NONE);
-			if (0 < pw.getObjectCount())
-				writePack(GC_TXN, pw, pm, 0 /* unknown pack size */);
-		}
-	}
-
 	private void packGarbage(ProgressMonitor pm) throws IOException {
 		PackConfig cfg = new PackConfig(packConfig);
 		cfg.setReuseDeltas(true);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
index 4dab3b2..46ec87d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
@@ -105,13 +105,6 @@
 		GC_REST,
 
 		/**
-		 * RefTreeGraph pack was created by Git garbage collection.
-		 *
-		 * @see DfsGarbageCollector
-		 */
-		GC_TXN,
-
-		/**
 		 * Pack was created by Git garbage collection.
 		 * <p>
 		 * This pack contains only unreachable garbage that was found during the
@@ -133,7 +126,6 @@
 						.add(COMPACT)
 						.add(GC)
 						.add(GC_REST)
-						.add(GC_TXN)
 						.add(UNREACHABLE_GARBAGE)
 						.build();
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectRepresentation.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectRepresentation.java
index 3f113a3..8e124e3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectRepresentation.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjectRepresentation.java
@@ -48,7 +48,6 @@
 		switch (pack.getPackDescription().getPackSource()) {
 		case GC:
 		case GC_REST:
-		case GC_TXN:
 			return true;
 		default:
 			return false;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java
index 0c8755f..4f418ab 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java
@@ -533,7 +533,6 @@
 		switch (s) {
 		case GC:
 		case GC_REST:
-		case GC_TXN:
 			return true;
 		default:
 			return false;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java
index b1e9552..96ca690 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java
@@ -607,8 +607,15 @@
 
 	private void readFully(long position, byte[] dstbuf, int dstoff, int cnt,
 			DfsReader ctx) throws IOException {
-		if (ctx.copy(this, position, dstbuf, dstoff, cnt) != cnt)
-			throw new EOFException();
+		while (cnt > 0) {
+			int copied = ctx.copy(this, position, dstbuf, dstoff, cnt);
+			if (copied == 0) {
+				throw new EOFException();
+			}
+			position += copied;
+			dstoff += copied;
+			cnt -= copied;
+		}
 	}
 
 	ObjectLoader load(DfsReader ctx, long pos)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java
index 8a54431..6c3b056 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java
@@ -17,6 +17,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
 import org.eclipse.jgit.annotations.Nullable;
@@ -62,11 +63,12 @@
 		reftableDatabase = new ReftableDatabase() {
 			@Override
 			public MergedReftable openMergedReftable() throws IOException {
-				DfsReftableDatabase.this.getLock().lock();
+				Lock l = DfsReftableDatabase.this.getLock();
+				l.lock();
 				try {
 					return new MergedReftable(stack().readers());
 				} finally {
-					DfsReftableDatabase.this.getLock().unlock();
+					l.unlock();
 				}
 			}
 		};
@@ -176,6 +178,13 @@
 
 	/** {@inheritDoc} */
 	@Override
+	public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes)
+			throws IOException {
+		return reftableDatabase.getRefsByPrefixWithExclusions(include, excludes);
+	}
+
+	/** {@inheritDoc} */
+	@Override
 	public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException {
 		if (!getReftableConfig().isIndexObjects()) {
 			return super.getTipsWithSha1(id);
@@ -207,7 +216,8 @@
 
 	@Override
 	void clearCache() {
-		getLock().lock();
+		ReentrantLock l = getLock();
+		l.lock();
 		try {
 			if (ctx != null) {
 				ctx.close();
@@ -219,7 +229,7 @@
 				stack = null;
 			}
 		} finally {
-			getLock().unlock();
+			l.unlock();
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java
index 45d9c85..1036535 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java
@@ -25,7 +25,7 @@
 final class ByteArrayWindow extends ByteWindow {
 	private final byte[] array;
 
-	ByteArrayWindow(PackFile pack, long o, byte[] b) {
+	ByteArrayWindow(Pack pack, long o, byte[] b) {
 		super(pack, o, b.length);
 		array = b;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java
index 8703216..b687757 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java
@@ -27,7 +27,7 @@
 final class ByteBufferWindow extends ByteWindow {
 	private final ByteBuffer buffer;
 
-	ByteBufferWindow(PackFile pack, long o, ByteBuffer b) {
+	ByteBufferWindow(Pack pack, long o, ByteBuffer b) {
 		super(pack, o, b.capacity());
 		buffer = b;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteWindow.java
index 159f31c..31e7ead 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteWindow.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteWindow.java
@@ -27,7 +27,7 @@
  * </p>
  */
 abstract class ByteWindow {
-	protected final PackFile pack;
+	protected final Pack pack;
 
 	protected final long start;
 
@@ -37,13 +37,13 @@
 	 * Constructor for ByteWindow.
 	 *
 	 * @param p
-	 *            a {@link org.eclipse.jgit.internal.storage.file.PackFile}.
+	 *            a {@link org.eclipse.jgit.internal.storage.file.Pack}.
 	 * @param s
 	 *            where the byte window starts in the pack file
 	 * @param n
 	 *            size of the byte window
 	 */
-	protected ByteWindow(PackFile p, long s, int n) {
+	protected ByteWindow(Pack p, long s, int n) {
 		pack = p;
 		start = s;
 		end = start + n;
@@ -53,8 +53,8 @@
 		return (int) (end - start);
 	}
 
-	final boolean contains(PackFile neededFile, long neededPos) {
-		return pack == neededFile && start <= neededPos && neededPos < end;
+	final boolean contains(Pack neededPack, long neededPos) {
+		return pack == neededPack && start <= neededPos && neededPos < end;
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java
index 9c7a2e7..7dedeb5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java
@@ -239,7 +239,7 @@
 	}
 
 	@Override
-	PackFile openPack(File pack) throws IOException {
+	Pack openPack(File pack) throws IOException {
 		return wrapped.openPack(pack);
 	}
 
@@ -250,7 +250,7 @@
 	}
 
 	@Override
-	Collection<PackFile> getPacks() {
+	Collection<Pack> getPacks() {
 		return wrapped.getPacks();
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/DeltaBaseCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/DeltaBaseCache.java
index cef5a33..69cebad 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/DeltaBaseCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/DeltaBaseCache.java
@@ -49,7 +49,7 @@
 		cache = new Slot[CACHE_SZ];
 	}
 
-	Entry get(PackFile pack, long position) {
+	Entry get(Pack pack, long position) {
 		Slot e = cache[hash(position)];
 		if (e == null)
 			return null;
@@ -63,7 +63,7 @@
 		return null;
 	}
 
-	void store(final PackFile pack, final long position,
+	void store(final Pack pack, final long position,
 			final byte[] data, final int objectType) {
 		if (data.length > maxByteCount)
 			return; // Too large to cache.
@@ -146,7 +146,7 @@
 
 		Slot lruNext;
 
-		PackFile provider;
+		Pack provider;
 
 		long position;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java
index 11ed10c..01dd27d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java
@@ -71,7 +71,7 @@
 	abstract InsertLooseObjectResult insertUnpackedObject(File tmp,
 			ObjectId id, boolean createDuplicate) throws IOException;
 
-	abstract PackFile openPack(File pack) throws IOException;
+	abstract Pack openPack(File pack) throws IOException;
 
-	abstract Collection<PackFile> getPacks();
+	abstract Collection<Pack> getPacks();
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java
index e613a58..a80fa837 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java
@@ -21,7 +21,9 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Collectors;
 
@@ -107,12 +109,13 @@
 	 * @throws IOException on I/O errors
 	 */
 	public void compactFully() throws IOException {
-		reftableDatabase.getLock().lock();
+		Lock l = reftableDatabase.getLock();
+		l.lock();
 		try {
 			reftableStack.compactFully();
 			reftableDatabase.clearCache();
 		} finally {
-			reftableDatabase.getLock().unlock();
+			l.unlock();
 		}
 	}
 
@@ -179,6 +182,13 @@
 
 	/** {@inheritDoc} */
 	@Override
+	public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes)
+			throws IOException {
+		return reftableDatabase.getRefsByPrefixWithExclusions(include, excludes);
+	}
+
+	/** {@inheritDoc} */
+	@Override
 	public List<Ref> getAdditionalRefs() throws IOException {
 		return Collections.emptyList();
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java
index 71130f0..f02c861 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java
@@ -21,6 +21,7 @@
 import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
+import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
@@ -65,6 +66,8 @@
 
 	private final Runnable onChange;
 
+	private final SecureRandom random = new SecureRandom();
+
 	private final Supplier<Config> configSupplier;
 
 	// Used for stats & testing.
@@ -328,8 +331,9 @@
 	}
 
 	private String filename(long low, long high) {
-		return String.format("%012x-%012x", //$NON-NLS-1$
-				Long.valueOf(low), Long.valueOf(high));
+		return String.format("%012x-%012x-%08x", //$NON-NLS-1$
+				Long.valueOf(low), Long.valueOf(high),
+				Integer.valueOf(random.nextInt()));
 	}
 
 	/**
@@ -599,6 +603,9 @@
 
 		@Override
 		public boolean equals(Object other) {
+			if (other == null) {
+				return false;
+			}
 			Segment o = (Segment) other;
 			return o.bytes == bytes && o.log == log && o.start == start
 					&& o.end == end;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
index fd052ce..fecced1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
@@ -40,7 +40,6 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateHandle;
 import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateRepository;
-import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
 import org.eclipse.jgit.lib.BaseRepositoryBuilder;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ConfigConstants;
@@ -182,9 +181,6 @@
 			if (StringUtils.equalsIgnoreCase(reftype,
 					ConfigConstants.CONFIG_REF_STORAGE_REFTABLE)) {
 				refs = new FileReftableDatabase(this);
-			} else if (StringUtils.equalsIgnoreCase(reftype,
-					ConfigConstants.CONFIG_REFSTORAGE_REFTREE)) {
-				refs = new RefTreeDatabase(this, new RefDirectory(this));
 			} else {
 				throw new IOException(JGitText.get().unknownRepositoryFormat);
 			}
@@ -247,7 +243,7 @@
 
 		RefUpdate head = updateRef(Constants.HEAD);
 		head.disableRefLog();
-		head.link(Constants.R_HEADS + Constants.MASTER);
+		head.link(Constants.R_HEADS + getInitialBranch());
 
 		final boolean fileMode;
 		if (getFS().supportsExecute()) {
@@ -640,7 +636,7 @@
 		refsHeadsFile.delete();
 		// RefDirectory wants to create the refs/ directory from scratch, so
 		// remove that too.
-		refsFile.delete();
+			refsFile.delete();
 		// remove HEAD so its previous invalid value doesn't cause issues.
 		headFile.delete();
 
@@ -668,7 +664,7 @@
 				for (ReflogEntry e : logs) {
 					logWriter.log(r.getName(), e);
 				}
-			}
+		}
 		}
 
 		try (RevWalk rw = new RevWalk(this)) {
@@ -731,7 +727,7 @@
 			throws IOException {
 		File reftableDir = new File(getDirectory(), Constants.REFTABLE);
 		File headFile = new File(getDirectory(), Constants.HEAD);
-		if (reftableDir.exists() && reftableDir.listFiles().length > 0) {
+		if (reftableDir.exists() && FileUtils.hasFiles(reftableDir.toPath())) {
 			throw new IOException(JGitText.get().reftableDirExists);
 		}
 
@@ -763,9 +759,11 @@
 			}
 		} else {
 			FileUtils.delete(packedRefs, FileUtils.SKIP_MISSING);
-			FileUtils.delete(headFile);
-			FileUtils.delete(logsDir, FileUtils.RECURSIVE);
-			FileUtils.delete(refsFile, FileUtils.RECURSIVE);
+			FileUtils.delete(headFile, FileUtils.SKIP_MISSING);
+			FileUtils.delete(logsDir,
+					FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
+			FileUtils.delete(refsFile,
+					FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
 			for (String r : additional) {
 				new File(getDirectory(), r).delete();
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
index 54ff7d2..8c48cd9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
@@ -12,8 +12,10 @@
 
 import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_FILESTORE_ATTRIBUTES;
 import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_TIMESTAMP_RESOLUTION;
+
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.time.Duration;
 import java.time.Instant;
@@ -221,14 +223,20 @@
 		this.file = file;
 		this.lastRead = Instant.now();
 		this.fileStoreAttributeCache = useConfig
-				? FS.getFileStoreAttributes(file.toPath().getParent())
+				? FS.getFileStoreAttributes(file.toPath())
 				: FALLBACK_FILESTORE_ATTRIBUTES;
 		BasicFileAttributes fileAttributes = null;
 		try {
 			fileAttributes = FS.DETECTED.fileAttributes(file);
+		} catch (NoSuchFileException e) {
+			this.lastModified = Instant.EPOCH;
+			this.size = 0L;
+			this.fileKey = MISSING_FILEKEY;
+			return;
 		} catch (IOException e) {
-			this.lastModified = Instant.ofEpochMilli(file.lastModified());
-			this.size = file.length();
+			LOG.error(e.getMessage(), e);
+			this.lastModified = Instant.EPOCH;
+			this.size = 0L;
 			this.fileKey = MISSING_FILEKEY;
 			return;
 		}
@@ -309,9 +317,14 @@
 			currLastModified = fileAttributes.lastModifiedTime().toInstant();
 			currSize = fileAttributes.size();
 			currFileKey = getFileKey(fileAttributes);
+		} catch (NoSuchFileException e) {
+			currLastModified = Instant.EPOCH;
+			currSize = 0L;
+			currFileKey = MISSING_FILEKEY;
 		} catch (IOException e) {
-			currLastModified = Instant.ofEpochMilli(path.lastModified());
-			currSize = path.length();
+			LOG.error(e.getMessage(), e);
+			currLastModified = Instant.EPOCH;
+			currSize = 0L;
 			currFileKey = MISSING_FILEKEY;
 		}
 		sizeChanged = isSizeChanged(currSize);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
index 1f2fe10..9ffff9f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
@@ -12,6 +12,8 @@
 
 import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -60,7 +62,6 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
-import org.eclipse.jgit.internal.storage.reftree.RefTreeNames;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
@@ -206,16 +207,16 @@
 	 * gc.log.
 	 *
 	 * @return the collection of
-	 *         {@link org.eclipse.jgit.internal.storage.file.PackFile}'s which
+	 *         {@link org.eclipse.jgit.internal.storage.file.Pack}'s which
 	 *         are newly created
 	 * @throws java.io.IOException
 	 * @throws java.text.ParseException
 	 *             If the configuration parameter "gc.pruneexpire" couldn't be
 	 *             parsed
 	 */
-	// TODO(ms): change signature and return Future<Collection<PackFile>>
+	// TODO(ms): change signature and return Future<Collection<Pack>>
 	@SuppressWarnings("FutureReturnValueIgnored")
-	public Collection<PackFile> gc() throws IOException, ParseException {
+	public Collection<Pack> gc() throws IOException, ParseException {
 		if (!background) {
 			return doGc();
 		}
@@ -225,9 +226,9 @@
 			return Collections.emptyList();
 		}
 
-		Callable<Collection<PackFile>> gcTask = () -> {
+		Callable<Collection<Pack>> gcTask = () -> {
 			try {
-				Collection<PackFile> newPacks = doGc();
+				Collection<Pack> newPacks = doGc();
 				if (automatic && tooManyLooseObjects()) {
 					String message = JGitText.get().gcTooManyUnpruned;
 					gcLog.write(message);
@@ -259,14 +260,14 @@
 		return (executor != null) ? executor : WorkQueue.getExecutor();
 	}
 
-	private Collection<PackFile> doGc() throws IOException, ParseException {
+	private Collection<Pack> doGc() throws IOException, ParseException {
 		if (automatic && !needGc()) {
 			return Collections.emptyList();
 		}
 		pm.start(6 /* tasks */);
 		packRefs();
 		// TODO: implement reflog_expire(pm, repo);
-		Collection<PackFile> newPacks = repack();
+		Collection<Pack> newPacks = repack();
 		prune(Collections.emptySet());
 		// TODO: implement rerere_gc(pm);
 		return newPacks;
@@ -282,7 +283,7 @@
 	 * @param existing
 	 * @throws IOException
 	 */
-	private void loosen(ObjectDirectoryInserter inserter, ObjectReader reader, PackFile pack, HashSet<ObjectId> existing)
+	private void loosen(ObjectDirectoryInserter inserter, ObjectReader reader, Pack pack, HashSet<ObjectId> existing)
 			throws IOException {
 		for (PackIndex.MutableEntry entry : pack) {
 			ObjectId oid = entry.toObjectId();
@@ -314,10 +315,10 @@
 	 * @throws ParseException
 	 * @throws IOException
 	 */
-	private void deleteOldPacks(Collection<PackFile> oldPacks,
-			Collection<PackFile> newPacks) throws ParseException, IOException {
+	private void deleteOldPacks(Collection<Pack> oldPacks,
+			Collection<Pack> newPacks) throws ParseException, IOException {
 		HashSet<ObjectId> ids = new HashSet<>();
-		for (PackFile pack : newPacks) {
+		for (Pack pack : newPacks) {
 			for (PackIndex.MutableEntry entry : pack) {
 				ids.add(entry.toObjectId());
 			}
@@ -330,12 +331,12 @@
 
 		prunePreserved();
 		long packExpireDate = getPackExpireDate();
-		oldPackLoop: for (PackFile oldPack : oldPacks) {
+		oldPackLoop: for (Pack oldPack : oldPacks) {
 			checkCancelled();
 			String oldName = oldPack.getPackName();
 			// check whether an old pack file is also among the list of new
 			// pack files. Then we must not delete it.
-			for (PackFile newPack : newPacks)
+			for (Pack newPack : newPacks)
 				if (oldName.equals(newPack.getPackName()))
 					continue oldPackLoop;
 
@@ -347,7 +348,7 @@
 				if (shouldLoosen) {
 					loosen(inserter, reader, oldPack, ids);
 				}
-				prunePack(oldName);
+				prunePack(oldPack.getPackFile());
 			}
 		}
 
@@ -361,19 +362,17 @@
 	 * moves the pack file to the preserved directory
 	 *
 	 * @param packFile
-	 * @param packName
-	 * @param ext
 	 * @param deleteOptions
 	 * @throws IOException
 	 */
-	private void removeOldPack(File packFile, String packName, PackExt ext,
-			int deleteOptions) throws IOException {
+	private void removeOldPack(PackFile packFile, int deleteOptions)
+			throws IOException {
 		if (pconfig.isPreserveOldPacks()) {
 			File oldPackDir = repo.getObjectDatabase().getPreservedDirectory();
 			FileUtils.mkdir(oldPackDir, true);
 
-			String oldPackName = "pack-" + packName + ".old-" + ext.getExtension();  //$NON-NLS-1$ //$NON-NLS-2$
-			File oldPackFile = new File(oldPackDir, oldPackName);
+			PackFile oldPackFile = packFile
+					.createPreservedForDirectory(oldPackDir);
 			FileUtils.rename(packFile, oldPackFile);
 		} else {
 			FileUtils.delete(packFile, deleteOptions);
@@ -402,27 +401,21 @@
 	 * ".index" file and when failing to delete the ".pack" file we are left
 	 * with a ".pack" file without a ".index" file.
 	 *
-	 * @param packName
+	 * @param packFile
 	 */
-	private void prunePack(String packName) {
-		PackExt[] extensions = PackExt.values();
+	private void prunePack(PackFile packFile) {
 		try {
 			// Delete the .pack file first and if this fails give up on deleting
 			// the other files
 			int deleteOptions = FileUtils.RETRY | FileUtils.SKIP_MISSING;
-			for (PackExt ext : extensions)
-				if (PackExt.PACK.equals(ext)) {
-					File f = nameFor(packName, "." + ext.getExtension()); //$NON-NLS-1$
-					removeOldPack(f, packName, ext, deleteOptions);
-					break;
-				}
+			removeOldPack(packFile.create(PackExt.PACK), deleteOptions);
+
 			// The .pack file has been deleted. Delete as many as the other
 			// files as you can.
 			deleteOptions |= FileUtils.IGNORE_ERRORS;
-			for (PackExt ext : extensions) {
+			for (PackExt ext : PackExt.values()) {
 				if (!PackExt.PACK.equals(ext)) {
-					File f = nameFor(packName, "." + ext.getExtension()); //$NON-NLS-1$
-					removeOldPack(f, packName, ext, deleteOptions);
+					removeOldPack(packFile.create(ext), deleteOptions);
 				}
 			}
 		} catch (IOException e) {
@@ -439,7 +432,7 @@
 	 */
 	public void prunePacked() throws IOException {
 		ObjectDirectory objdb = repo.getObjectDatabase();
-		Collection<PackFile> packs = objdb.getPacks();
+		Collection<Pack> packs = objdb.getPacks();
 		File objects = repo.getObjectsDirectory();
 		String[] fanout = objects.list();
 
@@ -467,7 +460,7 @@
 							continue;
 						}
 						boolean found = false;
-						for (PackFile p : packs) {
+						for (Pack p : packs) {
 							checkCancelled();
 							if (p.hasObject(id)) {
 								found = true;
@@ -789,8 +782,8 @@
 	 *             reflog-entries or during writing to the packfiles
 	 *             {@link java.io.IOException} occurs
 	 */
-	public Collection<PackFile> repack() throws IOException {
-		Collection<PackFile> toBeDeleted = repo.getObjectDatabase().getPacks();
+	public Collection<Pack> repack() throws IOException {
+		Collection<Pack> toBeDeleted = repo.getObjectDatabase().getPacks();
 
 		long time = System.currentTimeMillis();
 		Collection<Ref> refsBefore = getAllRefs();
@@ -802,7 +795,6 @@
 		Set<ObjectId> txnHeads = new HashSet<>();
 		Set<ObjectId> tagTargets = new HashSet<>();
 		Set<ObjectId> indexObjects = listNonHEADIndexObjects();
-		RefDatabase refdb = repo.getRefDatabase();
 
 		for (Ref ref : refsBefore) {
 			checkCancelled();
@@ -814,8 +806,6 @@
 				allHeads.add(ref.getObjectId());
 			} else if (isTag(ref)) {
 				allTags.add(ref.getObjectId());
-			} else if (RefTreeNames.isRefTree(refdb, ref.getName())) {
-				txnHeads.add(ref.getObjectId());
 			} else {
 				nonHeads.add(ref.getObjectId());
 			}
@@ -825,10 +815,10 @@
 		}
 
 		List<ObjectIdSet> excluded = new LinkedList<>();
-		for (PackFile f : repo.getObjectDatabase().getPacks()) {
+		for (Pack p : repo.getObjectDatabase().getPacks()) {
 			checkCancelled();
-			if (f.shouldBeKept())
-				excluded.add(f.getIndex());
+			if (p.shouldBeKept())
+				excluded.add(p.getIndex());
 		}
 
 		// Don't exclude tags that are also branch tips
@@ -846,8 +836,8 @@
 			nonHeads.clear();
 		}
 
-		List<PackFile> ret = new ArrayList<>(2);
-		PackFile heads = null;
+		List<Pack> ret = new ArrayList<>(2);
+		Pack heads = null;
 		if (!allHeadsAndTags.isEmpty()) {
 			heads = writePack(allHeadsAndTags, PackWriter.NONE, allTags,
 					tagTargets, excluded);
@@ -857,13 +847,13 @@
 			}
 		}
 		if (!nonHeads.isEmpty()) {
-			PackFile rest = writePack(nonHeads, allHeadsAndTags, PackWriter.NONE,
+			Pack rest = writePack(nonHeads, allHeadsAndTags, PackWriter.NONE,
 					tagTargets, excluded);
 			if (rest != null)
 				ret.add(rest);
 		}
 		if (!txnHeads.isEmpty()) {
-			PackFile txn = writePack(txnHeads, PackWriter.NONE, PackWriter.NONE,
+			Pack txn = writePack(txnHeads, PackWriter.NONE, PackWriter.NONE,
 					null, excluded);
 			if (txn != null)
 				ret.add(txn);
@@ -977,20 +967,21 @@
 			return;
 		}
 
-		String base = null;
+		String latestId = null;
 		for (String n : fileNames) {
-			if (n.endsWith(PACK_EXT) || n.endsWith(KEEP_EXT)) {
-				base = n.substring(0, n.lastIndexOf('.'));
-			} else {
-				if (base == null || !n.startsWith(base)) {
-					try {
-						Path delete = packDir.resolve(n);
-						FileUtils.delete(delete.toFile(),
-								FileUtils.RETRY | FileUtils.SKIP_MISSING);
-						LOG.warn(JGitText.get().deletedOrphanInPackDir, delete);
-					} catch (IOException e) {
-						LOG.error(e.getMessage(), e);
-					}
+			PackFile pf = new PackFile(packDir.toFile(), n);
+			PackExt ext = pf.getPackExt();
+			if (ext.equals(PACK) || ext.equals(KEEP)) {
+				latestId = pf.getId();
+			}
+			if (latestId == null || !pf.getId().equals(latestId)) {
+				// no pack or keep for this id
+				try {
+					FileUtils.delete(pf,
+							FileUtils.RETRY | FileUtils.SKIP_MISSING);
+					LOG.warn(JGitText.get().deletedOrphanInPackDir, pf);
+				} catch (IOException e) {
+					LOG.error(e.getMessage(), e);
 				}
 			}
 		}
@@ -1133,7 +1124,7 @@
 		}
 	}
 
-	private PackFile writePack(@NonNull Set<? extends ObjectId> want,
+	private Pack writePack(@NonNull Set<? extends ObjectId> want,
 			@NonNull Set<? extends ObjectId> have, @NonNull Set<ObjectId> tags,
 			Set<ObjectId> tagTargets, List<ObjectIdSet> excludeObjects)
 			throws IOException {
@@ -1172,7 +1163,7 @@
 			checkCancelled();
 
 			// create temporary files
-			String id = pw.computeName().getName();
+			ObjectId id = pw.computeName();
 			File packdir = repo.getObjectDatabase().getPackDirectory();
 			packdir.mkdirs();
 			tmpPack = File.createTempFile("gc_", ".pack_tmp", packdir); //$NON-NLS-1$ //$NON-NLS-2$
@@ -1222,7 +1213,8 @@
 			}
 
 			// rename the temporary files to real files
-			File realPack = nameFor(id, ".pack"); //$NON-NLS-1$
+			File packDir = repo.getObjectDatabase().getPackDirectory();
+			PackFile realPack = new PackFile(packDir, id, PackExt.PACK);
 
 			repo.getObjectDatabase().closeAllPackHandles(realPack);
 			tmpPack.setReadOnly();
@@ -1232,8 +1224,7 @@
 				File tmpExt = tmpEntry.getValue();
 				tmpExt.setReadOnly();
 
-				File realExt = nameFor(id,
-						"." + tmpEntry.getKey().getExtension()); //$NON-NLS-1$
+				PackFile realExt = new PackFile(packDir, id, tmpEntry.getKey());
 				try {
 					FileUtils.rename(tmpExt, realExt,
 							StandardCopyOption.ATOMIC_MOVE);
@@ -1279,11 +1270,6 @@
 		}
 	}
 
-	private File nameFor(String name, String ext) {
-		File packdir = repo.getObjectDatabase().getPackDirectory();
-		return new File(packdir, "pack-" + name + ext); //$NON-NLS-1$
-	}
-
 	private void checkCancelled() throws CancelledException {
 		if (pm.isCancelled() || Thread.currentThread().isInterrupted()) {
 			throw new CancelledException(JGitText.get().operationCanceled);
@@ -1360,13 +1346,13 @@
 	 */
 	public RepoStatistics getStatistics() throws IOException {
 		RepoStatistics ret = new RepoStatistics();
-		Collection<PackFile> packs = repo.getObjectDatabase().getPacks();
-		for (PackFile f : packs) {
-			ret.numberOfPackedObjects += f.getIndex().getObjectCount();
+		Collection<Pack> packs = repo.getObjectDatabase().getPacks();
+		for (Pack p : packs) {
+			ret.numberOfPackedObjects += p.getIndex().getObjectCount();
 			ret.numberOfPackFiles++;
-			ret.sizeOfPackedObjects += f.getPackFile().length();
-			if (f.getBitmapIndex() != null)
-				ret.numberOfBitmaps += f.getBitmapIndex().getBitmapCount();
+			ret.sizeOfPackedObjects += p.getPackFile().length();
+			if (p.getBitmapIndex() != null)
+				ret.numberOfBitmaps += p.getBitmapIndex().getBitmapCount();
 		}
 		File objDir = repo.getObjectsDirectory();
 		String[] fanout = objDir.list();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LargePackedWholeObject.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LargePackedWholeObject.java
index ee4bbc1..e2fbd7a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LargePackedWholeObject.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LargePackedWholeObject.java
@@ -30,12 +30,12 @@
 
 	private final int headerLength;
 
-	private final PackFile pack;
+	private final Pack pack;
 
 	private final FileObjectDatabase db;
 
 	LargePackedWholeObject(int type, long size, long objectOffset,
-			int headerLength, PackFile pack, FileObjectDatabase db) {
+			int headerLength, Pack pack, FileObjectDatabase db) {
 		this.type = type;
 		this.size = size;
 		this.objectOffset = objectOffset;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java
index 9d04062..f112947 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java
@@ -17,6 +17,7 @@
 
 import org.eclipse.jgit.internal.storage.pack.CachedPack;
 import org.eclipse.jgit.internal.storage.pack.ObjectToPack;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
 import org.eclipse.jgit.internal.storage.pack.StoredObjectRepresentation;
 
@@ -25,31 +26,31 @@
 
 	private final String[] packNames;
 
-	private PackFile[] packs;
+	private Pack[] packs;
 
 	LocalCachedPack(ObjectDirectory odb, List<String> packNames) {
 		this.odb = odb;
 		this.packNames = packNames.toArray(new String[0]);
 	}
 
-	LocalCachedPack(List<PackFile> packs) {
+	LocalCachedPack(List<Pack> packs) {
 		odb = null;
 		packNames = null;
-		this.packs = packs.toArray(new PackFile[0]);
+		this.packs = packs.toArray(new Pack[0]);
 	}
 
 	/** {@inheritDoc} */
 	@Override
 	public long getObjectCount() throws IOException {
 		long cnt = 0;
-		for (PackFile pack : getPacks())
+		for (Pack pack : getPacks())
 			cnt += pack.getObjectCount();
 		return cnt;
 	}
 
 	void copyAsIs(PackOutputStream out, WindowCursor wc)
 			throws IOException {
-		for (PackFile pack : getPacks())
+		for (Pack pack : getPacks())
 			pack.copyPackAsIs(out, wc);
 	}
 
@@ -58,7 +59,7 @@
 	public boolean hasObject(ObjectToPack obj, StoredObjectRepresentation rep) {
 		try {
 			LocalObjectRepresentation local = (LocalObjectRepresentation) rep;
-			for (PackFile pack : getPacks()) {
+			for (Pack pack : getPacks()) {
 				if (local.pack == pack)
 					return true;
 			}
@@ -68,9 +69,9 @@
 		}
 	}
 
-	private PackFile[] getPacks() throws FileNotFoundException {
+	private Pack[] getPacks() throws FileNotFoundException {
 		if (packs == null) {
-			PackFile[] p = new PackFile[packNames.length];
+			Pack[] p = new Pack[packNames.length];
 			for (int i = 0; i < packNames.length; i++)
 				p[i] = getPackFile(packNames[i]);
 			packs = p;
@@ -78,8 +79,8 @@
 		return packs;
 	}
 
-	private PackFile getPackFile(String packName) throws FileNotFoundException {
-		for (PackFile pack : odb.getPacks()) {
+	private Pack getPackFile(String packName) throws FileNotFoundException {
+		for (Pack pack : odb.getPacks()) {
 			if (packName.equals(pack.getPackName()))
 				return pack;
 		}
@@ -88,6 +89,6 @@
 
 	private String getPackFilePath(String packName) {
 		final File packDir = odb.getPackDirectory();
-		return new File(packDir, "pack-" + packName + ".pack").getPath(); //$NON-NLS-1$ //$NON-NLS-2$
+		return new PackFile(packDir, packName, PackExt.PACK).getPath();
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectRepresentation.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectRepresentation.java
index 3950dde..559718a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectRepresentation.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectRepresentation.java
@@ -16,40 +16,40 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 class LocalObjectRepresentation extends StoredObjectRepresentation {
-	static LocalObjectRepresentation newWhole(PackFile f, long p, long length) {
+	static LocalObjectRepresentation newWhole(Pack pack, long offset, long length) {
 		LocalObjectRepresentation r = new LocalObjectRepresentation() {
 			@Override
 			public int getFormat() {
 				return PACK_WHOLE;
 			}
 		};
-		r.pack = f;
-		r.offset = p;
+		r.pack = pack;
+		r.offset = offset;
 		r.length = length;
 		return r;
 	}
 
-	static LocalObjectRepresentation newDelta(PackFile f, long p, long n,
+	static LocalObjectRepresentation newDelta(Pack pack, long offset, long length,
 			ObjectId base) {
 		LocalObjectRepresentation r = new Delta();
-		r.pack = f;
-		r.offset = p;
-		r.length = n;
+		r.pack = pack;
+		r.offset = offset;
+		r.length = length;
 		r.baseId = base;
 		return r;
 	}
 
-	static LocalObjectRepresentation newDelta(PackFile f, long p, long n,
+	static LocalObjectRepresentation newDelta(Pack pack, long offset, long length,
 			long base) {
 		LocalObjectRepresentation r = new Delta();
-		r.pack = f;
-		r.offset = p;
-		r.length = n;
+		r.pack = pack;
+		r.offset = offset;
+		r.length = length;
 		r.baseOffset = base;
 		return r;
 	}
 
-	PackFile pack;
+	Pack pack;
 
 	long offset;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectToPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectToPack.java
index 4a0ac1f..ac6cd21 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectToPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalObjectToPack.java
@@ -17,7 +17,7 @@
 /** {@link ObjectToPack} for {@link ObjectDirectory}. */
 class LocalObjectToPack extends ObjectToPack {
 	/** Pack to reuse compressed data from, otherwise null. */
-	PackFile pack;
+	Pack pack;
 
 	/** Offset of the object's header in {@link #pack}. */
 	long offset;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java
new file mode 100644
index 0000000..33621a1
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LooseObjects.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2009, Google Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.StandardCopyOption;
+import java.text.MessageFormat;
+import java.util.Set;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.FileObjectDatabase.InsertLooseObjectResult;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Traditional file system based loose objects handler.
+ * <p>
+ * This is the loose object representation for a Git object database, where
+ * objects are stored loose by hashing them into directories by their
+ * {@link org.eclipse.jgit.lib.ObjectId}.
+ */
+class LooseObjects {
+	private static final Logger LOG = LoggerFactory
+			.getLogger(LooseObjects.class);
+
+	/**
+	 * Maximum number of attempts to read a loose object for which a stale file
+	 * handle exception is thrown
+	 */
+	private final static int MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS = 5;
+
+	private final File directory;
+
+	private final UnpackedObjectCache unpackedObjectCache;
+
+	/**
+	 * Initialize a reference to an on-disk object directory.
+	 *
+	 * @param dir
+	 *            the location of the <code>objects</code> directory.
+	 */
+	LooseObjects(File dir) {
+		directory = dir;
+		unpackedObjectCache = new UnpackedObjectCache();
+	}
+
+	/**
+	 * Getter for the field <code>directory</code>.
+	 *
+	 * @return the location of the <code>objects</code> directory.
+	 */
+	File getDirectory() {
+		return directory;
+	}
+
+	void create() throws IOException {
+		FileUtils.mkdirs(directory);
+	}
+
+	void close() {
+		unpackedObjectCache().clear();
+	}
+
+	/** {@inheritDoc} */
+	@Override
+	public String toString() {
+		return "LooseObjects[" + directory + "]"; //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	boolean hasCached(AnyObjectId id) {
+		return unpackedObjectCache().isUnpacked(id);
+	}
+
+	/**
+	 * Does the requested object exist as a loose object?
+	 *
+	 * @param objectId
+	 *            identity of the object to test for existence of.
+	 * @return {@code true} if the specified object is stored as a loose object.
+	 */
+	boolean has(AnyObjectId objectId) {
+		return fileFor(objectId).exists();
+	}
+
+	/**
+	 * Find objects matching the prefix abbreviation.
+	 *
+	 * @param matches
+	 *            set to add any located ObjectIds to. This is an output
+	 *            parameter.
+	 * @param id
+	 *            prefix to search for.
+	 * @param matchLimit
+	 *            maximum number of results to return. At most this many
+	 *            ObjectIds should be added to matches before returning.
+	 * @return {@code true} if the matches were exhausted before reaching
+	 *         {@code maxLimit}.
+	 */
+	boolean resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
+			int matchLimit) {
+		String fanOut = id.name().substring(0, 2);
+		String[] entries = new File(directory, fanOut).list();
+		if (entries != null) {
+			for (String e : entries) {
+				if (e.length() != Constants.OBJECT_ID_STRING_LENGTH - 2) {
+					continue;
+				}
+				try {
+					ObjectId entId = ObjectId.fromString(fanOut + e);
+					if (id.prefixCompare(entId) == 0) {
+						matches.add(entId);
+					}
+				} catch (IllegalArgumentException notId) {
+					continue;
+				}
+				if (matches.size() > matchLimit) {
+					return false;
+				}
+			}
+		}
+		return true;
+	}
+
+	ObjectLoader open(WindowCursor curs, AnyObjectId id) throws IOException {
+		int readAttempts = 0;
+		while (readAttempts < MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS) {
+			readAttempts++;
+			File path = fileFor(id);
+			try {
+				return getObjectLoader(curs, path, id);
+			} catch (FileNotFoundException noFile) {
+				if (path.exists()) {
+					throw noFile;
+				}
+				break;
+			} catch (IOException e) {
+				if (!FileUtils.isStaleFileHandleInCausalChain(e)) {
+					throw e;
+				}
+				if (LOG.isDebugEnabled()) {
+					LOG.debug(MessageFormat.format(
+							JGitText.get().looseObjectHandleIsStale, id.name(),
+							Integer.valueOf(readAttempts), Integer.valueOf(
+									MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS)));
+				}
+			}
+		}
+		unpackedObjectCache().remove(id);
+		return null;
+	}
+
+	/**
+	 * Provides a loader for an objectId
+	 *
+	 * @param curs
+	 *            cursor on the database
+	 * @param path
+	 *            the path of the loose object
+	 * @param id
+	 *            the object id
+	 * @return a loader for the loose file object
+	 * @throws IOException
+	 *             when file does not exist or it could not be opened
+	 */
+	ObjectLoader getObjectLoader(WindowCursor curs, File path, AnyObjectId id)
+			throws IOException {
+		try (FileInputStream in = new FileInputStream(path)) {
+			unpackedObjectCache().add(id);
+			return UnpackedObject.open(in, path, id, curs);
+		}
+	}
+
+	/**
+	 * <p>
+	 * Getter for the field <code>unpackedObjectCache</code>.
+	 * </p>
+	 * This accessor is particularly useful to allow mocking of this class for
+	 * testing purposes.
+	 *
+	 * @return the cache of the objects currently unpacked.
+	 */
+	UnpackedObjectCache unpackedObjectCache() {
+		return unpackedObjectCache;
+	}
+
+	long getSize(WindowCursor curs, AnyObjectId id) throws IOException {
+		File f = fileFor(id);
+		try (FileInputStream in = new FileInputStream(f)) {
+			unpackedObjectCache().add(id);
+			return UnpackedObject.getSize(in, id, curs);
+		} catch (FileNotFoundException noFile) {
+			if (f.exists()) {
+				throw noFile;
+			}
+			unpackedObjectCache().remove(id);
+			return -1;
+		}
+	}
+
+	InsertLooseObjectResult insert(File tmp, ObjectId id) throws IOException {
+		final File dst = fileFor(id);
+		if (dst.exists()) {
+			// We want to be extra careful and avoid replacing an object
+			// that already exists. We can't be sure renameTo() would
+			// fail on all platforms if dst exists, so we check first.
+			//
+			FileUtils.delete(tmp, FileUtils.RETRY);
+			return InsertLooseObjectResult.EXISTS_LOOSE;
+		}
+
+		try {
+			return tryMove(tmp, dst, id);
+		} catch (NoSuchFileException e) {
+			// It's possible the directory doesn't exist yet as the object
+			// directories are always lazily created. Note that we try the
+			// rename/move first as the directory likely does exist.
+			//
+			// Create the directory.
+			//
+			FileUtils.mkdir(dst.getParentFile(), true);
+		} catch (IOException e) {
+			// Any other IO error is considered a failure.
+			//
+			LOG.error(e.getMessage(), e);
+			FileUtils.delete(tmp, FileUtils.RETRY);
+			return InsertLooseObjectResult.FAILURE;
+		}
+
+		try {
+			return tryMove(tmp, dst, id);
+		} catch (IOException e) {
+			// The object failed to be renamed into its proper location and
+			// it doesn't exist in the repository either. We really don't
+			// know what went wrong, so fail.
+			//
+			LOG.error(e.getMessage(), e);
+			FileUtils.delete(tmp, FileUtils.RETRY);
+			return InsertLooseObjectResult.FAILURE;
+		}
+	}
+
+	private InsertLooseObjectResult tryMove(File tmp, File dst, ObjectId id)
+			throws IOException {
+		Files.move(FileUtils.toPath(tmp), FileUtils.toPath(dst),
+				StandardCopyOption.ATOMIC_MOVE);
+		dst.setReadOnly();
+		unpackedObjectCache().add(id);
+		return InsertLooseObjectResult.INSERTED;
+	}
+
+	/**
+	 * Compute the location of a loose object file.
+	 *
+	 * @param objectId
+	 *            identity of the object to get the File location for.
+	 * @return {@link java.io.File} location of the specified loose object.
+	 */
+	File fileFor(AnyObjectId objectId) {
+		String n = objectId.name();
+		String d = n.substring(0, 2);
+		String f = n.substring(2);
+		return new File(new File(getDirectory(), d), f);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
index 106cddd..627facc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
@@ -11,33 +11,25 @@
 package org.eclipse.jgit.internal.storage.file;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 
 import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.PackInvalidException;
-import org.eclipse.jgit.errors.PackMismatchException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.ObjectToPack;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
@@ -45,7 +37,6 @@
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectDatabase;
 import org.eclipse.jgit.lib.ObjectId;
@@ -54,8 +45,6 @@
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Traditional file system based {@link org.eclipse.jgit.lib.ObjectDatabase}.
@@ -63,7 +52,7 @@
  * This is the classical object database representation for a Git repository,
  * where objects are stored loose by hashing them into directories by their
  * {@link org.eclipse.jgit.lib.ObjectId}, or are stored in compressed containers
- * known as {@link org.eclipse.jgit.internal.storage.file.PackFile}s.
+ * known as {@link org.eclipse.jgit.internal.storage.file.Pack}s.
  * <p>
  * Optionally an object database can reference one or more alternates; other
  * ObjectDatabase instances that are searched in addition to the current
@@ -76,19 +65,9 @@
  * considered.
  */
 public class ObjectDirectory extends FileObjectDatabase {
-	private static final Logger LOG = LoggerFactory
-			.getLogger(ObjectDirectory.class);
-
-	private static final PackList NO_PACKS = new PackList(
-			FileSnapshot.DIRTY, new PackFile[0]);
-
 	/** Maximum number of candidates offered as resolutions of abbreviation. */
 	private static final int RESOLVE_ABBREV_LIMIT = 256;
 
-	/** Maximum number of attempts to read a loose object for which a stale file
-	 *  handle exception is thrown */
-	final static int MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS = 5;
-
 	private final AlternateHandle handle = new AlternateHandle(this);
 
 	private final Config config;
@@ -97,9 +76,11 @@
 
 	private final File infoDirectory;
 
-	private final File packDirectory;
+	private final LooseObjects loose;
 
-	private final File preservedDirectory;
+	private final PackDirectory packed;
+
+	private final PackDirectory preserved;
 
 	private final File alternatesFile;
 
@@ -107,16 +88,12 @@
 
 	private final AtomicReference<AlternateHandle[]> alternates;
 
-	private final UnpackedObjectCache unpackedObjectCache;
-
 	private final File shallowFile;
 
 	private FileSnapshot shallowFileSnapshot = FileSnapshot.DIRTY;
 
 	private Set<ObjectId> shallowCommitsIds;
 
-	final AtomicReference<PackList> packList;
-
 	/**
 	 * Initialize a reference to an on-disk object directory.
 	 *
@@ -140,11 +117,12 @@
 		config = cfg;
 		objects = dir;
 		infoDirectory = new File(objects, "info"); //$NON-NLS-1$
-		packDirectory = new File(objects, "pack"); //$NON-NLS-1$
-		preservedDirectory = new File(packDirectory, "preserved"); //$NON-NLS-1$
+		File packDirectory = new File(objects, "pack"); //$NON-NLS-1$
+		File preservedDirectory = new File(packDirectory, "preserved"); //$NON-NLS-1$
 		alternatesFile = new File(objects, Constants.INFO_ALTERNATES);
-		packList = new AtomicReference<>(NO_PACKS);
-		unpackedObjectCache = new UnpackedObjectCache();
+		loose = new LooseObjects(objects);
+		packed = new PackDirectory(config, packDirectory);
+		preserved = new PackDirectory(config, preservedDirectory);
 		this.fs = fs;
 		this.shallowFile = shallowFile;
 
@@ -162,7 +140,7 @@
 	/** {@inheritDoc} */
 	@Override
 	public final File getDirectory() {
-		return objects;
+		return loose.getDirectory();
 	}
 
 	/**
@@ -171,7 +149,7 @@
 	 * @return the location of the <code>pack</code> directory.
 	 */
 	public final File getPackDirectory() {
-		return packDirectory;
+		return packed.getDirectory();
 	}
 
 	/**
@@ -180,7 +158,7 @@
 	 * @return the location of the <code>preserved</code> directory.
 	 */
 	public final File getPreservedDirectory() {
-		return preservedDirectory;
+		return preserved.getDirectory();
 	}
 
 	/** {@inheritDoc} */
@@ -192,9 +170,9 @@
 	/** {@inheritDoc} */
 	@Override
 	public void create() throws IOException {
-		FileUtils.mkdirs(objects);
+		loose.create();
 		FileUtils.mkdir(infoDirectory);
-		FileUtils.mkdir(packDirectory);
+		packed.create();
 	}
 
 	/** {@inheritDoc} */
@@ -216,13 +194,9 @@
 	/** {@inheritDoc} */
 	@Override
 	public void close() {
-		unpackedObjectCache().clear();
+		loose.close();
 
-		final PackList packs = packList.get();
-		if (packs != NO_PACKS && packList.compareAndSet(packs, NO_PACKS)) {
-			for (PackFile p : packs.packs)
-				p.close();
-		}
+		packed.close();
 
 		// Fully close all loaded alternates and clear the alternate list.
 		AlternateHandle[] alt = alternates.get();
@@ -234,12 +208,8 @@
 
 	/** {@inheritDoc} */
 	@Override
-	public Collection<PackFile> getPacks() {
-		PackList list = packList.get();
-		if (list == NO_PACKS)
-			list = scanPacks(list);
-		PackFile[] packs = list.packs;
-		return Collections.unmodifiableCollection(Arrays.asList(packs));
+	public Collection<Pack> getPacks() {
+		return packed.getPacks();
 	}
 
 	/**
@@ -248,27 +218,27 @@
 	 * Add a single existing pack to the list of available pack files.
 	 */
 	@Override
-	public PackFile openPack(File pack)
-			throws IOException {
-		final String p = pack.getName();
-		if (p.length() != 50 || !p.startsWith("pack-") || !p.endsWith(".pack")) //$NON-NLS-1$ //$NON-NLS-2$
-			throw new IOException(MessageFormat.format(JGitText.get().notAValidPack, pack));
-
-		// The pack and index are assumed to exist. The existence of other
-		// extensions needs to be explicitly checked.
-		//
-		int extensions = PACK.getBit() | INDEX.getBit();
-		final String base = p.substring(0, p.length() - 4);
-		for (PackExt ext : PackExt.values()) {
-			if ((extensions & ext.getBit()) == 0) {
-				final String name = base + ext.getExtension();
-				if (new File(pack.getParentFile(), name).exists())
-					extensions |= ext.getBit();
-			}
+	public Pack openPack(File pack) throws IOException {
+		PackFile pf;
+		try {
+			pf = new PackFile(pack);
+		} catch (IllegalArgumentException e) {
+			throw new IOException(
+					MessageFormat.format(JGitText.get().notAValidPack, pack),
+					e);
 		}
 
-		PackFile res = new PackFile(pack, extensions);
-		insertPack(res);
+		String p = pf.getName();
+		// TODO(nasserg): See if PackFile can do these checks instead
+		if (p.length() != 50 || !p.startsWith("pack-") //$NON-NLS-1$
+				|| !pf.getPackExt().equals(PACK)) {
+			throw new IOException(
+					MessageFormat.format(JGitText.get().notAValidPack, pack));
+		}
+
+		PackFile bitmapIdx = pf.create(BITMAP_INDEX);
+		Pack res = new Pack(pack, bitmapIdx.exists() ? bitmapIdx : null);
+		packed.insert(res);
 		return res;
 	}
 
@@ -281,8 +251,14 @@
 	/** {@inheritDoc} */
 	@Override
 	public boolean has(AnyObjectId objectId) {
-		return unpackedObjectCache().isUnpacked(objectId)
-				|| hasPackedInSelfOrAlternate(objectId, null)
+		return loose.hasCached(objectId)
+				|| hasPackedOrLooseInSelfOrAlternate(objectId)
+				|| (restoreFromSelfOrAlternate(objectId, null)
+						&& hasPackedOrLooseInSelfOrAlternate(objectId));
+	}
+
+	private boolean hasPackedOrLooseInSelfOrAlternate(AnyObjectId objectId) {
+		return hasPackedInSelfOrAlternate(objectId, null)
 				|| hasLooseInSelfOrAlternate(objectId, null);
 	}
 
@@ -304,7 +280,7 @@
 
 	private boolean hasLooseInSelfOrAlternate(AnyObjectId objectId,
 			Set<AlternateHandle.Id> skips) {
-		if (fileFor(objectId).exists()) {
+		if (loose.has(objectId)) {
 			return true;
 		}
 		skips = addMe(skips);
@@ -319,25 +295,7 @@
 	}
 
 	boolean hasPackedObject(AnyObjectId objectId) {
-		PackList pList;
-		do {
-			pList = packList.get();
-			for (PackFile p : pList.packs) {
-				try {
-					if (p.hasObject(objectId))
-						return true;
-				} catch (IOException e) {
-					// The hasObject call should have only touched the index,
-					// so any failure here indicates the index is unreadable
-					// by this process, and the pack is likewise not readable.
-					LOG.warn(MessageFormat.format(
-							JGitText.get().unableToReadPackfile,
-							p.getPackFile().getAbsolutePath()), e);
-					removePack(p);
-				}
-			}
-		} while (searchPacksAgain(pList));
-		return false;
+		return packed.has(objectId);
 	}
 
 	@Override
@@ -349,41 +307,11 @@
 	private void resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
 			Set<AlternateHandle.Id> skips)
 			throws IOException {
-		// Go through the packs once. If we didn't find any resolutions
-		// scan for new packs and check once more.
-		int oldSize = matches.size();
-		PackList pList;
-		do {
-			pList = packList.get();
-			for (PackFile p : pList.packs) {
-				try {
-					p.resolve(matches, id, RESOLVE_ABBREV_LIMIT);
-					p.resetTransientErrorCount();
-				} catch (IOException e) {
-					handlePackError(e, p);
-				}
-				if (matches.size() > RESOLVE_ABBREV_LIMIT)
-					return;
-			}
-		} while (matches.size() == oldSize && searchPacksAgain(pList));
+		if (!packed.resolve(matches, id, RESOLVE_ABBREV_LIMIT))
+			return;
 
-		String fanOut = id.name().substring(0, 2);
-		String[] entries = new File(getDirectory(), fanOut).list();
-		if (entries != null) {
-			for (String e : entries) {
-				if (e.length() != Constants.OBJECT_ID_STRING_LENGTH - 2)
-					continue;
-				try {
-					ObjectId entId = ObjectId.fromString(fanOut + e);
-					if (id.prefixCompare(entId) == 0)
-						matches.add(entId);
-				} catch (IllegalArgumentException notId) {
-					continue;
-				}
-				if (matches.size() > RESOLVE_ABBREV_LIMIT)
-					return;
-			}
-		}
+		if (!loose.resolve(matches, id, RESOLVE_ABBREV_LIMIT))
+			return;
 
 		skips = addMe(skips);
 		for (AlternateHandle alt : myAlternates()) {
@@ -399,7 +327,16 @@
 	@Override
 	ObjectLoader openObject(WindowCursor curs, AnyObjectId objectId)
 			throws IOException {
-		if (unpackedObjectCache().isUnpacked(objectId)) {
+		ObjectLoader ldr = openObjectWithoutRestoring(curs, objectId);
+		if (ldr == null && restoreFromSelfOrAlternate(objectId, null)) {
+			ldr = openObjectWithoutRestoring(curs, objectId);
+		}
+		return ldr;
+	}
+
+	private ObjectLoader openObjectWithoutRestoring(WindowCursor curs, AnyObjectId objectId)
+			throws IOException {
+		if (loose.hasCached(objectId)) {
 			ObjectLoader ldr = openLooseObject(curs, objectId);
 			if (ldr != null) {
 				return ldr;
@@ -450,99 +387,28 @@
 	}
 
 	ObjectLoader openPackedObject(WindowCursor curs, AnyObjectId objectId) {
-		PackList pList;
-		do {
-			SEARCH: for (;;) {
-				pList = packList.get();
-				for (PackFile p : pList.packs) {
-					try {
-						ObjectLoader ldr = p.get(curs, objectId);
-						p.resetTransientErrorCount();
-						if (ldr != null)
-							return ldr;
-					} catch (PackMismatchException e) {
-						// Pack was modified; refresh the entire pack list.
-						if (searchPacksAgain(pList))
-							continue SEARCH;
-					} catch (IOException e) {
-						handlePackError(e, p);
-					}
-				}
-				break SEARCH;
-			}
-		} while (searchPacksAgain(pList));
-		return null;
+		return packed.open(curs, objectId);
 	}
 
 	@Override
 	ObjectLoader openLooseObject(WindowCursor curs, AnyObjectId id)
 			throws IOException {
-		int readAttempts = 0;
-		while (readAttempts < MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS) {
-			readAttempts++;
-			File path = fileFor(id);
-			try {
-				return getObjectLoader(curs, path, id);
-			} catch (FileNotFoundException noFile) {
-				if (path.exists()) {
-					throw noFile;
-				}
-				break;
-			} catch (IOException e) {
-				if (!FileUtils.isStaleFileHandleInCausalChain(e)) {
-					throw e;
-				}
-				if (LOG.isDebugEnabled()) {
-					LOG.debug(MessageFormat.format(
-							JGitText.get().looseObjectHandleIsStale, id.name(),
-							Integer.valueOf(readAttempts), Integer.valueOf(
-									MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS)));
-				}
-			}
-		}
-		unpackedObjectCache().remove(id);
-		return null;
-	}
-
-	/**
-	 * Provides a loader for an objectId
-	 *
-	 * @param curs
-	 *            cursor on the database
-	 * @param path
-	 *            the path of the loose object
-	 * @param id
-	 *            the object id
-	 * @return a loader for the loose file object
-	 * @throws IOException
-	 *             when file does not exist or it could not be opened
-	 */
-	ObjectLoader getObjectLoader(WindowCursor curs, File path, AnyObjectId id)
-			throws IOException {
-		try (FileInputStream in = new FileInputStream(path)) {
-			unpackedObjectCache().add(id);
-			return UnpackedObject.open(in, path, id, curs);
-		}
-	}
-
-	/**
-	 * <p>
-	 * Getter for the field <code>unpackedObjectCache</code>.
-	 * </p>
-	 * This accessor is particularly useful to allow mocking of this class for
-	 * testing purposes.
-	 *
-	 * @return the cache of the objects currently unpacked.
-	 */
-	UnpackedObjectCache unpackedObjectCache() {
-		return unpackedObjectCache;
+		return loose.open(curs, id);
 	}
 
 	@Override
-	long getObjectSize(WindowCursor curs, AnyObjectId id)
-			throws IOException {
-		if (unpackedObjectCache().isUnpacked(id)) {
-			long len = getLooseObjectSize(curs, id);
+	long getObjectSize(WindowCursor curs, AnyObjectId id) throws IOException {
+		long sz = getObjectSizeWithoutRestoring(curs, id);
+		if (0 > sz && restoreFromSelfOrAlternate(id, null)) {
+			sz = getObjectSizeWithoutRestoring(curs, id);
+		}
+		return sz;
+	}
+
+	private long getObjectSizeWithoutRestoring(WindowCursor curs,
+			AnyObjectId id) throws IOException {
+		if (loose.hasCached(id)) {
+			long len = loose.getSize(curs, id);
 			if (0 <= len) {
 				return len;
 			}
@@ -556,7 +422,7 @@
 
 	private long getPackedSizeFromSelfOrAlternate(WindowCursor curs,
 			AnyObjectId id, Set<AlternateHandle.Id> skips) {
-		long len = getPackedObjectSize(curs, id);
+		long len = packed.getSize(curs, id);
 		if (0 <= len) {
 			return len;
 		}
@@ -574,7 +440,7 @@
 
 	private long getLooseSizeFromSelfOrAlternate(WindowCursor curs,
 			AnyObjectId id, Set<AlternateHandle.Id> skips) throws IOException {
-		long len = getLooseObjectSize(curs, id);
+		long len = loose.getSize(curs, id);
 		if (0 <= len) {
 			return len;
 		}
@@ -590,46 +456,6 @@
 		return -1;
 	}
 
-	private long getPackedObjectSize(WindowCursor curs, AnyObjectId id) {
-		PackList pList;
-		do {
-			SEARCH: for (;;) {
-				pList = packList.get();
-				for (PackFile p : pList.packs) {
-					try {
-						long len = p.getObjectSize(curs, id);
-						p.resetTransientErrorCount();
-						if (0 <= len)
-							return len;
-					} catch (PackMismatchException e) {
-						// Pack was modified; refresh the entire pack list.
-						if (searchPacksAgain(pList))
-							continue SEARCH;
-					} catch (IOException e) {
-						handlePackError(e, p);
-					}
-				}
-				break SEARCH;
-			}
-		} while (searchPacksAgain(pList));
-		return -1;
-	}
-
-	private long getLooseObjectSize(WindowCursor curs, AnyObjectId id)
-			throws IOException {
-		File f = fileFor(id);
-		try (FileInputStream in = new FileInputStream(f)) {
-			unpackedObjectCache().add(id);
-			return UnpackedObject.getSize(in, id, curs);
-		} catch (FileNotFoundException noFile) {
-			if (f.exists()) {
-				throw noFile;
-			}
-			unpackedObjectCache().remove(id);
-			return -1;
-		}
-	}
-
 	@Override
 	void selectObjectRepresentation(PackWriter packer, ObjectToPack otp,
 			WindowCursor curs) throws IOException {
@@ -638,25 +464,7 @@
 
 	private void selectObjectRepresentation(PackWriter packer, ObjectToPack otp,
 			WindowCursor curs, Set<AlternateHandle.Id> skips) throws IOException {
-		PackList pList = packList.get();
-		SEARCH: for (;;) {
-			for (PackFile p : pList.packs) {
-				try {
-					LocalObjectRepresentation rep = p.representation(curs, otp);
-					p.resetTransientErrorCount();
-					if (rep != null)
-						packer.select(otp, rep);
-				} catch (PackMismatchException e) {
-					// Pack was modified; refresh the entire pack list.
-					//
-					pList = scanPacks(pList);
-					continue SEARCH;
-				} catch (IOException e) {
-					handlePackError(e, p);
-				}
-			}
-			break SEARCH;
-		}
+		packed.selectRepresentation(packer, otp, curs);
 
 		skips = addMe(skips);
 		for (AlternateHandle h : myAlternates()) {
@@ -666,52 +474,49 @@
 		}
 	}
 
-	private void handlePackError(IOException e, PackFile p) {
-		String warnTmpl = null;
-		int transientErrorCount = 0;
-		String errTmpl = JGitText.get().exceptionWhileReadingPack;
-		if ((e instanceof CorruptObjectException)
-				|| (e instanceof PackInvalidException)) {
-			warnTmpl = JGitText.get().corruptPack;
-			LOG.warn(MessageFormat.format(warnTmpl,
-					p.getPackFile().getAbsolutePath()), e);
-			// Assume the pack is corrupted, and remove it from the list.
-			removePack(p);
-		} else if (e instanceof FileNotFoundException) {
-			if (p.getPackFile().exists()) {
-				errTmpl = JGitText.get().packInaccessible;
-				transientErrorCount = p.incrementTransientErrorCount();
-			} else {
-				warnTmpl = JGitText.get().packWasDeleted;
-				removePack(p);
-			}
-		} else if (FileUtils.isStaleFileHandleInCausalChain(e)) {
-			warnTmpl = JGitText.get().packHandleIsStale;
-			removePack(p);
-		} else {
-			transientErrorCount = p.incrementTransientErrorCount();
+	private boolean restoreFromSelfOrAlternate(AnyObjectId objectId,
+			Set<AlternateHandle.Id> skips) {
+		if (restoreFromSelf(objectId)) {
+			return true;
 		}
-		if (warnTmpl != null) {
-			LOG.warn(MessageFormat.format(warnTmpl,
-					p.getPackFile().getAbsolutePath()), e);
-		} else {
-			if (doLogExponentialBackoff(transientErrorCount)) {
-				// Don't remove the pack from the list, as the error may be
-				// transient.
-				LOG.error(MessageFormat.format(errTmpl,
-						p.getPackFile().getAbsolutePath(),
-						Integer.valueOf(transientErrorCount)), e);
+
+		skips = addMe(skips);
+		for (AlternateHandle alt : myAlternates()) {
+			if (!skips.contains(alt.getId())) {
+				if (alt.db.restoreFromSelfOrAlternate(objectId, skips)) {
+					return true;
+				}
 			}
 		}
+		return false;
 	}
 
-	/**
-	 * @param n
-	 *            count of consecutive failures
-	 * @return @{code true} if i is a power of 2
-	 */
-	private boolean doLogExponentialBackoff(int n) {
-		return (n & (n - 1)) == 0;
+	private boolean restoreFromSelf(AnyObjectId objectId) {
+		Pack preservedPack = preserved.getPack(objectId);
+		if (preservedPack == null) {
+			return false;
+		}
+		PackFile preservedFile = new PackFile(preservedPack.getPackFile());
+		// Restore the index last since the set will be considered for use once
+		// the index appears.
+		for (PackExt ext : PackExt.values()) {
+			if (!INDEX.equals(ext)) {
+				restore(preservedFile.create(ext));
+			}
+		}
+		restore(preservedFile.create(INDEX));
+		return true;
+	}
+
+	private boolean restore(PackFile preservedPack) {
+		PackFile restored = preservedPack
+				.createForDirectory(packed.getDirectory());
+		try {
+			Files.createLink(restored.toPath(), preservedPack.toPath());
+		} catch (IOException e) {
+			return false;
+		}
+		return true;
 	}
 
 	@Override
@@ -719,7 +524,7 @@
 			boolean createDuplicate) throws IOException {
 		// If the object is already in the repository, remove temporary file.
 		//
-		if (unpackedObjectCache().isUnpacked(id)) {
+		if (loose.hasCached(id)) {
 			FileUtils.delete(tmp, FileUtils.RETRY);
 			return InsertLooseObjectResult.EXISTS_LOOSE;
 		}
@@ -727,71 +532,7 @@
 			FileUtils.delete(tmp, FileUtils.RETRY);
 			return InsertLooseObjectResult.EXISTS_PACKED;
 		}
-
-		final File dst = fileFor(id);
-		if (dst.exists()) {
-			// We want to be extra careful and avoid replacing an object
-			// that already exists. We can't be sure renameTo() would
-			// fail on all platforms if dst exists, so we check first.
-			//
-			FileUtils.delete(tmp, FileUtils.RETRY);
-			return InsertLooseObjectResult.EXISTS_LOOSE;
-		}
-
-		try {
-			return tryMove(tmp, dst, id);
-		} catch (NoSuchFileException e) {
-			// It's possible the directory doesn't exist yet as the object
-			// directories are always lazily created. Note that we try the
-			// rename/move first as the directory likely does exist.
-			//
-			// Create the directory.
-			//
-			FileUtils.mkdir(dst.getParentFile(), true);
-		} catch (IOException e) {
-			// Any other IO error is considered a failure.
-			//
-			LOG.error(e.getMessage(), e);
-			FileUtils.delete(tmp, FileUtils.RETRY);
-			return InsertLooseObjectResult.FAILURE;
-		}
-
-		try {
-			return tryMove(tmp, dst, id);
-		} catch (IOException e) {
-			// The object failed to be renamed into its proper location and
-			// it doesn't exist in the repository either. We really don't
-			// know what went wrong, so fail.
-			//
-			LOG.error(e.getMessage(), e);
-			FileUtils.delete(tmp, FileUtils.RETRY);
-			return InsertLooseObjectResult.FAILURE;
-		}
-	}
-
-	private InsertLooseObjectResult tryMove(File tmp, File dst,
-			ObjectId id)
-			throws IOException {
-		Files.move(FileUtils.toPath(tmp), FileUtils.toPath(dst),
-				StandardCopyOption.ATOMIC_MOVE);
-		dst.setReadOnly();
-		unpackedObjectCache().add(id);
-		return InsertLooseObjectResult.INSERTED;
-	}
-
-	boolean searchPacksAgain(PackList old) {
-		// Whether to trust the pack folder's modification time. If set
-		// to false we will always scan the .git/objects/pack folder to
-		// check for new pack files. If set to true (default) we use the
-		// lastmodified attribute of the folder and assume that no new
-		// pack files can be in this folder if his modification time has
-		// not changed.
-		boolean trustFolderStat = config.getBoolean(
-				ConfigConstants.CONFIG_CORE_SECTION,
-				ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true);
-
-		return ((!trustFolderStat) || old.snapshot.isModified(packDirectory))
-				&& old != scanPacks(old);
+		return loose.insert(tmp, id);
 	}
 
 	@Override
@@ -832,182 +573,13 @@
 		return shallowCommitsIds;
 	}
 
-	private void insertPack(PackFile pf) {
-		PackList o, n;
-		do {
-			o = packList.get();
-
-			// If the pack in question is already present in the list
-			// (picked up by a concurrent thread that did a scan?) we
-			// do not want to insert it a second time.
-			//
-			final PackFile[] oldList = o.packs;
-			final String name = pf.getPackFile().getName();
-			for (PackFile p : oldList) {
-				if (name.equals(p.getPackFile().getName()))
-					return;
-			}
-
-			final PackFile[] newList = new PackFile[1 + oldList.length];
-			newList[0] = pf;
-			System.arraycopy(oldList, 0, newList, 1, oldList.length);
-			n = new PackList(o.snapshot, newList);
-		} while (!packList.compareAndSet(o, n));
-	}
-
-	private void removePack(PackFile deadPack) {
-		PackList o, n;
-		do {
-			o = packList.get();
-
-			final PackFile[] oldList = o.packs;
-			final int j = indexOf(oldList, deadPack);
-			if (j < 0)
-				break;
-
-			final PackFile[] newList = new PackFile[oldList.length - 1];
-			System.arraycopy(oldList, 0, newList, 0, j);
-			System.arraycopy(oldList, j + 1, newList, j, newList.length - j);
-			n = new PackList(o.snapshot, newList);
-		} while (!packList.compareAndSet(o, n));
-		deadPack.close();
-	}
-
-	private static int indexOf(PackFile[] list, PackFile pack) {
-		for (int i = 0; i < list.length; i++) {
-			if (list[i] == pack)
-				return i;
-		}
-		return -1;
-	}
-
-	private PackList scanPacks(PackList original) {
-		synchronized (packList) {
-			PackList o, n;
-			do {
-				o = packList.get();
-				if (o != original) {
-					// Another thread did the scan for us, while we
-					// were blocked on the monitor above.
-					//
-					return o;
-				}
-				n = scanPacksImpl(o);
-				if (n == o)
-					return n;
-			} while (!packList.compareAndSet(o, n));
-			return n;
-		}
-	}
-
-	private PackList scanPacksImpl(PackList old) {
-		final Map<String, PackFile> forReuse = reuseMap(old);
-		final FileSnapshot snapshot = FileSnapshot.save(packDirectory);
-		final Set<String> names = listPackDirectory();
-		final List<PackFile> list = new ArrayList<>(names.size() >> 2);
-		boolean foundNew = false;
-		for (String indexName : names) {
-			// Must match "pack-[0-9a-f]{40}.idx" to be an index.
-			//
-			if (indexName.length() != 49 || !indexName.endsWith(".idx")) //$NON-NLS-1$
-				continue;
-
-			final String base = indexName.substring(0, indexName.length() - 3);
-			int extensions = 0;
-			for (PackExt ext : PackExt.values()) {
-				if (names.contains(base + ext.getExtension()))
-					extensions |= ext.getBit();
-			}
-
-			if ((extensions & PACK.getBit()) == 0) {
-				// Sometimes C Git's HTTP fetch transport leaves a
-				// .idx file behind and does not download the .pack.
-				// We have to skip over such useless indexes.
-				//
-				continue;
-			}
-
-			final String packName = base + PACK.getExtension();
-			final File packFile = new File(packDirectory, packName);
-			final PackFile oldPack = forReuse.get(packName);
-			if (oldPack != null
-					&& !oldPack.getFileSnapshot().isModified(packFile)) {
-				forReuse.remove(packName);
-				list.add(oldPack);
-				continue;
-			}
-
-			list.add(new PackFile(packFile, extensions));
-			foundNew = true;
-		}
-
-		// If we did not discover any new files, the modification time was not
-		// changed, and we did not remove any files, then the set of files is
-		// the same as the set we were given. Instead of building a new object
-		// return the same collection.
-		//
-		if (!foundNew && forReuse.isEmpty() && snapshot.equals(old.snapshot)) {
-			old.snapshot.setClean(snapshot);
-			return old;
-		}
-
-		for (PackFile p : forReuse.values()) {
-			p.close();
-		}
-
-		if (list.isEmpty())
-			return new PackList(snapshot, NO_PACKS.packs);
-
-		final PackFile[] r = list.toArray(new PackFile[0]);
-		Arrays.sort(r, PackFile.SORT);
-		return new PackList(snapshot, r);
-	}
-
-	private static Map<String, PackFile> reuseMap(PackList old) {
-		final Map<String, PackFile> forReuse = new HashMap<>();
-		for (PackFile p : old.packs) {
-			if (p.invalid()) {
-				// The pack instance is corrupted, and cannot be safely used
-				// again. Do not include it in our reuse map.
-				//
-				p.close();
-				continue;
-			}
-
-			final PackFile prior = forReuse.put(p.getPackFile().getName(), p);
-			if (prior != null) {
-				// This should never occur. It should be impossible for us
-				// to have two pack files with the same name, as all of them
-				// came out of the same directory. If it does, we promised to
-				// close any PackFiles we did not reuse, so close the second,
-				// readers are likely to be actively using the first.
-				//
-				forReuse.put(prior.getPackFile().getName(), prior);
-				p.close();
-			}
-		}
-		return forReuse;
-	}
-
-	private Set<String> listPackDirectory() {
-		final String[] nameList = packDirectory.list();
-		if (nameList == null)
-			return Collections.emptySet();
-		final Set<String> nameSet = new HashSet<>(nameList.length << 1);
-		for (String name : nameList) {
-			if (name.startsWith("pack-")) //$NON-NLS-1$
-				nameSet.add(name);
-		}
-		return nameSet;
-	}
-
 	void closeAllPackHandles(File packFile) {
 		// if the packfile already exists (because we are rewriting a
 		// packfile for the same set of objects maybe with different
 		// PackConfig) then make sure we get rid of all handles on the file.
 		// Windows will not allow for rename otherwise.
 		if (packFile.exists()) {
-			for (PackFile p : getPacks()) {
+			for (Pack p : packed.getPacks()) {
 				if (packFile.getPath().equals(p.getPackFile().getPath())) {
 					p.close();
 					break;
@@ -1077,29 +649,11 @@
 	}
 
 	/**
-	 * {@inheritDoc}
-	 * <p>
 	 * Compute the location of a loose object file.
 	 */
 	@Override
 	public File fileFor(AnyObjectId objectId) {
-		String n = objectId.name();
-		String d = n.substring(0, 2);
-		String f = n.substring(2);
-		return new File(new File(getDirectory(), d), f);
-	}
-
-	static final class PackList {
-		/** State just before reading the pack directory. */
-		final FileSnapshot snapshot;
-
-		/** All known packs, sorted by {@link PackFile#SORT}. */
-		final PackFile[] packs;
-
-		PackList(FileSnapshot monitor, PackFile[] packs) {
-			this.snapshot = monitor;
-			this.packs = packs;
-		}
+		return loose.fileFor(objectId);
 	}
 
 	static class AlternateHandle {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
index e275186..dba8ccd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
@@ -27,6 +27,7 @@
 
 import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig;
@@ -88,7 +89,7 @@
 	private Deflater def;
 
 	/** The pack that was created, if parsing was successful. */
-	private PackFile newPack;
+	private Pack newPack;
 
 	private PackConfig pconfig;
 
@@ -129,14 +130,14 @@
 	}
 
 	/**
-	 * Get the imported {@link org.eclipse.jgit.internal.storage.file.PackFile}.
+	 * Get the imported {@link org.eclipse.jgit.internal.storage.file.Pack}.
 	 * <p>
 	 * This method is supplied only to support testing; applications shouldn't
 	 * be using it directly to access the imported data.
 	 *
 	 * @return the imported PackFile, if parsing was successful.
 	 */
-	public PackFile getPackFile() {
+	public Pack getPack() {
 		return newPack;
 	}
 
@@ -426,10 +427,10 @@
 			d.update(oeBytes);
 		}
 
-		final String name = ObjectId.fromRaw(d.digest()).name();
-		final File packDir = new File(db.getDirectory(), "pack"); //$NON-NLS-1$
-		final File finalPack = new File(packDir, "pack-" + name + ".pack"); //$NON-NLS-1$ //$NON-NLS-2$
-		final File finalIdx = new File(packDir, "pack-" + name + ".idx"); //$NON-NLS-1$ //$NON-NLS-2$
+		ObjectId id = ObjectId.fromRaw(d.digest());
+		File packDir = new File(db.getDirectory(), "pack"); //$NON-NLS-1$
+		PackFile finalPack = new PackFile(packDir, id, PackExt.PACK);
+		PackFile finalIdx = finalPack.create(PackExt.INDEX);
 		final PackLock keep = new PackLock(finalPack, db.getFS());
 
 		if (!packDir.exists() && !packDir.mkdir() && !packDir.exists()) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java
new file mode 100644
index 0000000..5efd4c5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java
@@ -0,0 +1,1189 @@
+/*
+ * Copyright (C) 2008-2009, Google Inc.
+ * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2006-2008, 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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.RandomAccessFile;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel.MapMode;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.NoSuchFileException;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.CRC32;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.NoPackSignatureException;
+import org.eclipse.jgit.errors.PackInvalidException;
+import org.eclipse.jgit.errors.PackMismatchException;
+import org.eclipse.jgit.errors.StoredObjectRepresentationNotAvailableException;
+import org.eclipse.jgit.errors.UnpackException;
+import org.eclipse.jgit.errors.UnsupportedPackIndexVersionException;
+import org.eclipse.jgit.errors.UnsupportedPackVersionException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.BinaryDelta;
+import org.eclipse.jgit.internal.storage.pack.ObjectToPack;
+import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.util.LongList;
+import org.eclipse.jgit.util.NB;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Git version 2 pack file representation. A pack file contains Git objects in
+ * delta packed format yielding high compression of lots of object where some
+ * objects are similar.
+ */
+public class Pack implements Iterable<PackIndex.MutableEntry> {
+	private static final Logger LOG = LoggerFactory.getLogger(Pack.class);
+
+	/**
+	 * Sorts PackFiles to be most recently created to least recently created.
+	 */
+	public static final Comparator<Pack> SORT = (a, b) -> b.packLastModified
+			.compareTo(a.packLastModified);
+
+	private final PackFile packFile;
+
+	private PackFile keepFile;
+
+	final int hash;
+
+	private RandomAccessFile fd;
+
+	/** Serializes reads performed against {@link #fd}. */
+	private final Object readLock = new Object();
+
+	long length;
+
+	private int activeWindows;
+
+	private int activeCopyRawData;
+
+	Instant packLastModified;
+
+	private PackFileSnapshot fileSnapshot;
+
+	private volatile boolean invalid;
+
+	private volatile Exception invalidatingCause;
+
+	@Nullable
+	private PackFile bitmapIdxFile;
+
+	private AtomicInteger transientErrorCount = new AtomicInteger();
+
+	private byte[] packChecksum;
+
+	private volatile PackIndex loadedIdx;
+
+	private PackReverseIndex reverseIdx;
+
+	private PackBitmapIndex bitmapIdx;
+
+	/**
+	 * Objects we have tried to read, and discovered to be corrupt.
+	 * <p>
+	 * The list is allocated after the first corruption is found, and filled in
+	 * as more entries are discovered. Typically this list is never used, as
+	 * pack files do not usually contain corrupt objects.
+	 */
+	private volatile LongList corruptObjects;
+
+	/**
+	 * Construct a reader for an existing, pre-indexed packfile.
+	 *
+	 * @param packFile
+	 *            path of the <code>.pack</code> file holding the data.
+	 * @param bitmapIdxFile
+	 *            existing bitmap index file with the same base as the pack
+	 */
+	public Pack(File packFile, @Nullable PackFile bitmapIdxFile) {
+		this.packFile = new PackFile(packFile);
+		this.fileSnapshot = PackFileSnapshot.save(packFile);
+		this.packLastModified = fileSnapshot.lastModifiedInstant();
+		this.bitmapIdxFile = bitmapIdxFile;
+
+		// Multiply by 31 here so we can more directly combine with another
+		// value in WindowCache.hash(), without doing the multiply there.
+		//
+		hash = System.identityHashCode(this) * 31;
+		length = Long.MAX_VALUE;
+	}
+
+	private PackIndex idx() throws IOException {
+		PackIndex idx = loadedIdx;
+		if (idx == null) {
+			synchronized (this) {
+				idx = loadedIdx;
+				if (idx == null) {
+					if (invalid) {
+						throw new PackInvalidException(packFile,
+								invalidatingCause);
+					}
+					try {
+						long start = System.currentTimeMillis();
+						PackFile idxFile = packFile.create(INDEX);
+						idx = PackIndex.open(idxFile);
+						if (LOG.isDebugEnabled()) {
+							LOG.debug(String.format(
+									"Opening pack index %s, size %.3f MB took %d ms", //$NON-NLS-1$
+									idxFile.getAbsolutePath(),
+									Float.valueOf(idxFile.length()
+											/ (1024f * 1024)),
+									Long.valueOf(System.currentTimeMillis()
+											- start)));
+						}
+
+						if (packChecksum == null) {
+							packChecksum = idx.packChecksum;
+							fileSnapshot.setChecksum(
+									ObjectId.fromRaw(packChecksum));
+						} else if (!Arrays.equals(packChecksum,
+								idx.packChecksum)) {
+							throw new PackMismatchException(MessageFormat
+									.format(JGitText.get().packChecksumMismatch,
+											packFile.getPath(),
+											ObjectId.fromRaw(packChecksum)
+													.name(),
+											ObjectId.fromRaw(idx.packChecksum)
+													.name()));
+						}
+						loadedIdx = idx;
+					} catch (InterruptedIOException e) {
+						// don't invalidate the pack, we are interrupted from
+						// another thread
+						throw e;
+					} catch (IOException e) {
+						invalid = true;
+						invalidatingCause = e;
+						throw e;
+					}
+				}
+			}
+		}
+		return idx;
+	}
+	/**
+	 * Get the File object which locates this pack on disk.
+	 *
+	 * @return the File object which locates this pack on disk.
+	 */
+	public PackFile getPackFile() {
+		return packFile;
+	}
+
+	/**
+	 * Get the index for this pack file.
+	 *
+	 * @return the index for this pack file.
+	 * @throws java.io.IOException
+	 */
+	public PackIndex getIndex() throws IOException {
+		return idx();
+	}
+
+	/**
+	 * Get name extracted from {@code pack-*.pack} pattern.
+	 *
+	 * @return name extracted from {@code pack-*.pack} pattern.
+	 */
+	public String getPackName() {
+		return packFile.getId();
+	}
+
+	/**
+	 * Determine if an object is contained within the pack file.
+	 * <p>
+	 * For performance reasons only the index file is searched; the main pack
+	 * content is ignored entirely.
+	 * </p>
+	 *
+	 * @param id
+	 *            the object to look for. Must not be null.
+	 * @return true if the object is in this pack; false otherwise.
+	 * @throws java.io.IOException
+	 *             the index file cannot be loaded into memory.
+	 */
+	public boolean hasObject(AnyObjectId id) throws IOException {
+		final long offset = idx().findOffset(id);
+		return 0 < offset && !isCorrupt(offset);
+	}
+
+	/**
+	 * Determines whether a .keep file exists for this pack file.
+	 *
+	 * @return true if a .keep file exist.
+	 */
+	public boolean shouldBeKept() {
+		if (keepFile == null) {
+			keepFile = packFile.create(KEEP);
+		}
+		return keepFile.exists();
+	}
+
+	/**
+	 * Get an object from this pack.
+	 *
+	 * @param curs
+	 *            temporary working space associated with the calling thread.
+	 * @param id
+	 *            the object to obtain from the pack. Must not be null.
+	 * @return the object loader for the requested object if it is contained in
+	 *         this pack; null if the object was not found.
+	 * @throws IOException
+	 *             the pack file or the index could not be read.
+	 */
+	ObjectLoader get(WindowCursor curs, AnyObjectId id)
+			throws IOException {
+		final long offset = idx().findOffset(id);
+		return 0 < offset && !isCorrupt(offset) ? load(curs, offset) : null;
+	}
+
+	void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, int matchLimit)
+			throws IOException {
+		idx().resolve(matches, id, matchLimit);
+	}
+
+	/**
+	 * Close the resources utilized by this repository
+	 */
+	public void close() {
+		WindowCache.purge(this);
+		synchronized (this) {
+			loadedIdx = null;
+			reverseIdx = null;
+		}
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * <p>
+	 * Provide iterator over entries in associated pack index, that should also
+	 * exist in this pack file. Objects returned by such iterator are mutable
+	 * during iteration.
+	 * <p>
+	 * Iterator returns objects in SHA-1 lexicographical order.
+	 * </p>
+	 *
+	 * @see PackIndex#iterator()
+	 */
+	@Override
+	public Iterator<PackIndex.MutableEntry> iterator() {
+		try {
+			return idx().iterator();
+		} catch (IOException e) {
+			return Collections.<PackIndex.MutableEntry> emptyList().iterator();
+		}
+	}
+
+	/**
+	 * Obtain the total number of objects available in this pack. This method
+	 * relies on pack index, giving number of effectively available objects.
+	 *
+	 * @return number of objects in index of this pack, likewise in this pack
+	 * @throws IOException
+	 *             the index file cannot be loaded into memory.
+	 */
+	long getObjectCount() throws IOException {
+		return idx().getObjectCount();
+	}
+
+	/**
+	 * Search for object id with the specified start offset in associated pack
+	 * (reverse) index.
+	 *
+	 * @param offset
+	 *            start offset of object to find
+	 * @return object id for this offset, or null if no object was found
+	 * @throws IOException
+	 *             the index file cannot be loaded into memory.
+	 */
+	ObjectId findObjectForOffset(long offset) throws IOException {
+		return getReverseIdx().findObject(offset);
+	}
+
+	/**
+	 * Return the @{@link FileSnapshot} associated to the underlying packfile
+	 * that has been used when the object was created.
+	 *
+	 * @return the packfile @{@link FileSnapshot} that the object is loaded from.
+	 */
+	PackFileSnapshot getFileSnapshot() {
+		return fileSnapshot;
+	}
+
+	AnyObjectId getPackChecksum() {
+		return ObjectId.fromRaw(packChecksum);
+	}
+
+	private final byte[] decompress(final long position, final int sz,
+			final WindowCursor curs) throws IOException, DataFormatException {
+		byte[] dstbuf;
+		try {
+			dstbuf = new byte[sz];
+		} catch (OutOfMemoryError noMemory) {
+			// The size may be larger than our heap allows, return null to
+			// let the caller know allocation isn't possible and it should
+			// use the large object streaming approach instead.
+			//
+			// For example, this can occur when sz is 640 MB, and JRE
+			// maximum heap size is only 256 MB. Even if the JRE has
+			// 200 MB free, it cannot allocate a 640 MB byte array.
+			return null;
+		}
+
+		if (curs.inflate(this, position, dstbuf, false) != sz)
+			throw new EOFException(MessageFormat.format(
+					JGitText.get().shortCompressedStreamAt,
+					Long.valueOf(position)));
+		return dstbuf;
+	}
+
+	void copyPackAsIs(PackOutputStream out, WindowCursor curs)
+			throws IOException {
+		// Pin the first window, this ensures the length is accurate.
+		curs.pin(this, 0);
+		curs.copyPackAsIs(this, length, out);
+	}
+
+	final void copyAsIs(PackOutputStream out, LocalObjectToPack src,
+			boolean validate, WindowCursor curs) throws IOException,
+			StoredObjectRepresentationNotAvailableException {
+		beginCopyAsIs(src);
+		try {
+			copyAsIs2(out, src, validate, curs);
+		} finally {
+			endCopyAsIs();
+		}
+	}
+
+	private void copyAsIs2(PackOutputStream out, LocalObjectToPack src,
+			boolean validate, WindowCursor curs) throws IOException,
+			StoredObjectRepresentationNotAvailableException {
+		final CRC32 crc1 = validate ? new CRC32() : null;
+		final CRC32 crc2 = validate ? new CRC32() : null;
+		final byte[] buf = out.getCopyBuffer();
+
+		// Rip apart the header so we can discover the size.
+		//
+		readFully(src.offset, buf, 0, 20, curs);
+		int c = buf[0] & 0xff;
+		final int typeCode = (c >> 4) & 7;
+		long inflatedLength = c & 15;
+		int shift = 4;
+		int headerCnt = 1;
+		while ((c & 0x80) != 0) {
+			c = buf[headerCnt++] & 0xff;
+			inflatedLength += ((long) (c & 0x7f)) << shift;
+			shift += 7;
+		}
+
+		if (typeCode == Constants.OBJ_OFS_DELTA) {
+			do {
+				c = buf[headerCnt++] & 0xff;
+			} while ((c & 128) != 0);
+			if (validate) {
+				assert(crc1 != null && crc2 != null);
+				crc1.update(buf, 0, headerCnt);
+				crc2.update(buf, 0, headerCnt);
+			}
+		} else if (typeCode == Constants.OBJ_REF_DELTA) {
+			if (validate) {
+				assert(crc1 != null && crc2 != null);
+				crc1.update(buf, 0, headerCnt);
+				crc2.update(buf, 0, headerCnt);
+			}
+
+			readFully(src.offset + headerCnt, buf, 0, 20, curs);
+			if (validate) {
+				assert(crc1 != null && crc2 != null);
+				crc1.update(buf, 0, 20);
+				crc2.update(buf, 0, 20);
+			}
+			headerCnt += 20;
+		} else if (validate) {
+			assert(crc1 != null && crc2 != null);
+			crc1.update(buf, 0, headerCnt);
+			crc2.update(buf, 0, headerCnt);
+		}
+
+		final long dataOffset = src.offset + headerCnt;
+		final long dataLength = src.length;
+		final long expectedCRC;
+		final ByteArrayWindow quickCopy;
+
+		// Verify the object isn't corrupt before sending. If it is,
+		// we report it missing instead.
+		//
+		try {
+			quickCopy = curs.quickCopy(this, dataOffset, dataLength);
+
+			if (validate && idx().hasCRC32Support()) {
+				assert(crc1 != null);
+				// Index has the CRC32 code cached, validate the object.
+				//
+				expectedCRC = idx().findCRC32(src);
+				if (quickCopy != null) {
+					quickCopy.crc32(crc1, dataOffset, (int) dataLength);
+				} else {
+					long pos = dataOffset;
+					long cnt = dataLength;
+					while (cnt > 0) {
+						final int n = (int) Math.min(cnt, buf.length);
+						readFully(pos, buf, 0, n, curs);
+						crc1.update(buf, 0, n);
+						pos += n;
+						cnt -= n;
+					}
+				}
+				if (crc1.getValue() != expectedCRC) {
+					setCorrupt(src.offset);
+					throw new CorruptObjectException(MessageFormat.format(
+							JGitText.get().objectAtHasBadZlibStream,
+							Long.valueOf(src.offset), getPackFile()));
+				}
+			} else if (validate) {
+				// We don't have a CRC32 code in the index, so compute it
+				// now while inflating the raw data to get zlib to tell us
+				// whether or not the data is safe.
+				//
+				Inflater inf = curs.inflater();
+				byte[] tmp = new byte[1024];
+				if (quickCopy != null) {
+					quickCopy.check(inf, tmp, dataOffset, (int) dataLength);
+				} else {
+					assert(crc1 != null);
+					long pos = dataOffset;
+					long cnt = dataLength;
+					while (cnt > 0) {
+						final int n = (int) Math.min(cnt, buf.length);
+						readFully(pos, buf, 0, n, curs);
+						crc1.update(buf, 0, n);
+						inf.setInput(buf, 0, n);
+						while (inf.inflate(tmp, 0, tmp.length) > 0)
+							continue;
+						pos += n;
+						cnt -= n;
+					}
+				}
+				if (!inf.finished() || inf.getBytesRead() != dataLength) {
+					setCorrupt(src.offset);
+					throw new EOFException(MessageFormat.format(
+							JGitText.get().shortCompressedStreamAt,
+							Long.valueOf(src.offset)));
+				}
+				assert(crc1 != null);
+				expectedCRC = crc1.getValue();
+			} else {
+				expectedCRC = -1;
+			}
+		} catch (DataFormatException dataFormat) {
+			setCorrupt(src.offset);
+
+			CorruptObjectException corruptObject = new CorruptObjectException(
+					MessageFormat.format(
+							JGitText.get().objectAtHasBadZlibStream,
+							Long.valueOf(src.offset), getPackFile()),
+					dataFormat);
+
+			throw new StoredObjectRepresentationNotAvailableException(src,
+					corruptObject);
+
+		} catch (IOException ioError) {
+			throw new StoredObjectRepresentationNotAvailableException(src,
+					ioError);
+		}
+
+		if (quickCopy != null) {
+			// The entire object fits into a single byte array window slice,
+			// and we have it pinned.  Write this out without copying.
+			//
+			out.writeHeader(src, inflatedLength);
+			quickCopy.write(out, dataOffset, (int) dataLength);
+
+		} else if (dataLength <= buf.length) {
+			// Tiny optimization: Lots of objects are very small deltas or
+			// deflated commits that are likely to fit in the copy buffer.
+			//
+			if (!validate) {
+				long pos = dataOffset;
+				long cnt = dataLength;
+				while (cnt > 0) {
+					final int n = (int) Math.min(cnt, buf.length);
+					readFully(pos, buf, 0, n, curs);
+					pos += n;
+					cnt -= n;
+				}
+			}
+			out.writeHeader(src, inflatedLength);
+			out.write(buf, 0, (int) dataLength);
+		} else {
+			// Now we are committed to sending the object. As we spool it out,
+			// check its CRC32 code to make sure there wasn't corruption between
+			// the verification we did above, and us actually outputting it.
+			//
+			out.writeHeader(src, inflatedLength);
+			long pos = dataOffset;
+			long cnt = dataLength;
+			while (cnt > 0) {
+				final int n = (int) Math.min(cnt, buf.length);
+				readFully(pos, buf, 0, n, curs);
+				if (validate) {
+					assert(crc2 != null);
+					crc2.update(buf, 0, n);
+				}
+				out.write(buf, 0, n);
+				pos += n;
+				cnt -= n;
+			}
+			if (validate) {
+				assert(crc2 != null);
+				if (crc2.getValue() != expectedCRC) {
+					throw new CorruptObjectException(MessageFormat.format(
+							JGitText.get().objectAtHasBadZlibStream,
+							Long.valueOf(src.offset), getPackFile()));
+				}
+			}
+		}
+	}
+
+	boolean invalid() {
+		return invalid;
+	}
+
+	void setInvalid() {
+		invalid = true;
+	}
+
+	int incrementTransientErrorCount() {
+		return transientErrorCount.incrementAndGet();
+	}
+
+	void resetTransientErrorCount() {
+		transientErrorCount.set(0);
+	}
+
+	private void readFully(final long position, final byte[] dstbuf,
+			int dstoff, final int cnt, final WindowCursor curs)
+			throws IOException {
+		if (curs.copy(this, position, dstbuf, dstoff, cnt) != cnt)
+			throw new EOFException();
+	}
+
+	private synchronized void beginCopyAsIs(ObjectToPack otp)
+			throws StoredObjectRepresentationNotAvailableException {
+		if (++activeCopyRawData == 1 && activeWindows == 0) {
+			try {
+				doOpen();
+			} catch (IOException thisPackNotValid) {
+				throw new StoredObjectRepresentationNotAvailableException(otp,
+						thisPackNotValid);
+			}
+		}
+	}
+
+	private synchronized void endCopyAsIs() {
+		if (--activeCopyRawData == 0 && activeWindows == 0)
+			doClose();
+	}
+
+	synchronized boolean beginWindowCache() throws IOException {
+		if (++activeWindows == 1) {
+			if (activeCopyRawData == 0)
+				doOpen();
+			return true;
+		}
+		return false;
+	}
+
+	synchronized boolean endWindowCache() {
+		final boolean r = --activeWindows == 0;
+		if (r && activeCopyRawData == 0)
+			doClose();
+		return r;
+	}
+
+	private void doOpen() throws IOException {
+		if (invalid) {
+			openFail(true, invalidatingCause);
+			throw new PackInvalidException(packFile, invalidatingCause);
+		}
+		try {
+			synchronized (readLock) {
+				fd = new RandomAccessFile(packFile, "r"); //$NON-NLS-1$
+				length = fd.length();
+				onOpenPack();
+			}
+		} catch (InterruptedIOException e) {
+			// don't invalidate the pack, we are interrupted from another thread
+			openFail(false, e);
+			throw e;
+		} catch (FileNotFoundException fn) {
+			// don't invalidate the pack if opening an existing file failed
+			// since it may be related to a temporary lack of resources (e.g.
+			// max open files)
+			openFail(!packFile.exists(), fn);
+			throw fn;
+		} catch (EOFException | AccessDeniedException | NoSuchFileException
+				| CorruptObjectException | NoPackSignatureException
+				| PackMismatchException | UnpackException
+				| UnsupportedPackIndexVersionException
+				| UnsupportedPackVersionException pe) {
+			// exceptions signaling permanent problems with a pack
+			openFail(true, pe);
+			throw pe;
+		} catch (IOException | RuntimeException ge) {
+			// generic exceptions could be transient so we should not mark the
+			// pack invalid to avoid false MissingObjectExceptions
+			openFail(false, ge);
+			throw ge;
+		}
+	}
+
+	private void openFail(boolean invalidate, Exception cause) {
+		activeWindows = 0;
+		activeCopyRawData = 0;
+		invalid = invalidate;
+		invalidatingCause = cause;
+		doClose();
+	}
+
+	private void doClose() {
+		synchronized (readLock) {
+			if (fd != null) {
+				try {
+					fd.close();
+				} catch (IOException err) {
+					// Ignore a close event. We had it open only for reading.
+					// There should not be errors related to network buffers
+					// not flushed, etc.
+				}
+				fd = null;
+			}
+		}
+	}
+
+	ByteArrayWindow read(long pos, int size) throws IOException {
+		synchronized (readLock) {
+			if (invalid || fd == null) {
+				// Due to concurrency between a read and another packfile invalidation thread
+				// one thread could come up to this point and then fail with NPE.
+				// Detect the situation and throw a proper exception so that can be properly
+				// managed by the main packfile search loop and the Git client won't receive
+				// any failures.
+				throw new PackInvalidException(packFile, invalidatingCause);
+			}
+			if (length < pos + size)
+				size = (int) (length - pos);
+			final byte[] buf = new byte[size];
+			fd.seek(pos);
+			fd.readFully(buf, 0, size);
+			return new ByteArrayWindow(this, pos, buf);
+		}
+	}
+
+	ByteWindow mmap(long pos, int size) throws IOException {
+		synchronized (readLock) {
+			if (length < pos + size)
+				size = (int) (length - pos);
+
+			MappedByteBuffer map;
+			try {
+				map = fd.getChannel().map(MapMode.READ_ONLY, pos, size);
+			} catch (IOException ioe1) {
+				// The most likely reason this failed is the JVM has run out
+				// of virtual memory. We need to discard quickly, and try to
+				// force the GC to finalize and release any existing mappings.
+				//
+				System.gc();
+				System.runFinalization();
+				map = fd.getChannel().map(MapMode.READ_ONLY, pos, size);
+			}
+
+			if (map.hasArray())
+				return new ByteArrayWindow(this, pos, map.array());
+			return new ByteBufferWindow(this, pos, map);
+		}
+	}
+
+	private void onOpenPack() throws IOException {
+		final PackIndex idx = idx();
+		final byte[] buf = new byte[20];
+
+		fd.seek(0);
+		fd.readFully(buf, 0, 12);
+		if (RawParseUtils.match(buf, 0, Constants.PACK_SIGNATURE) != 4) {
+			throw new NoPackSignatureException(JGitText.get().notAPACKFile);
+		}
+		final long vers = NB.decodeUInt32(buf, 4);
+		final long packCnt = NB.decodeUInt32(buf, 8);
+		if (vers != 2 && vers != 3) {
+			throw new UnsupportedPackVersionException(vers);
+		}
+
+		if (packCnt != idx.getObjectCount()) {
+			throw new PackMismatchException(MessageFormat.format(
+					JGitText.get().packObjectCountMismatch,
+					Long.valueOf(packCnt), Long.valueOf(idx.getObjectCount()),
+					getPackFile()));
+		}
+
+		fd.seek(length - 20);
+		fd.readFully(buf, 0, 20);
+		if (!Arrays.equals(buf, packChecksum)) {
+			throw new PackMismatchException(MessageFormat.format(
+					JGitText.get().packChecksumMismatch,
+					getPackFile(),
+					ObjectId.fromRaw(buf).name(),
+					ObjectId.fromRaw(idx.packChecksum).name()));
+		}
+	}
+
+	ObjectLoader load(WindowCursor curs, long pos)
+			throws IOException, LargeObjectException {
+		try {
+			final byte[] ib = curs.tempId;
+			Delta delta = null;
+			byte[] data = null;
+			int type = Constants.OBJ_BAD;
+			boolean cached = false;
+
+			SEARCH: for (;;) {
+				readFully(pos, ib, 0, 20, curs);
+				int c = ib[0] & 0xff;
+				final int typeCode = (c >> 4) & 7;
+				long sz = c & 15;
+				int shift = 4;
+				int p = 1;
+				while ((c & 0x80) != 0) {
+					c = ib[p++] & 0xff;
+					sz += ((long) (c & 0x7f)) << shift;
+					shift += 7;
+				}
+
+				switch (typeCode) {
+				case Constants.OBJ_COMMIT:
+				case Constants.OBJ_TREE:
+				case Constants.OBJ_BLOB:
+				case Constants.OBJ_TAG: {
+					if (delta != null || sz < curs.getStreamFileThreshold()) {
+						data = decompress(pos + p, (int) sz, curs);
+					}
+
+					if (delta != null) {
+						type = typeCode;
+						break SEARCH;
+					}
+
+					if (data != null) {
+						return new ObjectLoader.SmallObject(typeCode, data);
+					}
+					return new LargePackedWholeObject(typeCode, sz, pos, p,
+							this, curs.db);
+				}
+
+				case Constants.OBJ_OFS_DELTA: {
+					c = ib[p++] & 0xff;
+					long base = c & 127;
+					while ((c & 128) != 0) {
+						base += 1;
+						c = ib[p++] & 0xff;
+						base <<= 7;
+						base += (c & 127);
+					}
+					base = pos - base;
+					delta = new Delta(delta, pos, (int) sz, p, base);
+					if (sz != delta.deltaSize)
+						break SEARCH;
+
+					DeltaBaseCache.Entry e = curs.getDeltaBaseCache().get(this, base);
+					if (e != null) {
+						type = e.type;
+						data = e.data;
+						cached = true;
+						break SEARCH;
+					}
+					pos = base;
+					continue SEARCH;
+				}
+
+				case Constants.OBJ_REF_DELTA: {
+					readFully(pos + p, ib, 0, 20, curs);
+					long base = findDeltaBase(ObjectId.fromRaw(ib));
+					delta = new Delta(delta, pos, (int) sz, p + 20, base);
+					if (sz != delta.deltaSize)
+						break SEARCH;
+
+					DeltaBaseCache.Entry e = curs.getDeltaBaseCache().get(this, base);
+					if (e != null) {
+						type = e.type;
+						data = e.data;
+						cached = true;
+						break SEARCH;
+					}
+					pos = base;
+					continue SEARCH;
+				}
+
+				default:
+					throw new IOException(MessageFormat.format(
+							JGitText.get().unknownObjectType,
+							Integer.valueOf(typeCode)));
+				}
+			}
+
+			// At this point there is at least one delta to apply to data.
+			// (Whole objects with no deltas to apply return early above.)
+
+			if (data == null)
+				throw new IOException(JGitText.get().inMemoryBufferLimitExceeded);
+
+			assert(delta != null);
+			do {
+				// Cache only the base immediately before desired object.
+				if (cached)
+					cached = false;
+				else if (delta.next == null)
+					curs.getDeltaBaseCache().store(this, delta.basePos, data, type);
+
+				pos = delta.deltaPos;
+
+				final byte[] cmds = decompress(pos + delta.hdrLen,
+						delta.deltaSize, curs);
+				if (cmds == null) {
+					data = null; // Discard base in case of OutOfMemoryError
+					throw new LargeObjectException.OutOfMemory(new OutOfMemoryError());
+				}
+
+				final long sz = BinaryDelta.getResultSize(cmds);
+				if (Integer.MAX_VALUE <= sz)
+					throw new LargeObjectException.ExceedsByteArrayLimit();
+
+				final byte[] result;
+				try {
+					result = new byte[(int) sz];
+				} catch (OutOfMemoryError tooBig) {
+					data = null; // Discard base in case of OutOfMemoryError
+					throw new LargeObjectException.OutOfMemory(tooBig);
+				}
+
+				BinaryDelta.apply(data, cmds, result);
+				data = result;
+				delta = delta.next;
+			} while (delta != null);
+
+			return new ObjectLoader.SmallObject(type, data);
+
+		} catch (DataFormatException dfe) {
+			throw new CorruptObjectException(
+					MessageFormat.format(
+							JGitText.get().objectAtHasBadZlibStream,
+							Long.valueOf(pos), getPackFile()),
+					dfe);
+		}
+	}
+
+	private long findDeltaBase(ObjectId baseId) throws IOException,
+			MissingObjectException {
+		long ofs = idx().findOffset(baseId);
+		if (ofs < 0)
+			throw new MissingObjectException(baseId,
+					JGitText.get().missingDeltaBase);
+		return ofs;
+	}
+
+	private static class Delta {
+		/** Child that applies onto this object. */
+		final Delta next;
+
+		/** Offset of the delta object. */
+		final long deltaPos;
+
+		/** Size of the inflated delta stream. */
+		final int deltaSize;
+
+		/** Total size of the delta's pack entry header (including base). */
+		final int hdrLen;
+
+		/** Offset of the base object this delta applies onto. */
+		final long basePos;
+
+		Delta(Delta next, long ofs, int sz, int hdrLen, long baseOffset) {
+			this.next = next;
+			this.deltaPos = ofs;
+			this.deltaSize = sz;
+			this.hdrLen = hdrLen;
+			this.basePos = baseOffset;
+		}
+	}
+
+	byte[] getDeltaHeader(WindowCursor wc, long pos)
+			throws IOException, DataFormatException {
+		// The delta stream starts as two variable length integers. If we
+		// assume they are 64 bits each, we need 16 bytes to encode them,
+		// plus 2 extra bytes for the variable length overhead. So 18 is
+		// the longest delta instruction header.
+		//
+		final byte[] hdr = new byte[18];
+		wc.inflate(this, pos, hdr, true /* headerOnly */);
+		return hdr;
+	}
+
+	int getObjectType(WindowCursor curs, long pos) throws IOException {
+		final byte[] ib = curs.tempId;
+		for (;;) {
+			readFully(pos, ib, 0, 20, curs);
+			int c = ib[0] & 0xff;
+			final int type = (c >> 4) & 7;
+
+			switch (type) {
+			case Constants.OBJ_COMMIT:
+			case Constants.OBJ_TREE:
+			case Constants.OBJ_BLOB:
+			case Constants.OBJ_TAG:
+				return type;
+
+			case Constants.OBJ_OFS_DELTA: {
+				int p = 1;
+				while ((c & 0x80) != 0)
+					c = ib[p++] & 0xff;
+				c = ib[p++] & 0xff;
+				long ofs = c & 127;
+				while ((c & 128) != 0) {
+					ofs += 1;
+					c = ib[p++] & 0xff;
+					ofs <<= 7;
+					ofs += (c & 127);
+				}
+				pos = pos - ofs;
+				continue;
+			}
+
+			case Constants.OBJ_REF_DELTA: {
+				int p = 1;
+				while ((c & 0x80) != 0)
+					c = ib[p++] & 0xff;
+				readFully(pos + p, ib, 0, 20, curs);
+				pos = findDeltaBase(ObjectId.fromRaw(ib));
+				continue;
+			}
+
+			default:
+				throw new IOException(
+						MessageFormat.format(JGitText.get().unknownObjectType,
+								Integer.valueOf(type)));
+			}
+		}
+	}
+
+	long getObjectSize(WindowCursor curs, AnyObjectId id)
+			throws IOException {
+		final long offset = idx().findOffset(id);
+		return 0 < offset ? getObjectSize(curs, offset) : -1;
+	}
+
+	long getObjectSize(WindowCursor curs, long pos)
+			throws IOException {
+		final byte[] ib = curs.tempId;
+		readFully(pos, ib, 0, 20, curs);
+		int c = ib[0] & 0xff;
+		final int type = (c >> 4) & 7;
+		long sz = c & 15;
+		int shift = 4;
+		int p = 1;
+		while ((c & 0x80) != 0) {
+			c = ib[p++] & 0xff;
+			sz += ((long) (c & 0x7f)) << shift;
+			shift += 7;
+		}
+
+		long deltaAt;
+		switch (type) {
+		case Constants.OBJ_COMMIT:
+		case Constants.OBJ_TREE:
+		case Constants.OBJ_BLOB:
+		case Constants.OBJ_TAG:
+			return sz;
+
+		case Constants.OBJ_OFS_DELTA:
+			c = ib[p++] & 0xff;
+			while ((c & 128) != 0)
+				c = ib[p++] & 0xff;
+			deltaAt = pos + p;
+			break;
+
+		case Constants.OBJ_REF_DELTA:
+			deltaAt = pos + p + 20;
+			break;
+
+		default:
+			throw new IOException(MessageFormat.format(
+					JGitText.get().unknownObjectType, Integer.valueOf(type)));
+		}
+
+		try {
+			return BinaryDelta.getResultSize(getDeltaHeader(curs, deltaAt));
+		} catch (DataFormatException e) {
+			throw new CorruptObjectException(MessageFormat.format(
+					JGitText.get().objectAtHasBadZlibStream, Long.valueOf(pos),
+					getPackFile()), e);
+		}
+	}
+
+	LocalObjectRepresentation representation(final WindowCursor curs,
+			final AnyObjectId objectId) throws IOException {
+		final long pos = idx().findOffset(objectId);
+		if (pos < 0)
+			return null;
+
+		final byte[] ib = curs.tempId;
+		readFully(pos, ib, 0, 20, curs);
+		int c = ib[0] & 0xff;
+		int p = 1;
+		final int typeCode = (c >> 4) & 7;
+		while ((c & 0x80) != 0)
+			c = ib[p++] & 0xff;
+
+		long len = (findEndOffset(pos) - pos);
+		switch (typeCode) {
+		case Constants.OBJ_COMMIT:
+		case Constants.OBJ_TREE:
+		case Constants.OBJ_BLOB:
+		case Constants.OBJ_TAG:
+			return LocalObjectRepresentation.newWhole(this, pos, len - p);
+
+		case Constants.OBJ_OFS_DELTA: {
+			c = ib[p++] & 0xff;
+			long ofs = c & 127;
+			while ((c & 128) != 0) {
+				ofs += 1;
+				c = ib[p++] & 0xff;
+				ofs <<= 7;
+				ofs += (c & 127);
+			}
+			ofs = pos - ofs;
+			return LocalObjectRepresentation.newDelta(this, pos, len - p, ofs);
+		}
+
+		case Constants.OBJ_REF_DELTA: {
+			len -= p;
+			len -= Constants.OBJECT_ID_LENGTH;
+			readFully(pos + p, ib, 0, 20, curs);
+			ObjectId id = ObjectId.fromRaw(ib);
+			return LocalObjectRepresentation.newDelta(this, pos, len, id);
+		}
+
+		default:
+			throw new IOException(
+					MessageFormat.format(JGitText.get().unknownObjectType,
+							Integer.valueOf(typeCode)));
+		}
+	}
+
+	private long findEndOffset(long startOffset)
+			throws IOException, CorruptObjectException {
+		final long maxOffset = length - 20;
+		return getReverseIdx().findNextOffset(startOffset, maxOffset);
+	}
+
+	synchronized PackBitmapIndex getBitmapIndex() throws IOException {
+		if (invalid || bitmapIdxFile == null) {
+			return null;
+		}
+		if (bitmapIdx == null) {
+			final PackBitmapIndex idx;
+			try {
+				idx = PackBitmapIndex.open(bitmapIdxFile, idx(),
+						getReverseIdx());
+			} catch (FileNotFoundException e) {
+				// Once upon a time this bitmap file existed. Now it
+				// has been removed. Most likely an external gc  has
+				// removed this packfile and the bitmap
+				bitmapIdxFile = null;
+				return null;
+			}
+
+			// At this point, idx() will have set packChecksum.
+			if (Arrays.equals(packChecksum, idx.packChecksum)) {
+				bitmapIdx = idx;
+			} else {
+				bitmapIdxFile = null;
+			}
+		}
+		return bitmapIdx;
+	}
+
+	private synchronized PackReverseIndex getReverseIdx() throws IOException {
+		if (reverseIdx == null)
+			reverseIdx = new PackReverseIndex(idx());
+		return reverseIdx;
+	}
+
+	private boolean isCorrupt(long offset) {
+		LongList list = corruptObjects;
+		if (list == null)
+			return false;
+		synchronized (list) {
+			return list.contains(offset);
+		}
+	}
+
+	private void setCorrupt(long offset) {
+		LongList list = corruptObjects;
+		if (list == null) {
+			synchronized (readLock) {
+				list = corruptObjects;
+				if (list == null) {
+					list = new LongList();
+					corruptObjects = list;
+				}
+			}
+		}
+		synchronized (list) {
+			list.add(offset);
+		}
+	}
+
+	@SuppressWarnings("nls")
+	@Override
+	public String toString() {
+		return "Pack [packFileName=" + packFile.getName() + ", length="
+				+ packFile.length() + ", packChecksum="
+				+ ObjectId.fromRaw(packChecksum).name() + "]";
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
new file mode 100644
index 0000000..73745d8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2009, Google Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.PackInvalidException;
+import org.eclipse.jgit.errors.PackMismatchException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.ObjectToPack;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.pack.PackWriter;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Traditional file system packed objects directory handler.
+ * <p>
+ * This is the {@link org.eclipse.jgit.internal.storage.file.Pack}s object
+ * representation for a Git object database, where objects are stored in
+ * compressed containers known as
+ * {@link org.eclipse.jgit.internal.storage.file.Pack}s.
+ */
+class PackDirectory {
+	private final static Logger LOG = LoggerFactory
+			.getLogger(PackDirectory.class);
+
+	private static final PackList NO_PACKS = new PackList(FileSnapshot.DIRTY,
+			new Pack[0]);
+
+	private final Config config;
+
+	private final File directory;
+
+	private final AtomicReference<PackList> packList;
+
+	/**
+	 * Initialize a reference to an on-disk 'pack' directory.
+	 *
+	 * @param config
+	 *            configuration this directory consults for write settings.
+	 * @param directory
+	 *            the location of the {@code pack} directory.
+	 */
+	PackDirectory(Config config, File directory) {
+		this.config = config;
+		this.directory = directory;
+		packList = new AtomicReference<>(NO_PACKS);
+	}
+
+	/**
+	 * Getter for the field {@code directory}.
+	 *
+	 * @return the location of the {@code pack} directory.
+	 */
+	File getDirectory() {
+		return directory;
+	}
+
+	void create() throws IOException {
+		FileUtils.mkdir(directory);
+	}
+
+	void close() {
+		PackList packs = packList.get();
+		if (packs != NO_PACKS && packList.compareAndSet(packs, NO_PACKS)) {
+			for (Pack p : packs.packs) {
+				p.close();
+			}
+		}
+	}
+
+	Collection<Pack> getPacks() {
+		PackList list = packList.get();
+		if (list == NO_PACKS) {
+			list = scanPacks(list);
+		}
+		Pack[] packs = list.packs;
+		return Collections.unmodifiableCollection(Arrays.asList(packs));
+	}
+
+	/** {@inheritDoc} */
+	@Override
+	public String toString() {
+		return "PackDirectory[" + getDirectory() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	/**
+	 * Does the requested object exist in this PackDirectory?
+	 *
+	 * @param objectId
+	 *            identity of the object to test for existence of.
+	 * @return {@code true} if the specified object is stored in this PackDirectory.
+	 */
+	boolean has(AnyObjectId objectId) {
+		return getPack(objectId) != null;
+	}
+
+	/**
+	 * Get the {@link org.eclipse.jgit.internal.storage.file.Pack} for the
+	 * specified object if it is stored in this PackDirectory.
+	 *
+	 * @param objectId
+	 *            identity of the object to find the Pack for.
+	 * @return {@link org.eclipse.jgit.internal.storage.file.Pack} which
+	 *         contains the specified object or {@code null} if it is not stored
+	 *         in this PackDirectory.
+	 */
+	@Nullable
+	Pack getPack(AnyObjectId objectId) {
+		PackList pList;
+		do {
+			pList = packList.get();
+			for (Pack p : pList.packs) {
+				try {
+					if (p.hasObject(objectId)) {
+						return p;
+					}
+				} catch (IOException e) {
+					// The hasObject call should have only touched the index, so
+					// any failure here indicates the index is unreadable by
+					// this process, and the pack is likewise not readable.
+					LOG.warn(MessageFormat.format(
+							JGitText.get().unableToReadPackfile,
+							p.getPackFile().getAbsolutePath()), e);
+					remove(p);
+				}
+			}
+		} while (searchPacksAgain(pList));
+		return null;
+	}
+
+	/**
+	 * Find objects matching the prefix abbreviation.
+	 *
+	 * @param matches
+	 *            set to add any located ObjectIds to. This is an output
+	 *            parameter.
+	 * @param id
+	 *            prefix to search for.
+	 * @param matchLimit
+	 *            maximum number of results to return. At most this many
+	 *            ObjectIds should be added to matches before returning.
+	 * @return {@code true} if the matches were exhausted before reaching
+	 *         {@code maxLimit}.
+	 */
+	boolean resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
+			int matchLimit) {
+		// Go through the packs once. If we didn't find any resolutions
+		// scan for new packs and check once more.
+		int oldSize = matches.size();
+		PackList pList;
+		do {
+			pList = packList.get();
+			for (Pack p : pList.packs) {
+				try {
+					p.resolve(matches, id, matchLimit);
+					p.resetTransientErrorCount();
+				} catch (IOException e) {
+					handlePackError(e, p);
+				}
+				if (matches.size() > matchLimit) {
+					return false;
+				}
+			}
+		} while (matches.size() == oldSize && searchPacksAgain(pList));
+		return true;
+	}
+
+	ObjectLoader open(WindowCursor curs, AnyObjectId objectId) {
+		PackList pList;
+		do {
+			SEARCH: for (;;) {
+				pList = packList.get();
+				for (Pack p : pList.packs) {
+					try {
+						ObjectLoader ldr = p.get(curs, objectId);
+						p.resetTransientErrorCount();
+						if (ldr != null)
+							return ldr;
+					} catch (PackMismatchException e) {
+						// Pack was modified; refresh the entire pack list.
+						if (searchPacksAgain(pList)) {
+							continue SEARCH;
+						}
+					} catch (IOException e) {
+						handlePackError(e, p);
+					}
+				}
+				break SEARCH;
+			}
+		} while (searchPacksAgain(pList));
+		return null;
+	}
+
+	long getSize(WindowCursor curs, AnyObjectId id) {
+		PackList pList;
+		do {
+			SEARCH: for (;;) {
+				pList = packList.get();
+				for (Pack p : pList.packs) {
+					try {
+						long len = p.getObjectSize(curs, id);
+						p.resetTransientErrorCount();
+						if (0 <= len) {
+							return len;
+						}
+					} catch (PackMismatchException e) {
+						// Pack was modified; refresh the entire pack list.
+						if (searchPacksAgain(pList)) {
+							continue SEARCH;
+						}
+					} catch (IOException e) {
+						handlePackError(e, p);
+					}
+				}
+				break SEARCH;
+			}
+		} while (searchPacksAgain(pList));
+		return -1;
+	}
+
+	void selectRepresentation(PackWriter packer, ObjectToPack otp,
+			WindowCursor curs) {
+		PackList pList = packList.get();
+		SEARCH: for (;;) {
+			for (Pack p : pList.packs) {
+				try {
+					LocalObjectRepresentation rep = p.representation(curs, otp);
+					p.resetTransientErrorCount();
+					if (rep != null) {
+						packer.select(otp, rep);
+					}
+				} catch (PackMismatchException e) {
+					// Pack was modified; refresh the entire pack list.
+					//
+					pList = scanPacks(pList);
+					continue SEARCH;
+				} catch (IOException e) {
+					handlePackError(e, p);
+				}
+			}
+			break SEARCH;
+		}
+	}
+
+	private void handlePackError(IOException e, Pack p) {
+		String warnTmpl = null;
+		int transientErrorCount = 0;
+		String errTmpl = JGitText.get().exceptionWhileReadingPack;
+		if ((e instanceof CorruptObjectException)
+				|| (e instanceof PackInvalidException)) {
+			warnTmpl = JGitText.get().corruptPack;
+			LOG.warn(MessageFormat.format(warnTmpl,
+					p.getPackFile().getAbsolutePath()), e);
+			// Assume the pack is corrupted, and remove it from the list.
+			remove(p);
+		} else if (e instanceof FileNotFoundException) {
+			if (p.getPackFile().exists()) {
+				errTmpl = JGitText.get().packInaccessible;
+				transientErrorCount = p.incrementTransientErrorCount();
+			} else {
+				warnTmpl = JGitText.get().packWasDeleted;
+				remove(p);
+			}
+		} else if (FileUtils.isStaleFileHandleInCausalChain(e)) {
+			warnTmpl = JGitText.get().packHandleIsStale;
+			remove(p);
+		} else {
+			transientErrorCount = p.incrementTransientErrorCount();
+		}
+		if (warnTmpl != null) {
+			LOG.warn(MessageFormat.format(warnTmpl,
+					p.getPackFile().getAbsolutePath()), e);
+		} else {
+			if (doLogExponentialBackoff(transientErrorCount)) {
+				// Don't remove the pack from the list, as the error may be
+				// transient.
+				LOG.error(MessageFormat.format(errTmpl,
+						p.getPackFile().getAbsolutePath(),
+						Integer.valueOf(transientErrorCount)), e);
+			}
+		}
+	}
+
+	/**
+	 * @param n
+	 *            count of consecutive failures
+	 * @return @{code true} if i is a power of 2
+	 */
+	private boolean doLogExponentialBackoff(int n) {
+		return (n & (n - 1)) == 0;
+	}
+
+	boolean searchPacksAgain(PackList old) {
+		// Whether to trust the pack folder's modification time. If set
+		// to false we will always scan the .git/objects/pack folder to
+		// check for new pack files. If set to true (default) we use the
+		// lastmodified attribute of the folder and assume that no new
+		// pack files can be in this folder if his modification time has
+		// not changed.
+		boolean trustFolderStat = config.getBoolean(
+				ConfigConstants.CONFIG_CORE_SECTION,
+				ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true);
+
+		return ((!trustFolderStat) || old.snapshot.isModified(directory))
+				&& old != scanPacks(old);
+	}
+
+	void insert(Pack pack) {
+		PackList o, n;
+		do {
+			o = packList.get();
+
+			// If the pack in question is already present in the list
+			// (picked up by a concurrent thread that did a scan?) we
+			// do not want to insert it a second time.
+			//
+			final Pack[] oldList = o.packs;
+			final String name = pack.getPackFile().getName();
+			for (Pack p : oldList) {
+				if (name.equals(p.getPackFile().getName())) {
+					return;
+				}
+			}
+
+			final Pack[] newList = new Pack[1 + oldList.length];
+			newList[0] = pack;
+			System.arraycopy(oldList, 0, newList, 1, oldList.length);
+			n = new PackList(o.snapshot, newList);
+		} while (!packList.compareAndSet(o, n));
+	}
+
+	private void remove(Pack deadPack) {
+		PackList o, n;
+		do {
+			o = packList.get();
+
+			final Pack[] oldList = o.packs;
+			final int j = indexOf(oldList, deadPack);
+			if (j < 0) {
+				break;
+			}
+
+			final Pack[] newList = new Pack[oldList.length - 1];
+			System.arraycopy(oldList, 0, newList, 0, j);
+			System.arraycopy(oldList, j + 1, newList, j, newList.length - j);
+			n = new PackList(o.snapshot, newList);
+		} while (!packList.compareAndSet(o, n));
+		deadPack.close();
+	}
+
+	private static int indexOf(Pack[] list, Pack pack) {
+		for (int i = 0; i < list.length; i++) {
+			if (list[i] == pack) {
+				return i;
+			}
+		}
+		return -1;
+	}
+
+	private PackList scanPacks(PackList original) {
+		synchronized (packList) {
+			PackList o, n;
+			do {
+				o = packList.get();
+				if (o != original) {
+					// Another thread did the scan for us, while we
+					// were blocked on the monitor above.
+					//
+					return o;
+				}
+				n = scanPacksImpl(o);
+				if (n == o) {
+					return n;
+				}
+			} while (!packList.compareAndSet(o, n));
+			return n;
+		}
+	}
+
+	private PackList scanPacksImpl(PackList old) {
+		final Map<String, Pack> forReuse = reuseMap(old);
+		final FileSnapshot snapshot = FileSnapshot.save(directory);
+		Map<String, Map<PackExt, PackFile>> packFilesByExtById = getPackFilesByExtById();
+		List<Pack> list = new ArrayList<>(packFilesByExtById.size());
+		boolean foundNew = false;
+		for (Map<PackExt, PackFile> packFilesByExt : packFilesByExtById
+				.values()) {
+			PackFile packFile = packFilesByExt.get(PACK);
+			if (packFile == null || !packFilesByExt.containsKey(INDEX)) {
+				// Sometimes C Git's HTTP fetch transport leaves a
+				// .idx file behind and does not download the .pack.
+				// We have to skip over such useless indexes.
+				// Also skip if we don't have any index for this id
+				continue;
+			}
+
+			Pack oldPack = forReuse.get(packFile.getName());
+			if (oldPack != null
+					&& !oldPack.getFileSnapshot().isModified(packFile)) {
+				forReuse.remove(packFile.getName());
+				list.add(oldPack);
+				continue;
+			}
+
+			list.add(new Pack(packFile, packFilesByExt.get(BITMAP_INDEX)));
+			foundNew = true;
+		}
+
+		// If we did not discover any new files, the modification time was not
+		// changed, and we did not remove any files, then the set of files is
+		// the same as the set we were given. Instead of building a new object
+		// return the same collection.
+		//
+		if (!foundNew && forReuse.isEmpty() && snapshot.equals(old.snapshot)) {
+			old.snapshot.setClean(snapshot);
+			return old;
+		}
+
+		for (Pack p : forReuse.values()) {
+			p.close();
+		}
+
+		if (list.isEmpty()) {
+			return new PackList(snapshot, NO_PACKS.packs);
+		}
+
+		final Pack[] r = list.toArray(new Pack[0]);
+		Arrays.sort(r, Pack.SORT);
+		return new PackList(snapshot, r);
+	}
+
+	private static Map<String, Pack> reuseMap(PackList old) {
+		final Map<String, Pack> forReuse = new HashMap<>();
+		for (Pack p : old.packs) {
+			if (p.invalid()) {
+				// The pack instance is corrupted, and cannot be safely used
+				// again. Do not include it in our reuse map.
+				//
+				p.close();
+				continue;
+			}
+
+			final Pack prior = forReuse.put(p.getPackFile().getName(), p);
+			if (prior != null) {
+				// This should never occur. It should be impossible for us
+				// to have two pack files with the same name, as all of them
+				// came out of the same directory. If it does, we promised to
+				// close any PackFiles we did not reuse, so close the second,
+				// readers are likely to be actively using the first.
+				//
+				forReuse.put(prior.getPackFile().getName(), prior);
+				p.close();
+			}
+		}
+		return forReuse;
+	}
+
+	/**
+	 * Scans the pack directory for
+	 * {@link org.eclipse.jgit.internal.storage.file.PackFile}s and returns them
+	 * organized by their extensions and their pack ids
+	 *
+	 * Skips files in the directory that we cannot create a
+	 * {@link org.eclipse.jgit.internal.storage.file.PackFile} for.
+	 *
+	 * @return a map of {@link org.eclipse.jgit.internal.storage.file.PackFile}s
+	 *         and {@link org.eclipse.jgit.internal.storage.pack.PackExt}s keyed
+	 *         by pack ids
+	 */
+	private Map<String, Map<PackExt, PackFile>> getPackFilesByExtById() {
+		final String[] nameList = directory.list();
+		if (nameList == null) {
+			return Collections.emptyMap();
+		}
+		Map<String, Map<PackExt, PackFile>> packFilesByExtById = new HashMap<>(
+				nameList.length / 2); // assume roughly 2 files per id
+		for (String name : nameList) {
+			try {
+				PackFile pack = new PackFile(directory, name);
+				if (pack.getPackExt() != null) {
+					Map<PackExt, PackFile> packByExt = packFilesByExtById
+							.get(pack.getId());
+					if (packByExt == null) {
+						packByExt = new EnumMap<>(PackExt.class);
+						packFilesByExtById.put(pack.getId(), packByExt);
+					}
+					packByExt.put(pack.getPackExt(), pack);
+				}
+			} catch (IllegalArgumentException e) {
+				continue;
+			}
+		}
+		return packFilesByExtById;
+	}
+
+	static final class PackList {
+		/** State just before reading the pack directory. */
+		final FileSnapshot snapshot;
+
+		/** All known packs, sorted by {@link Pack#SORT}. */
+		final Pack[] packs;
+
+		PackList(FileSnapshot monitor, Pack[] packs) {
+			this.snapshot = monitor;
+			this.packs = packs;
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
index e112fe7..19979d0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
@@ -1,7 +1,6 @@
 /*
- * Copyright (C) 2008-2009, Google Inc.
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (c) 2021 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -12,1197 +11,177 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
-import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
-import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
-import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP;
-
-import java.io.EOFException;
 import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InterruptedIOException;
-import java.io.RandomAccessFile;
-import java.nio.MappedByteBuffer;
-import java.nio.channels.FileChannel.MapMode;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.NoSuchFileException;
 import java.text.MessageFormat;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.zip.CRC32;
-import java.util.zip.DataFormatException;
-import java.util.zip.Inflater;
 
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.LargeObjectException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.NoPackSignatureException;
-import org.eclipse.jgit.errors.PackInvalidException;
-import org.eclipse.jgit.errors.PackMismatchException;
-import org.eclipse.jgit.errors.StoredObjectRepresentationNotAvailableException;
-import org.eclipse.jgit.errors.UnpackException;
-import org.eclipse.jgit.errors.UnsupportedPackIndexVersionException;
-import org.eclipse.jgit.errors.UnsupportedPackVersionException;
 import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.internal.storage.pack.BinaryDelta;
-import org.eclipse.jgit.internal.storage.pack.ObjectToPack;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
-import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.util.LongList;
-import org.eclipse.jgit.util.NB;
-import org.eclipse.jgit.util.RawParseUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
- * A Git version 2 pack file representation. A pack file contains Git objects in
- * delta packed format yielding high compression of lots of object where some
- * objects are similar.
+ * A pack file (or pack related) File.
+ *
+ * Example: "pack-0123456789012345678901234567890123456789.idx"
  */
-public class PackFile implements Iterable<PackIndex.MutableEntry> {
-	private static final Logger LOG = LoggerFactory.getLogger(PackFile.class);
+public class PackFile extends File {
+	private static final long serialVersionUID = 1L;
 
-	/**
-	 * Sorts PackFiles to be most recently created to least recently created.
-	 */
-	public static final Comparator<PackFile> SORT = (a, b) -> b.packLastModified
-			.compareTo(a.packLastModified);
+	private static final String PREFIX = "pack-"; //$NON-NLS-1$
 
-	private final File packFile;
+	private final String base; // PREFIX + id i.e.
+								// pack-0123456789012345678901234567890123456789
 
-	private final int extensions;
+	private final String id; // i.e. 0123456789012345678901234567890123456789
 
-	private File keepFile;
+	private final boolean hasOldPrefix;
 
-	private volatile String packName;
+	private final PackExt packExt;
 
-	final int hash;
-
-	private RandomAccessFile fd;
-
-	/** Serializes reads performed against {@link #fd}. */
-	private final Object readLock = new Object();
-
-	long length;
-
-	private int activeWindows;
-
-	private int activeCopyRawData;
-
-	Instant packLastModified;
-
-	private PackFileSnapshot fileSnapshot;
-
-	private volatile boolean invalid;
-
-	private volatile Exception invalidatingCause;
-
-	private boolean invalidBitmap;
-
-	private AtomicInteger transientErrorCount = new AtomicInteger();
-
-	private byte[] packChecksum;
-
-	private volatile PackIndex loadedIdx;
-
-	private PackReverseIndex reverseIdx;
-
-	private PackBitmapIndex bitmapIdx;
-
-	/**
-	 * Objects we have tried to read, and discovered to be corrupt.
-	 * <p>
-	 * The list is allocated after the first corruption is found, and filled in
-	 * as more entries are discovered. Typically this list is never used, as
-	 * pack files do not usually contain corrupt objects.
-	 */
-	private volatile LongList corruptObjects;
-
-	/**
-	 * Construct a reader for an existing, pre-indexed packfile.
-	 *
-	 * @param packFile
-	 *            path of the <code>.pack</code> file holding the data.
-	 * @param extensions
-	 *            additional pack file extensions with the same base as the pack
-	 */
-	public PackFile(File packFile, int extensions) {
-		this.packFile = packFile;
-		this.fileSnapshot = PackFileSnapshot.save(packFile);
-		this.packLastModified = fileSnapshot.lastModifiedInstant();
-		this.extensions = extensions;
-
-		// Multiply by 31 here so we can more directly combine with another
-		// value in WindowCache.hash(), without doing the multiply there.
-		//
-		hash = System.identityHashCode(this) * 31;
-		length = Long.MAX_VALUE;
-	}
-
-	private PackIndex idx() throws IOException {
-		PackIndex idx = loadedIdx;
-		if (idx == null) {
-			synchronized (this) {
-				idx = loadedIdx;
-				if (idx == null) {
-					if (invalid) {
-						throw new PackInvalidException(packFile, invalidatingCause);
-					}
-					try {
-						long start = System.currentTimeMillis();
-						idx = PackIndex.open(extFile(INDEX));
-						if (LOG.isDebugEnabled()) {
-							LOG.debug(String.format(
-									"Opening pack index %s, size %.3f MB took %d ms", //$NON-NLS-1$
-									extFile(INDEX).getAbsolutePath(),
-									Float.valueOf(extFile(INDEX).length()
-											/ (1024f * 1024)),
-									Long.valueOf(System.currentTimeMillis()
-											- start)));
-						}
-
-						if (packChecksum == null) {
-							packChecksum = idx.packChecksum;
-							fileSnapshot.setChecksum(
-									ObjectId.fromRaw(packChecksum));
-						} else if (!Arrays.equals(packChecksum,
-								idx.packChecksum)) {
-							throw new PackMismatchException(MessageFormat
-									.format(JGitText.get().packChecksumMismatch,
-											packFile.getPath(),
-											ObjectId.fromRaw(packChecksum)
-													.name(),
-											ObjectId.fromRaw(idx.packChecksum)
-													.name()));
-						}
-						loadedIdx = idx;
-					} catch (InterruptedIOException e) {
-						// don't invalidate the pack, we are interrupted from
-						// another thread
-						throw e;
-					} catch (IOException e) {
-						invalid = true;
-						invalidatingCause = e;
-						throw e;
-					}
-				}
-			}
-		}
-		return idx;
-	}
-	/**
-	 * Get the File object which locates this pack on disk.
-	 *
-	 * @return the File object which locates this pack on disk.
-	 */
-	public File getPackFile() {
-		return packFile;
+	private static String createName(String id, PackExt extension) {
+		return PREFIX + id + '.' + extension.getExtension();
 	}
 
 	/**
-	 * Get the index for this pack file.
+	 * Create a PackFile for a pack or related file.
 	 *
-	 * @return the index for this pack file.
-	 * @throws java.io.IOException
+	 * @param file
+	 *            File pointing to the location of the file.
 	 */
-	public PackIndex getIndex() throws IOException {
-		return idx();
+	public PackFile(File file) {
+		this(file.getParentFile(), file.getName());
 	}
 
 	/**
-	 * Get name extracted from {@code pack-*.pack} pattern.
+	 * Create a PackFile for a pack or related file.
 	 *
-	 * @return name extracted from {@code pack-*.pack} pattern.
-	 */
-	public String getPackName() {
-		String name = packName;
-		if (name == null) {
-			name = getPackFile().getName();
-			if (name.startsWith("pack-")) //$NON-NLS-1$
-				name = name.substring("pack-".length()); //$NON-NLS-1$
-			if (name.endsWith(".pack")) //$NON-NLS-1$
-				name = name.substring(0, name.length() - ".pack".length()); //$NON-NLS-1$
-			packName = name;
-		}
-		return name;
-	}
-
-	/**
-	 * Determine if an object is contained within the pack file.
-	 * <p>
-	 * For performance reasons only the index file is searched; the main pack
-	 * content is ignored entirely.
-	 * </p>
-	 *
+	 * @param directory
+	 *            Directory to create the PackFile in.
 	 * @param id
-	 *            the object to look for. Must not be null.
-	 * @return true if the object is in this pack; false otherwise.
-	 * @throws java.io.IOException
-	 *             the index file cannot be loaded into memory.
+	 *            the {@link ObjectId} for this pack
+	 * @param ext
+	 *            the <code>packExt</code> of the name.
 	 */
-	public boolean hasObject(AnyObjectId id) throws IOException {
-		final long offset = idx().findOffset(id);
-		return 0 < offset && !isCorrupt(offset);
+	public PackFile(File directory, ObjectId id, PackExt ext) {
+		this(directory, id.name(), ext);
 	}
 
 	/**
-	 * Determines whether a .keep file exists for this pack file.
+	 * Create a PackFile for a pack or related file.
 	 *
-	 * @return true if a .keep file exist.
-	 */
-	public boolean shouldBeKept() {
-		if (keepFile == null)
-			keepFile = extFile(KEEP);
-		return keepFile.exists();
-	}
-
-	/**
-	 * Get an object from this pack.
-	 *
-	 * @param curs
-	 *            temporary working space associated with the calling thread.
+	 * @param directory
+	 *            Directory to create the PackFile in.
 	 * @param id
-	 *            the object to obtain from the pack. Must not be null.
-	 * @return the object loader for the requested object if it is contained in
-	 *         this pack; null if the object was not found.
-	 * @throws IOException
-	 *             the pack file or the index could not be read.
+	 *            the <code>id</code> (40 Hex char) section of the pack name.
+	 * @param ext
+	 *            the <code>packExt</code> of the name.
 	 */
-	ObjectLoader get(WindowCursor curs, AnyObjectId id)
-			throws IOException {
-		final long offset = idx().findOffset(id);
-		return 0 < offset && !isCorrupt(offset) ? load(curs, offset) : null;
-	}
-
-	void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, int matchLimit)
-			throws IOException {
-		idx().resolve(matches, id, matchLimit);
+	public PackFile(File directory, String id, PackExt ext) {
+		this(directory, createName(id, ext));
 	}
 
 	/**
-	 * Close the resources utilized by this repository
-	 */
-	public void close() {
-		WindowCache.purge(this);
-		synchronized (this) {
-			loadedIdx = null;
-			reverseIdx = null;
-		}
-	}
-
-	/**
-	 * {@inheritDoc}
-	 * <p>
-	 * Provide iterator over entries in associated pack index, that should also
-	 * exist in this pack file. Objects returned by such iterator are mutable
-	 * during iteration.
-	 * <p>
-	 * Iterator returns objects in SHA-1 lexicographical order.
-	 * </p>
+	 * Create a PackFile for a pack or related file.
 	 *
-	 * @see PackIndex#iterator()
+	 * @param directory
+	 *            Directory to create the PackFile in.
+	 * @param name
+	 *            Filename (last path section) of the PackFile
 	 */
-	@Override
-	public Iterator<PackIndex.MutableEntry> iterator() {
-		try {
-			return idx().iterator();
-		} catch (IOException e) {
-			return Collections.<PackIndex.MutableEntry> emptyList().iterator();
-		}
-	}
+	public PackFile(File directory, String name) {
+		super(directory, name);
+		int dot = name.lastIndexOf('.');
 
-	/**
-	 * Obtain the total number of objects available in this pack. This method
-	 * relies on pack index, giving number of effectively available objects.
-	 *
-	 * @return number of objects in index of this pack, likewise in this pack
-	 * @throws IOException
-	 *             the index file cannot be loaded into memory.
-	 */
-	long getObjectCount() throws IOException {
-		return idx().getObjectCount();
-	}
-
-	/**
-	 * Search for object id with the specified start offset in associated pack
-	 * (reverse) index.
-	 *
-	 * @param offset
-	 *            start offset of object to find
-	 * @return object id for this offset, or null if no object was found
-	 * @throws IOException
-	 *             the index file cannot be loaded into memory.
-	 */
-	ObjectId findObjectForOffset(long offset) throws IOException {
-		return getReverseIdx().findObject(offset);
-	}
-
-	/**
-	 * Return the @{@link FileSnapshot} associated to the underlying packfile
-	 * that has been used when the object was created.
-	 *
-	 * @return the packfile @{@link FileSnapshot} that the object is loaded from.
-	 */
-	PackFileSnapshot getFileSnapshot() {
-		return fileSnapshot;
-	}
-
-	AnyObjectId getPackChecksum() {
-		return ObjectId.fromRaw(packChecksum);
-	}
-
-	private final byte[] decompress(final long position, final int sz,
-			final WindowCursor curs) throws IOException, DataFormatException {
-		byte[] dstbuf;
-		try {
-			dstbuf = new byte[sz];
-		} catch (OutOfMemoryError noMemory) {
-			// The size may be larger than our heap allows, return null to
-			// let the caller know allocation isn't possible and it should
-			// use the large object streaming approach instead.
-			//
-			// For example, this can occur when sz is 640 MB, and JRE
-			// maximum heap size is only 256 MB. Even if the JRE has
-			// 200 MB free, it cannot allocate a 640 MB byte array.
-			return null;
-		}
-
-		if (curs.inflate(this, position, dstbuf, false) != sz)
-			throw new EOFException(MessageFormat.format(
-					JGitText.get().shortCompressedStreamAt,
-					Long.valueOf(position)));
-		return dstbuf;
-	}
-
-	void copyPackAsIs(PackOutputStream out, WindowCursor curs)
-			throws IOException {
-		// Pin the first window, this ensures the length is accurate.
-		curs.pin(this, 0);
-		curs.copyPackAsIs(this, length, out);
-	}
-
-	final void copyAsIs(PackOutputStream out, LocalObjectToPack src,
-			boolean validate, WindowCursor curs) throws IOException,
-			StoredObjectRepresentationNotAvailableException {
-		beginCopyAsIs(src);
-		try {
-			copyAsIs2(out, src, validate, curs);
-		} finally {
-			endCopyAsIs();
-		}
-	}
-
-	private void copyAsIs2(PackOutputStream out, LocalObjectToPack src,
-			boolean validate, WindowCursor curs) throws IOException,
-			StoredObjectRepresentationNotAvailableException {
-		final CRC32 crc1 = validate ? new CRC32() : null;
-		final CRC32 crc2 = validate ? new CRC32() : null;
-		final byte[] buf = out.getCopyBuffer();
-
-		// Rip apart the header so we can discover the size.
-		//
-		readFully(src.offset, buf, 0, 20, curs);
-		int c = buf[0] & 0xff;
-		final int typeCode = (c >> 4) & 7;
-		long inflatedLength = c & 15;
-		int shift = 4;
-		int headerCnt = 1;
-		while ((c & 0x80) != 0) {
-			c = buf[headerCnt++] & 0xff;
-			inflatedLength += ((long) (c & 0x7f)) << shift;
-			shift += 7;
-		}
-
-		if (typeCode == Constants.OBJ_OFS_DELTA) {
-			do {
-				c = buf[headerCnt++] & 0xff;
-			} while ((c & 128) != 0);
-			if (validate) {
-				assert(crc1 != null && crc2 != null);
-				crc1.update(buf, 0, headerCnt);
-				crc2.update(buf, 0, headerCnt);
-			}
-		} else if (typeCode == Constants.OBJ_REF_DELTA) {
-			if (validate) {
-				assert(crc1 != null && crc2 != null);
-				crc1.update(buf, 0, headerCnt);
-				crc2.update(buf, 0, headerCnt);
-			}
-
-			readFully(src.offset + headerCnt, buf, 0, 20, curs);
-			if (validate) {
-				assert(crc1 != null && crc2 != null);
-				crc1.update(buf, 0, 20);
-				crc2.update(buf, 0, 20);
-			}
-			headerCnt += 20;
-		} else if (validate) {
-			assert(crc1 != null && crc2 != null);
-			crc1.update(buf, 0, headerCnt);
-			crc2.update(buf, 0, headerCnt);
-		}
-
-		final long dataOffset = src.offset + headerCnt;
-		final long dataLength = src.length;
-		final long expectedCRC;
-		final ByteArrayWindow quickCopy;
-
-		// Verify the object isn't corrupt before sending. If it is,
-		// we report it missing instead.
-		//
-		try {
-			quickCopy = curs.quickCopy(this, dataOffset, dataLength);
-
-			if (validate && idx().hasCRC32Support()) {
-				assert(crc1 != null);
-				// Index has the CRC32 code cached, validate the object.
-				//
-				expectedCRC = idx().findCRC32(src);
-				if (quickCopy != null) {
-					quickCopy.crc32(crc1, dataOffset, (int) dataLength);
-				} else {
-					long pos = dataOffset;
-					long cnt = dataLength;
-					while (cnt > 0) {
-						final int n = (int) Math.min(cnt, buf.length);
-						readFully(pos, buf, 0, n, curs);
-						crc1.update(buf, 0, n);
-						pos += n;
-						cnt -= n;
-					}
-				}
-				if (crc1.getValue() != expectedCRC) {
-					setCorrupt(src.offset);
-					throw new CorruptObjectException(MessageFormat.format(
-							JGitText.get().objectAtHasBadZlibStream,
-							Long.valueOf(src.offset), getPackFile()));
-				}
-			} else if (validate) {
-				// We don't have a CRC32 code in the index, so compute it
-				// now while inflating the raw data to get zlib to tell us
-				// whether or not the data is safe.
-				//
-				Inflater inf = curs.inflater();
-				byte[] tmp = new byte[1024];
-				if (quickCopy != null) {
-					quickCopy.check(inf, tmp, dataOffset, (int) dataLength);
-				} else {
-					assert(crc1 != null);
-					long pos = dataOffset;
-					long cnt = dataLength;
-					while (cnt > 0) {
-						final int n = (int) Math.min(cnt, buf.length);
-						readFully(pos, buf, 0, n, curs);
-						crc1.update(buf, 0, n);
-						inf.setInput(buf, 0, n);
-						while (inf.inflate(tmp, 0, tmp.length) > 0)
-							continue;
-						pos += n;
-						cnt -= n;
-					}
-				}
-				if (!inf.finished() || inf.getBytesRead() != dataLength) {
-					setCorrupt(src.offset);
-					throw new EOFException(MessageFormat.format(
-							JGitText.get().shortCompressedStreamAt,
-							Long.valueOf(src.offset)));
-				}
-				assert(crc1 != null);
-				expectedCRC = crc1.getValue();
-			} else {
-				expectedCRC = -1;
-			}
-		} catch (DataFormatException dataFormat) {
-			setCorrupt(src.offset);
-
-			CorruptObjectException corruptObject = new CorruptObjectException(
-					MessageFormat.format(
-							JGitText.get().objectAtHasBadZlibStream,
-							Long.valueOf(src.offset), getPackFile()),
-					dataFormat);
-
-			throw new StoredObjectRepresentationNotAvailableException(src,
-					corruptObject);
-
-		} catch (IOException ioError) {
-			throw new StoredObjectRepresentationNotAvailableException(src,
-					ioError);
-		}
-
-		if (quickCopy != null) {
-			// The entire object fits into a single byte array window slice,
-			// and we have it pinned.  Write this out without copying.
-			//
-			out.writeHeader(src, inflatedLength);
-			quickCopy.write(out, dataOffset, (int) dataLength);
-
-		} else if (dataLength <= buf.length) {
-			// Tiny optimization: Lots of objects are very small deltas or
-			// deflated commits that are likely to fit in the copy buffer.
-			//
-			if (!validate) {
-				long pos = dataOffset;
-				long cnt = dataLength;
-				while (cnt > 0) {
-					final int n = (int) Math.min(cnt, buf.length);
-					readFully(pos, buf, 0, n, curs);
-					pos += n;
-					cnt -= n;
-				}
-			}
-			out.writeHeader(src, inflatedLength);
-			out.write(buf, 0, (int) dataLength);
+		if (dot < 0) {
+			base = name;
+			hasOldPrefix = false;
+			packExt = null;
 		} else {
-			// Now we are committed to sending the object. As we spool it out,
-			// check its CRC32 code to make sure there wasn't corruption between
-			// the verification we did above, and us actually outputting it.
-			//
-			out.writeHeader(src, inflatedLength);
-			long pos = dataOffset;
-			long cnt = dataLength;
-			while (cnt > 0) {
-				final int n = (int) Math.min(cnt, buf.length);
-				readFully(pos, buf, 0, n, curs);
-				if (validate) {
-					assert(crc2 != null);
-					crc2.update(buf, 0, n);
-				}
-				out.write(buf, 0, n);
-				pos += n;
-				cnt -= n;
-			}
-			if (validate) {
-				assert(crc2 != null);
-				if (crc2.getValue() != expectedCRC) {
-					throw new CorruptObjectException(MessageFormat.format(
-							JGitText.get().objectAtHasBadZlibStream,
-							Long.valueOf(src.offset), getPackFile()));
-				}
+			base = name.substring(0, dot);
+			String tail = name.substring(dot + 1); // ["old-"] + extension
+			packExt = getPackExt(tail);
+			String old = tail.substring(0,
+					tail.length() - getExtension().length());
+			hasOldPrefix = old.equals(getExtPrefix(true));
+		}
+
+		id = base.startsWith(PREFIX) ? base.substring(PREFIX.length()) : base;
+	}
+
+	/**
+	 * Getter for the field <code>id</code>.
+	 *
+	 * @return the <code>id</code> (40 Hex char) section of the name.
+	 */
+	public String getId() {
+		return id;
+	}
+
+	/**
+	 * Getter for the field <code>packExt</code>.
+	 *
+	 * @return the <code>packExt</code> of the name.
+	 */
+	public PackExt getPackExt() {
+		return packExt;
+	}
+
+	/**
+	 * Create a new similar PackFile with the given extension instead.
+	 *
+	 * @param ext
+	 *            PackExt the extension to use.
+	 * @return a PackFile instance with specified extension
+	 */
+	public PackFile create(PackExt ext) {
+		return new PackFile(getParentFile(), getName(ext));
+	}
+
+	/**
+	 * Create a new similar PackFile in the given directory.
+	 *
+	 * @param directory
+	 *            Directory to create the new PackFile in.
+	 * @return a PackFile in the given directory
+	 */
+	public PackFile createForDirectory(File directory) {
+		return new PackFile(directory, getName(false));
+	}
+
+	/**
+	 * Create a new similar preserved PackFile in the given directory.
+	 *
+	 * @param directory
+	 *            Directory to create the new PackFile in.
+	 * @return a PackFile in the given directory with "old-" prefixing the
+	 *         extension
+	 */
+	public PackFile createPreservedForDirectory(File directory) {
+		return new PackFile(directory, getName(true));
+	}
+
+	private String getName(PackExt ext) {
+		return base + '.' + getExtPrefix(hasOldPrefix) + ext.getExtension();
+	}
+
+	private String getName(boolean isPreserved) {
+		return base + '.' + getExtPrefix(isPreserved) + getExtension();
+	}
+
+	private String getExtension() {
+		return packExt == null ? "" : packExt.getExtension(); //$NON-NLS-1$
+	}
+
+	private static String getExtPrefix(boolean isPreserved) {
+		return isPreserved ? "old-" : ""; //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	private static PackExt getPackExt(String endsWithExtension) {
+		for (PackExt ext : PackExt.values()) {
+			if (endsWithExtension.endsWith(ext.getExtension())) {
+				return ext;
 			}
 		}
-	}
-
-	boolean invalid() {
-		return invalid;
-	}
-
-	void setInvalid() {
-		invalid = true;
-	}
-
-	int incrementTransientErrorCount() {
-		return transientErrorCount.incrementAndGet();
-	}
-
-	void resetTransientErrorCount() {
-		transientErrorCount.set(0);
-	}
-
-	private void readFully(final long position, final byte[] dstbuf,
-			int dstoff, final int cnt, final WindowCursor curs)
-			throws IOException {
-		if (curs.copy(this, position, dstbuf, dstoff, cnt) != cnt)
-			throw new EOFException();
-	}
-
-	private synchronized void beginCopyAsIs(ObjectToPack otp)
-			throws StoredObjectRepresentationNotAvailableException {
-		if (++activeCopyRawData == 1 && activeWindows == 0) {
-			try {
-				doOpen();
-			} catch (IOException thisPackNotValid) {
-				throw new StoredObjectRepresentationNotAvailableException(otp,
-						thisPackNotValid);
-			}
-		}
-	}
-
-	private synchronized void endCopyAsIs() {
-		if (--activeCopyRawData == 0 && activeWindows == 0)
-			doClose();
-	}
-
-	synchronized boolean beginWindowCache() throws IOException {
-		if (++activeWindows == 1) {
-			if (activeCopyRawData == 0)
-				doOpen();
-			return true;
-		}
-		return false;
-	}
-
-	synchronized boolean endWindowCache() {
-		final boolean r = --activeWindows == 0;
-		if (r && activeCopyRawData == 0)
-			doClose();
-		return r;
-	}
-
-	private void doOpen() throws IOException {
-		if (invalid) {
-			openFail(true, invalidatingCause);
-			throw new PackInvalidException(packFile, invalidatingCause);
-		}
-		try {
-			synchronized (readLock) {
-				fd = new RandomAccessFile(packFile, "r"); //$NON-NLS-1$
-				length = fd.length();
-				onOpenPack();
-			}
-		} catch (InterruptedIOException e) {
-			// don't invalidate the pack, we are interrupted from another thread
-			openFail(false, e);
-			throw e;
-		} catch (FileNotFoundException fn) {
-			// don't invalidate the pack if opening an existing file failed
-			// since it may be related to a temporary lack of resources (e.g.
-			// max open files)
-			openFail(!packFile.exists(), fn);
-			throw fn;
-		} catch (EOFException | AccessDeniedException | NoSuchFileException
-				| CorruptObjectException | NoPackSignatureException
-				| PackMismatchException | UnpackException
-				| UnsupportedPackIndexVersionException
-				| UnsupportedPackVersionException pe) {
-			// exceptions signaling permanent problems with a pack
-			openFail(true, pe);
-			throw pe;
-		} catch (IOException | RuntimeException ge) {
-			// generic exceptions could be transient so we should not mark the
-			// pack invalid to avoid false MissingObjectExceptions
-			openFail(false, ge);
-			throw ge;
-		}
-	}
-
-	private void openFail(boolean invalidate, Exception cause) {
-		activeWindows = 0;
-		activeCopyRawData = 0;
-		invalid = invalidate;
-		invalidatingCause = cause;
-		doClose();
-	}
-
-	private void doClose() {
-		synchronized (readLock) {
-			if (fd != null) {
-				try {
-					fd.close();
-				} catch (IOException err) {
-					// Ignore a close event. We had it open only for reading.
-					// There should not be errors related to network buffers
-					// not flushed, etc.
-				}
-				fd = null;
-			}
-		}
-	}
-
-	ByteArrayWindow read(long pos, int size) throws IOException {
-		synchronized (readLock) {
-			if (invalid || fd == null) {
-				// Due to concurrency between a read and another packfile invalidation thread
-				// one thread could come up to this point and then fail with NPE.
-				// Detect the situation and throw a proper exception so that can be properly
-				// managed by the main packfile search loop and the Git client won't receive
-				// any failures.
-				throw new PackInvalidException(packFile, invalidatingCause);
-			}
-			if (length < pos + size)
-				size = (int) (length - pos);
-			final byte[] buf = new byte[size];
-			fd.seek(pos);
-			fd.readFully(buf, 0, size);
-			return new ByteArrayWindow(this, pos, buf);
-		}
-	}
-
-	ByteWindow mmap(long pos, int size) throws IOException {
-		synchronized (readLock) {
-			if (length < pos + size)
-				size = (int) (length - pos);
-
-			MappedByteBuffer map;
-			try {
-				map = fd.getChannel().map(MapMode.READ_ONLY, pos, size);
-			} catch (IOException ioe1) {
-				// The most likely reason this failed is the JVM has run out
-				// of virtual memory. We need to discard quickly, and try to
-				// force the GC to finalize and release any existing mappings.
-				//
-				System.gc();
-				System.runFinalization();
-				map = fd.getChannel().map(MapMode.READ_ONLY, pos, size);
-			}
-
-			if (map.hasArray())
-				return new ByteArrayWindow(this, pos, map.array());
-			return new ByteBufferWindow(this, pos, map);
-		}
-	}
-
-	private void onOpenPack() throws IOException {
-		final PackIndex idx = idx();
-		final byte[] buf = new byte[20];
-
-		fd.seek(0);
-		fd.readFully(buf, 0, 12);
-		if (RawParseUtils.match(buf, 0, Constants.PACK_SIGNATURE) != 4) {
-			throw new NoPackSignatureException(JGitText.get().notAPACKFile);
-		}
-		final long vers = NB.decodeUInt32(buf, 4);
-		final long packCnt = NB.decodeUInt32(buf, 8);
-		if (vers != 2 && vers != 3) {
-			throw new UnsupportedPackVersionException(vers);
-		}
-
-		if (packCnt != idx.getObjectCount()) {
-			throw new PackMismatchException(MessageFormat.format(
-					JGitText.get().packObjectCountMismatch,
-					Long.valueOf(packCnt), Long.valueOf(idx.getObjectCount()),
-					getPackFile()));
-		}
-
-		fd.seek(length - 20);
-		fd.readFully(buf, 0, 20);
-		if (!Arrays.equals(buf, packChecksum)) {
-			throw new PackMismatchException(MessageFormat.format(
-					JGitText.get().packChecksumMismatch,
-					getPackFile(),
-					ObjectId.fromRaw(buf).name(),
-					ObjectId.fromRaw(idx.packChecksum).name()));
-		}
-	}
-
-	ObjectLoader load(WindowCursor curs, long pos)
-			throws IOException, LargeObjectException {
-		try {
-			final byte[] ib = curs.tempId;
-			Delta delta = null;
-			byte[] data = null;
-			int type = Constants.OBJ_BAD;
-			boolean cached = false;
-
-			SEARCH: for (;;) {
-				readFully(pos, ib, 0, 20, curs);
-				int c = ib[0] & 0xff;
-				final int typeCode = (c >> 4) & 7;
-				long sz = c & 15;
-				int shift = 4;
-				int p = 1;
-				while ((c & 0x80) != 0) {
-					c = ib[p++] & 0xff;
-					sz += ((long) (c & 0x7f)) << shift;
-					shift += 7;
-				}
-
-				switch (typeCode) {
-				case Constants.OBJ_COMMIT:
-				case Constants.OBJ_TREE:
-				case Constants.OBJ_BLOB:
-				case Constants.OBJ_TAG: {
-					if (delta != null || sz < curs.getStreamFileThreshold()) {
-						data = decompress(pos + p, (int) sz, curs);
-					}
-
-					if (delta != null) {
-						type = typeCode;
-						break SEARCH;
-					}
-
-					if (data != null) {
-						return new ObjectLoader.SmallObject(typeCode, data);
-					}
-					return new LargePackedWholeObject(typeCode, sz, pos, p,
-							this, curs.db);
-				}
-
-				case Constants.OBJ_OFS_DELTA: {
-					c = ib[p++] & 0xff;
-					long base = c & 127;
-					while ((c & 128) != 0) {
-						base += 1;
-						c = ib[p++] & 0xff;
-						base <<= 7;
-						base += (c & 127);
-					}
-					base = pos - base;
-					delta = new Delta(delta, pos, (int) sz, p, base);
-					if (sz != delta.deltaSize)
-						break SEARCH;
-
-					DeltaBaseCache.Entry e = curs.getDeltaBaseCache().get(this, base);
-					if (e != null) {
-						type = e.type;
-						data = e.data;
-						cached = true;
-						break SEARCH;
-					}
-					pos = base;
-					continue SEARCH;
-				}
-
-				case Constants.OBJ_REF_DELTA: {
-					readFully(pos + p, ib, 0, 20, curs);
-					long base = findDeltaBase(ObjectId.fromRaw(ib));
-					delta = new Delta(delta, pos, (int) sz, p + 20, base);
-					if (sz != delta.deltaSize)
-						break SEARCH;
-
-					DeltaBaseCache.Entry e = curs.getDeltaBaseCache().get(this, base);
-					if (e != null) {
-						type = e.type;
-						data = e.data;
-						cached = true;
-						break SEARCH;
-					}
-					pos = base;
-					continue SEARCH;
-				}
-
-				default:
-					throw new IOException(MessageFormat.format(
-							JGitText.get().unknownObjectType,
-							Integer.valueOf(typeCode)));
-				}
-			}
-
-			// At this point there is at least one delta to apply to data.
-			// (Whole objects with no deltas to apply return early above.)
-
-			if (data == null)
-				throw new IOException(JGitText.get().inMemoryBufferLimitExceeded);
-
-			assert(delta != null);
-			do {
-				// Cache only the base immediately before desired object.
-				if (cached)
-					cached = false;
-				else if (delta.next == null)
-					curs.getDeltaBaseCache().store(this, delta.basePos, data, type);
-
-				pos = delta.deltaPos;
-
-				final byte[] cmds = decompress(pos + delta.hdrLen,
-						delta.deltaSize, curs);
-				if (cmds == null) {
-					data = null; // Discard base in case of OutOfMemoryError
-					throw new LargeObjectException.OutOfMemory(new OutOfMemoryError());
-				}
-
-				final long sz = BinaryDelta.getResultSize(cmds);
-				if (Integer.MAX_VALUE <= sz)
-					throw new LargeObjectException.ExceedsByteArrayLimit();
-
-				final byte[] result;
-				try {
-					result = new byte[(int) sz];
-				} catch (OutOfMemoryError tooBig) {
-					data = null; // Discard base in case of OutOfMemoryError
-					throw new LargeObjectException.OutOfMemory(tooBig);
-				}
-
-				BinaryDelta.apply(data, cmds, result);
-				data = result;
-				delta = delta.next;
-			} while (delta != null);
-
-			return new ObjectLoader.SmallObject(type, data);
-
-		} catch (DataFormatException dfe) {
-			throw new CorruptObjectException(
-					MessageFormat.format(
-							JGitText.get().objectAtHasBadZlibStream,
-							Long.valueOf(pos), getPackFile()),
-					dfe);
-		}
-	}
-
-	private long findDeltaBase(ObjectId baseId) throws IOException,
-			MissingObjectException {
-		long ofs = idx().findOffset(baseId);
-		if (ofs < 0)
-			throw new MissingObjectException(baseId,
-					JGitText.get().missingDeltaBase);
-		return ofs;
-	}
-
-	private static class Delta {
-		/** Child that applies onto this object. */
-		final Delta next;
-
-		/** Offset of the delta object. */
-		final long deltaPos;
-
-		/** Size of the inflated delta stream. */
-		final int deltaSize;
-
-		/** Total size of the delta's pack entry header (including base). */
-		final int hdrLen;
-
-		/** Offset of the base object this delta applies onto. */
-		final long basePos;
-
-		Delta(Delta next, long ofs, int sz, int hdrLen, long baseOffset) {
-			this.next = next;
-			this.deltaPos = ofs;
-			this.deltaSize = sz;
-			this.hdrLen = hdrLen;
-			this.basePos = baseOffset;
-		}
-	}
-
-	byte[] getDeltaHeader(WindowCursor wc, long pos)
-			throws IOException, DataFormatException {
-		// The delta stream starts as two variable length integers. If we
-		// assume they are 64 bits each, we need 16 bytes to encode them,
-		// plus 2 extra bytes for the variable length overhead. So 18 is
-		// the longest delta instruction header.
-		//
-		final byte[] hdr = new byte[18];
-		wc.inflate(this, pos, hdr, true /* headerOnly */);
-		return hdr;
-	}
-
-	int getObjectType(WindowCursor curs, long pos) throws IOException {
-		final byte[] ib = curs.tempId;
-		for (;;) {
-			readFully(pos, ib, 0, 20, curs);
-			int c = ib[0] & 0xff;
-			final int type = (c >> 4) & 7;
-
-			switch (type) {
-			case Constants.OBJ_COMMIT:
-			case Constants.OBJ_TREE:
-			case Constants.OBJ_BLOB:
-			case Constants.OBJ_TAG:
-				return type;
-
-			case Constants.OBJ_OFS_DELTA: {
-				int p = 1;
-				while ((c & 0x80) != 0)
-					c = ib[p++] & 0xff;
-				c = ib[p++] & 0xff;
-				long ofs = c & 127;
-				while ((c & 128) != 0) {
-					ofs += 1;
-					c = ib[p++] & 0xff;
-					ofs <<= 7;
-					ofs += (c & 127);
-				}
-				pos = pos - ofs;
-				continue;
-			}
-
-			case Constants.OBJ_REF_DELTA: {
-				int p = 1;
-				while ((c & 0x80) != 0)
-					c = ib[p++] & 0xff;
-				readFully(pos + p, ib, 0, 20, curs);
-				pos = findDeltaBase(ObjectId.fromRaw(ib));
-				continue;
-			}
-
-			default:
-				throw new IOException(
-						MessageFormat.format(JGitText.get().unknownObjectType,
-								Integer.valueOf(type)));
-			}
-		}
-	}
-
-	long getObjectSize(WindowCursor curs, AnyObjectId id)
-			throws IOException {
-		final long offset = idx().findOffset(id);
-		return 0 < offset ? getObjectSize(curs, offset) : -1;
-	}
-
-	long getObjectSize(WindowCursor curs, long pos)
-			throws IOException {
-		final byte[] ib = curs.tempId;
-		readFully(pos, ib, 0, 20, curs);
-		int c = ib[0] & 0xff;
-		final int type = (c >> 4) & 7;
-		long sz = c & 15;
-		int shift = 4;
-		int p = 1;
-		while ((c & 0x80) != 0) {
-			c = ib[p++] & 0xff;
-			sz += ((long) (c & 0x7f)) << shift;
-			shift += 7;
-		}
-
-		long deltaAt;
-		switch (type) {
-		case Constants.OBJ_COMMIT:
-		case Constants.OBJ_TREE:
-		case Constants.OBJ_BLOB:
-		case Constants.OBJ_TAG:
-			return sz;
-
-		case Constants.OBJ_OFS_DELTA:
-			c = ib[p++] & 0xff;
-			while ((c & 128) != 0)
-				c = ib[p++] & 0xff;
-			deltaAt = pos + p;
-			break;
-
-		case Constants.OBJ_REF_DELTA:
-			deltaAt = pos + p + 20;
-			break;
-
-		default:
-			throw new IOException(MessageFormat.format(
-					JGitText.get().unknownObjectType, Integer.valueOf(type)));
-		}
-
-		try {
-			return BinaryDelta.getResultSize(getDeltaHeader(curs, deltaAt));
-		} catch (DataFormatException e) {
-			throw new CorruptObjectException(MessageFormat.format(
-					JGitText.get().objectAtHasBadZlibStream, Long.valueOf(pos),
-					getPackFile()), e);
-		}
-	}
-
-	LocalObjectRepresentation representation(final WindowCursor curs,
-			final AnyObjectId objectId) throws IOException {
-		final long pos = idx().findOffset(objectId);
-		if (pos < 0)
-			return null;
-
-		final byte[] ib = curs.tempId;
-		readFully(pos, ib, 0, 20, curs);
-		int c = ib[0] & 0xff;
-		int p = 1;
-		final int typeCode = (c >> 4) & 7;
-		while ((c & 0x80) != 0)
-			c = ib[p++] & 0xff;
-
-		long len = (findEndOffset(pos) - pos);
-		switch (typeCode) {
-		case Constants.OBJ_COMMIT:
-		case Constants.OBJ_TREE:
-		case Constants.OBJ_BLOB:
-		case Constants.OBJ_TAG:
-			return LocalObjectRepresentation.newWhole(this, pos, len - p);
-
-		case Constants.OBJ_OFS_DELTA: {
-			c = ib[p++] & 0xff;
-			long ofs = c & 127;
-			while ((c & 128) != 0) {
-				ofs += 1;
-				c = ib[p++] & 0xff;
-				ofs <<= 7;
-				ofs += (c & 127);
-			}
-			ofs = pos - ofs;
-			return LocalObjectRepresentation.newDelta(this, pos, len - p, ofs);
-		}
-
-		case Constants.OBJ_REF_DELTA: {
-			len -= p;
-			len -= Constants.OBJECT_ID_LENGTH;
-			readFully(pos + p, ib, 0, 20, curs);
-			ObjectId id = ObjectId.fromRaw(ib);
-			return LocalObjectRepresentation.newDelta(this, pos, len, id);
-		}
-
-		default:
-			throw new IOException(
-					MessageFormat.format(JGitText.get().unknownObjectType,
-							Integer.valueOf(typeCode)));
-		}
-	}
-
-	private long findEndOffset(long startOffset)
-			throws IOException, CorruptObjectException {
-		final long maxOffset = length - 20;
-		return getReverseIdx().findNextOffset(startOffset, maxOffset);
-	}
-
-	synchronized PackBitmapIndex getBitmapIndex() throws IOException {
-		if (invalid || invalidBitmap)
-			return null;
-		if (bitmapIdx == null && hasExt(BITMAP_INDEX)) {
-			final PackBitmapIndex idx;
-			try {
-				idx = PackBitmapIndex.open(extFile(BITMAP_INDEX), idx(),
-						getReverseIdx());
-			} catch (FileNotFoundException e) {
-				// Once upon a time this bitmap file existed. Now it
-				// has been removed. Most likely an external gc  has
-				// removed this packfile and the bitmap
-				 invalidBitmap = true;
-				 return null;
-			}
-
-			// At this point, idx() will have set packChecksum.
-			if (Arrays.equals(packChecksum, idx.packChecksum))
-				bitmapIdx = idx;
-			else
-				invalidBitmap = true;
-		}
-		return bitmapIdx;
-	}
-
-	private synchronized PackReverseIndex getReverseIdx() throws IOException {
-		if (reverseIdx == null)
-			reverseIdx = new PackReverseIndex(idx());
-		return reverseIdx;
-	}
-
-	private boolean isCorrupt(long offset) {
-		LongList list = corruptObjects;
-		if (list == null)
-			return false;
-		synchronized (list) {
-			return list.contains(offset);
-		}
-	}
-
-	private void setCorrupt(long offset) {
-		LongList list = corruptObjects;
-		if (list == null) {
-			synchronized (readLock) {
-				list = corruptObjects;
-				if (list == null) {
-					list = new LongList();
-					corruptObjects = list;
-				}
-			}
-		}
-		synchronized (list) {
-			list.add(offset);
-		}
-	}
-
-	private File extFile(PackExt ext) {
-		String p = packFile.getName();
-		int dot = p.lastIndexOf('.');
-		String b = (dot < 0) ? p : p.substring(0, dot);
-		return new File(packFile.getParentFile(), b + '.' + ext.getExtension());
-	}
-
-	private boolean hasExt(PackExt ext) {
-		return (extensions & ext.getBit()) != 0;
-	}
-
-	@SuppressWarnings("nls")
-	@Override
-	public String toString() {
-		return "PackFile [packFileName=" + packFile.getName() + ", length="
-				+ packFile.length() + ", packChecksum="
-				+ ObjectId.fromRaw(packChecksum).name() + "]";
+		throw new IllegalArgumentException(MessageFormat.format(
+				JGitText.get().unrecognizedPackExtension, endsWithExtension));
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
index 31686be..942cc96 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
@@ -34,7 +34,7 @@
 
 /**
  * Access path to locate objects by {@link org.eclipse.jgit.lib.ObjectId} in a
- * {@link org.eclipse.jgit.internal.storage.file.PackFile}.
+ * {@link org.eclipse.jgit.internal.storage.file.Pack}.
  * <p>
  * Indexes are strictly redundant information in that we can rebuild all of the
  * data held in the index file from the on disk representation of the pack file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java
index 612b123..87e0b44 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java
@@ -25,7 +25,7 @@
 
 /**
  * Creates a table of contents to support random access by
- * {@link org.eclipse.jgit.internal.storage.file.PackFile}.
+ * {@link org.eclipse.jgit.internal.storage.file.Pack}.
  * <p>
  * Pack index files (the <code>.idx</code> suffix in a pack file pair) provides
  * random access to any object in the pack by associating an ObjectId to the
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java
index a9e0588..0bceca7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java
@@ -16,11 +16,11 @@
 class PackInputStream extends InputStream {
 	private final WindowCursor wc;
 
-	private final PackFile pack;
+	private final Pack pack;
 
 	private long pos;
 
-	PackInputStream(PackFile pack, long pos, WindowCursor wc)
+	PackInputStream(Pack pack, long pos, WindowCursor wc)
 			throws IOException {
 		this.pack = pack;
 		this.pos = pos;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java
index a27a2b0..d6209c4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java
@@ -76,6 +76,7 @@
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -273,16 +274,16 @@
 		}
 
 		Collections.sort(objectList);
-		File tmpIdx = idxFor(tmpPack);
+		File tmpIdx = idxFor(tmpPack); // TODO(nasserg) Use PackFile?
 		writePackIndex(tmpIdx, packHash, objectList);
 
-		File realPack = new File(db.getPackDirectory(),
-				"pack-" + computeName(objectList).name() + ".pack"); //$NON-NLS-1$ //$NON-NLS-2$
+		PackFile realPack = new PackFile(db.getPackDirectory(),
+				computeName(objectList), PackExt.PACK);
 		db.closeAllPackHandles(realPack);
 		tmpPack.setReadOnly();
 		FileUtils.rename(tmpPack, realPack, ATOMIC_MOVE);
 
-		File realIdx = idxFor(realPack);
+		PackFile realIdx = realPack.create(PackExt.INDEX);
 		tmpIdx.setReadOnly();
 		try {
 			FileUtils.rename(tmpIdx, realIdx, ATOMIC_MOVE);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackLock.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackLock.java
index 2c2f791..482b143 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackLock.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackLock.java
@@ -18,7 +18,7 @@
 import org.eclipse.jgit.util.FileUtils;
 
 /**
- * Keeps track of a {@link org.eclipse.jgit.internal.storage.file.PackFile}'s
+ * Keeps track of a {@link org.eclipse.jgit.internal.storage.file.Pack}'s
  * associated <code>.keep</code> file.
  */
 public class PackLock {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java
index 4d80a03..ee458e2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackReverseIndex.java
@@ -25,7 +25,7 @@
  * </p>
  *
  * @see PackIndex
- * @see PackFile
+ * @see Pack
  */
 public class PackReverseIndex {
 	/** Index we were created from, and that has our ObjectId data. */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java
index 9dbdbc7..2c0ade6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java
@@ -51,9 +51,6 @@
 	 */
 	private ObjectId objId;
 
-	/** True if HEAD must be moved to the destination reference. */
-	private boolean updateHEAD;
-
 	/** A reference we backup {@link #objId} into during the rename. */
 	private RefDirectoryUpdate tmp;
 
@@ -69,7 +66,7 @@
 			return Result.IO_FAILURE; // not supported
 
 		objId = source.getOldObjectId();
-		updateHEAD = needToUpdateHEAD();
+		boolean updateHEAD = needToUpdateHEAD();
 		tmp = refdb.newTemporaryUpdate();
 		try (RevWalk rw = new RevWalk(refdb.getRepository())) {
 			// First backup the source so its never unreachable.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java
index 3e8cb3a..25653b3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCache.java
@@ -33,7 +33,7 @@
 import org.eclipse.jgit.util.Monitoring;
 
 /**
- * Caches slices of a {@link org.eclipse.jgit.internal.storage.file.PackFile} in
+ * Caches slices of a {@link org.eclipse.jgit.internal.storage.file.Pack} in
  * memory for faster read access.
  * <p>
  * The WindowCache serves as a Java based "buffer cache", loading segments of a
@@ -41,7 +41,7 @@
  * only tiny slices of a file, the WindowCache tries to smooth out these tiny
  * reads into larger block-sized IO operations.
  * <p>
- * Whenever a cache miss occurs, {@link #load(PackFile, long)} is invoked by
+ * Whenever a cache miss occurs, {@link #load(Pack, long)} is invoked by
  * exactly one thread for the given <code>(PackFile,position)</code> key tuple.
  * This is ensured by an array of locks, with the tuple hashed to a lock
  * instance.
@@ -80,10 +80,10 @@
  * <p>
  * This cache has an implementation rule such that:
  * <ul>
- * <li>{@link #load(PackFile, long)} is invoked by at most one thread at a time
+ * <li>{@link #load(Pack, long)} is invoked by at most one thread at a time
  * for a given <code>(PackFile,position)</code> tuple.</li>
  * <li>For every <code>load()</code> invocation there is exactly one
- * {@link #createRef(PackFile, long, ByteWindow)} invocation to wrap a
+ * {@link #createRef(Pack, long, ByteWindow)} invocation to wrap a
  * SoftReference or a StrongReference around the cached entity.</li>
  * <li>For every Reference created by <code>createRef()</code> there will be
  * exactly one call to {@link #clear(PageRef)} to cleanup any resources associated
@@ -91,10 +91,10 @@
  * </ul>
  * <p>
  * Therefore, it is safe to perform resource accounting increments during the
- * {@link #load(PackFile, long)} or
- * {@link #createRef(PackFile, long, ByteWindow)} methods, and matching
+ * {@link #load(Pack, long)} or
+ * {@link #createRef(Pack, long, ByteWindow)} methods, and matching
  * decrements during {@link #clear(PageRef)}. Implementors may need to override
- * {@link #createRef(PackFile, long, ByteWindow)} in order to embed additional
+ * {@link #createRef(Pack, long, ByteWindow)} in order to embed additional
  * accounting information into an implementation specific
  * {@link org.eclipse.jgit.internal.storage.file.WindowCache.PageRef} subclass, as
  * the cached entity may have already been evicted by the JRE's garbage
@@ -170,7 +170,7 @@
 		 * @param delta
 		 *            delta of cached bytes
 		 */
-		void recordOpenBytes(PackFile pack, int delta);
+		void recordOpenBytes(Pack pack, int delta);
 
 		/**
 		 * Returns a snapshot of this recorder's stats. Note that this may be an
@@ -242,7 +242,7 @@
 		}
 
 		@Override
-		public void recordOpenBytes(PackFile pack, int delta) {
+		public void recordOpenBytes(Pack pack, int delta) {
 			openByteCount.add(delta);
 			String repositoryId = repositoryId(pack);
 			LongAdder la = openByteCountPerRepository
@@ -254,9 +254,8 @@
 			}
 		}
 
-		private static String repositoryId(PackFile pack) {
-			// use repository's gitdir since packfile doesn't know its
-			// repository
+		private static String repositoryId(Pack pack) {
+			// use repository's gitdir since Pack doesn't know its repository
 			return pack.getPackFile().getParentFile().getParentFile()
 					.getParent();
 		}
@@ -380,7 +379,7 @@
 		return cache.publishMBeanIfNeeded();
 	}
 
-	static final ByteWindow get(PackFile pack, long offset)
+	static final ByteWindow get(Pack pack, long offset)
 			throws IOException {
 		final WindowCache c = cache;
 		final ByteWindow r = c.getOrLoad(pack, c.toStart(offset));
@@ -395,7 +394,7 @@
 		return r;
 	}
 
-	static final void purge(PackFile pack) {
+	static final void purge(Pack pack) {
 		cache.removeAll(pack);
 	}
 
@@ -506,7 +505,7 @@
 		return packHash + (int) (off >>> windowSizeShift);
 	}
 
-	private ByteWindow load(PackFile pack, long offset) throws IOException {
+	private ByteWindow load(Pack pack, long offset) throws IOException {
 		long startTime = System.nanoTime();
 		if (pack.beginWindowCache())
 			statsRecorder.recordOpenFiles(1);
@@ -525,7 +524,7 @@
 		}
 	}
 
-	private PageRef<ByteWindow> createRef(PackFile p, long o, ByteWindow v) {
+	private PageRef<ByteWindow> createRef(Pack p, long o, ByteWindow v) {
 		final PageRef<ByteWindow> ref = useStrongRefs
 				? new StrongRef(p, o, v, queue)
 				: new SoftRef(p, o, v, (SoftCleanupQueue) queue);
@@ -539,7 +538,7 @@
 		close(ref.getPack());
 	}
 
-	private void close(PackFile pack) {
+	private void close(Pack pack) {
 		if (pack.endWindowCache()) {
 			statsRecorder.recordOpenFiles(-1);
 		}
@@ -578,9 +577,9 @@
 	 * @return the object reference.
 	 * @throws IOException
 	 *             the object reference was not in the cache and could not be
-	 *             obtained by {@link #load(PackFile, long)}.
+	 *             obtained by {@link #load(Pack, long)}.
 	 */
-	private ByteWindow getOrLoad(PackFile pack, long position)
+	private ByteWindow getOrLoad(Pack pack, long position)
 			throws IOException {
 		final int slot = slot(pack, position);
 		final Entry e1 = table.get(slot);
@@ -623,7 +622,7 @@
 		return v;
 	}
 
-	private ByteWindow scan(Entry n, PackFile pack, long position) {
+	private ByteWindow scan(Entry n, Pack pack, long position) {
 		for (; n != null; n = n.next) {
 			final PageRef<ByteWindow> r = n.ref;
 			if (r.getPack() == pack && r.getPosition() == position) {
@@ -704,7 +703,7 @@
 	/**
 	 * Clear all entries related to a single file.
 	 * <p>
-	 * Typically this method is invoked during {@link PackFile#close()}, when we
+	 * Typically this method is invoked during {@link Pack#close()}, when we
 	 * know the pack is never going to be useful to us again (for example, it no
 	 * longer exists on disk). A concurrent reader loading an entry from this
 	 * same pack may cause the pack to become stuck in the cache anyway.
@@ -712,7 +711,7 @@
 	 * @param pack
 	 *            the file to purge all entries of.
 	 */
-	private void removeAll(PackFile pack) {
+	private void removeAll(Pack pack) {
 		for (int s = 0; s < tableSize; s++) {
 			final Entry e1 = table.get(s);
 			boolean hasDead = false;
@@ -733,11 +732,11 @@
 		queue.gc();
 	}
 
-	private int slot(PackFile pack, long position) {
+	private int slot(Pack pack, long position) {
 		return (hash(pack.hash, position) >>> 1) % tableSize;
 	}
 
-	private Lock lock(PackFile pack, long position) {
+	private Lock lock(Pack pack, long position) {
 		return locks[(hash(pack.hash, position) >>> 1) % locks.length];
 	}
 
@@ -799,16 +798,20 @@
 		boolean kill();
 
 		/**
-		 * Get the packfile the referenced cache page is allocated for
+		 * Get the {@link org.eclipse.jgit.internal.storage.file.Pack} the
+		 * referenced cache page is allocated for
 		 *
-		 * @return the packfile the referenced cache page is allocated for
+		 * @return the {@link org.eclipse.jgit.internal.storage.file.Pack} the
+		 *         referenced cache page is allocated for
 		 */
-		PackFile getPack();
+		Pack getPack();
 
 		/**
-		 * Get the position of the referenced cache page in the packfile
+		 * Get the position of the referenced cache page in the
+		 * {@link org.eclipse.jgit.internal.storage.file.Pack}
 		 *
-		 * @return the position of the referenced cache page in the packfile
+		 * @return the position of the referenced cache page in the
+		 *         {@link org.eclipse.jgit.internal.storage.file.Pack}
 		 */
 		long getPosition();
 
@@ -844,7 +847,7 @@
 	/** A soft reference wrapped around a cached object. */
 	private static class SoftRef extends SoftReference<ByteWindow>
 			implements PageRef<ByteWindow> {
-		private final PackFile pack;
+		private final Pack pack;
 
 		private final long position;
 
@@ -852,7 +855,7 @@
 
 		private long lastAccess;
 
-		protected SoftRef(final PackFile pack, final long position,
+		protected SoftRef(final Pack pack, final long position,
 				final ByteWindow v, final SoftCleanupQueue queue) {
 			super(v, queue);
 			this.pack = pack;
@@ -861,7 +864,7 @@
 		}
 
 		@Override
-		public PackFile getPack() {
+		public Pack getPack() {
 			return pack;
 		}
 
@@ -900,7 +903,7 @@
 	private static class StrongRef implements PageRef<ByteWindow> {
 		private ByteWindow referent;
 
-		private final PackFile pack;
+		private final Pack pack;
 
 		private final long position;
 
@@ -910,7 +913,7 @@
 
 		private CleanupQueue queue;
 
-		protected StrongRef(final PackFile pack, final long position,
+		protected StrongRef(final Pack pack, final long position,
 				final ByteWindow v, final CleanupQueue queue) {
 			this.pack = pack;
 			this.position = position;
@@ -920,7 +923,7 @@
 		}
 
 		@Override
-		public PackFile getPack() {
+		public Pack getPack() {
 			return pack;
 		}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java
index 6c97570..e7fd7b9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java
@@ -86,7 +86,7 @@
 	/** {@inheritDoc} */
 	@Override
 	public BitmapIndex getBitmapIndex() throws IOException {
-		for (PackFile pack : db.getPacks()) {
+		for (Pack pack : db.getPacks()) {
 			PackBitmapIndex index = pack.getBitmapIndex();
 			if (index != null)
 				return new BitmapIndexImpl(index);
@@ -98,7 +98,7 @@
 	@Override
 	public Collection<CachedPack> getCachedPacksAndUpdate(
 			BitmapBuilder needBitmap) throws IOException {
-		for (PackFile pack : db.getPacks()) {
+		for (Pack pack : db.getPacks()) {
 			PackBitmapIndex index = pack.getBitmapIndex();
 			if (needBitmap.removeAllOrNone(index))
 				return Collections.<CachedPack> singletonList(
@@ -218,7 +218,7 @@
 	 *             this cursor does not match the provider or id and the proper
 	 *             window could not be acquired through the provider's cache.
 	 */
-	int copy(final PackFile pack, long position, final byte[] dstbuf,
+	int copy(final Pack pack, long position, final byte[] dstbuf,
 			int dstoff, final int cnt) throws IOException {
 		final long length = pack.length;
 		int need = cnt;
@@ -239,7 +239,7 @@
 		((LocalCachedPack) pack).copyAsIs(out, this);
 	}
 
-	void copyPackAsIs(final PackFile pack, final long length,
+	void copyPackAsIs(final Pack pack, final long length,
 			final PackOutputStream out) throws IOException {
 		long position = 12;
 		long remaining = length - (12 + 20);
@@ -275,7 +275,7 @@
 	 *             the inflater encountered an invalid chunk of data. Data
 	 *             stream corruption is likely.
 	 */
-	int inflate(final PackFile pack, long position, final byte[] dstbuf,
+	int inflate(final Pack pack, long position, final byte[] dstbuf,
 			boolean headerOnly) throws IOException, DataFormatException {
 		prepareInflater();
 		pin(pack, position);
@@ -293,7 +293,7 @@
 		}
 	}
 
-	ByteArrayWindow quickCopy(PackFile p, long pos, long cnt)
+	ByteArrayWindow quickCopy(Pack p, long pos, long cnt)
 			throws IOException {
 		pin(p, pos);
 		if (window instanceof ByteArrayWindow
@@ -314,7 +314,7 @@
 			inf.reset();
 	}
 
-	void pin(PackFile pack, long position)
+	void pin(Pack pack, long position)
 			throws IOException {
 		final ByteWindow w = window;
 		if (w == null || !w.contains(pack, position)) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java
index bedc693..6fb775d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java
@@ -13,66 +13,26 @@
 /**
  * A pack file extension.
  */
-public class PackExt {
-	private static volatile PackExt[] VALUES = new PackExt[] {};
-
+public enum PackExt {
 	/** A pack file extension. */
-	public static final PackExt PACK = newPackExt("pack"); //$NON-NLS-1$
+	PACK("pack"), //$NON-NLS-1$
 
 	/** A pack index file extension. */
-	public static final PackExt INDEX = newPackExt("idx"); //$NON-NLS-1$
+	INDEX("idx"), //$NON-NLS-1$
 
 	/** A keep pack file extension. */
-	public static final PackExt KEEP = newPackExt("keep"); //$NON-NLS-1$
+	KEEP("keep"), //$NON-NLS-1$
 
 	/** A pack bitmap index file extension. */
-	public static final PackExt BITMAP_INDEX = newPackExt("bitmap"); //$NON-NLS-1$
+	BITMAP_INDEX("bitmap"), //$NON-NLS-1$
 
 	/** A reftable file. */
-	public static final PackExt REFTABLE = newPackExt("ref"); //$NON-NLS-1$
-
-	/**
-	 * Get all of the PackExt values.
-	 *
-	 * @return all of the PackExt values.
-	 */
-	public static PackExt[] values() {
-		return VALUES;
-	}
-
-	/**
-	 * Returns a PackExt for the file extension and registers it in the values
-	 * array.
-	 *
-	 * @param ext
-	 *            the file extension.
-	 * @return the PackExt for the ext
-	 */
-	public static synchronized PackExt newPackExt(String ext) {
-		PackExt[] dst = new PackExt[VALUES.length + 1];
-		for (int i = 0; i < VALUES.length; i++) {
-			PackExt packExt = VALUES[i];
-			if (packExt.getExtension().equals(ext))
-				return packExt;
-			dst[i] = packExt;
-		}
-		if (VALUES.length >= 32)
-			throw new IllegalStateException(
-					"maximum number of pack extensions exceeded"); //$NON-NLS-1$
-
-		PackExt value = new PackExt(ext, VALUES.length);
-		dst[VALUES.length] = value;
-		VALUES = dst;
-		return value;
-	}
+	REFTABLE("ref"); //$NON-NLS-1$
 
 	private final String ext;
 
-	private final int pos;
-
-	private PackExt(String ext, int pos) {
+	private PackExt(String ext) {
 		this.ext = ext;
-		this.pos = pos;
 	}
 
 	/**
@@ -85,12 +45,12 @@
 	}
 
 	/**
-	 * Get the position of the extension in the values array.
+	 * Get the position of the extension in the enum declaration.
 	 *
-	 * @return the position of the extension in the values array.
+	 * @return the position of the extension in the enum declaration.
 	 */
 	public int getPosition() {
-		return pos;
+		return ordinal();
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java
index a78f4d2..e210acf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java
@@ -11,6 +11,7 @@
 package org.eclipse.jgit.internal.storage.reftable;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.PriorityQueue;
 
@@ -215,6 +216,23 @@
 			}
 		}
 
+		@Override
+		public void seekPastPrefix(String prefixName) throws IOException {
+			List<RefQueueEntry> entriesToAdd = new ArrayList<>();
+			entriesToAdd.addAll(queue);
+			if (head != null) {
+				entriesToAdd.add(head);
+			}
+
+			head = null;
+			queue.clear();
+
+			for(RefQueueEntry entry : entriesToAdd){
+				entry.rc.seekPastPrefix(prefixName);
+				add(entry);
+			}
+		}
+
 		private RefQueueEntry poll() {
 			RefQueueEntry e = head;
 			if (e != null) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/RefCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/RefCursor.java
index d96648e..5e2c350 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/RefCursor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/RefCursor.java
@@ -29,6 +29,19 @@
 	public abstract boolean next() throws IOException;
 
 	/**
+	 * Seeks forward to the first ref record lexicographically beyond
+	 * {@code prefixName} that doesn't start with {@code prefixName}. If there are
+	 * no more results, skipping some refs won't add new results. E.g if we create a
+	 * RefCursor that returns only results with a specific prefix, skipping that
+	 * prefix won't give results that are not part of the original prefix.
+	 *
+	 * @param prefixName prefix that should be skipped. All previous refs before it
+	 *                   will be skipped.
+	 * @throws java.io.IOException references cannot be read.
+	 */
+	public abstract void seekPastPrefix(String prefixName) throws IOException;
+
+	/**
 	 * Get reference at the current position.
 	 *
 	 * @return reference at the current position.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java
index 4747be3..0c16828 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java
@@ -14,10 +14,12 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.lib.ObjectId;
@@ -266,6 +268,54 @@
 	}
 
 	/**
+	 * Returns refs whose names start with a given prefix excluding all refs that
+	 * start with one of the given prefixes.
+	 *
+	 * @param include string that names of refs should start with; may be empty.
+	 * @param excludes strings that names of refs can't start with; may be empty.
+	 * @return immutable list of refs whose names start with {@code include} and
+	 *         none of the strings in {@code exclude}.
+	 * @throws java.io.IOException the reference space cannot be accessed.
+	 */
+	public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes) throws IOException {
+		if (excludes.isEmpty()) {
+			return getRefsByPrefix(include);
+		}
+		List<Ref> results = new ArrayList<>();
+		lock.lock();
+		try {
+			Reftable table = reader();
+			Iterator<String> excludeIterator =
+					excludes.stream().sorted().collect(Collectors.toList()).iterator();
+			String currentExclusion = excludeIterator.hasNext() ? excludeIterator.next() : null;
+			try (RefCursor rc = RefDatabase.ALL.equals(include) ? table.allRefs() : table.seekRefsWithPrefix(include)) {
+				while (rc.next()) {
+					Ref ref = table.resolve(rc.getRef());
+					if (ref == null || ref.getObjectId() == null) {
+						continue;
+					}
+					// Skip prefixes that will never see since we are already further than those
+					// prefixes lexicographically.
+					while (excludeIterator.hasNext() && !ref.getName().startsWith(currentExclusion)
+							&& ref.getName().compareTo(currentExclusion) > 0) {
+						currentExclusion = excludeIterator.next();
+					}
+
+					if (currentExclusion != null && ref.getName().startsWith(currentExclusion)) {
+						rc.seekPastPrefix(currentExclusion);
+						continue;
+					}
+					results.add(ref);
+				}
+			}
+		} finally {
+			lock.unlock();
+		}
+
+		return Collections.unmodifiableList(results);
+	}
+
+	/**
 	 * @return whether there is a fast SHA1 to ref map.
 	 * @throws IOException in case of I/O problems.
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java
index 095276f..9e2ae91 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java
@@ -509,6 +509,21 @@
 		}
 
 		@Override
+		public void seekPastPrefix(String prefixName) throws IOException {
+			initRefIndex();
+			byte[] key = prefixName.getBytes(UTF_8);
+			ByteBuffer byteBuffer = ByteBuffer.allocate(key.length + 1);
+			byteBuffer.put(key);
+			// Add the representation of the last byte lexicographically. Based on how UTF_8
+			// representation works, this byte will be bigger lexicographically than any
+			// UTF_8 character when translated into bytes, since 0xFF can never be a part of
+			// a UTF_8 string.
+			byteBuffer.put((byte) 0xFF);
+
+			block = seek(REF_BLOCK_TYPE, byteBuffer.array(), refIndex, 0, refEnd);
+		}
+
+		@Override
 		public Ref getRef() {
 			return ref;
 		}
@@ -682,6 +697,17 @@
 		}
 
 		@Override
+		/**
+		 * The implementation here would not be efficient complexity-wise since it
+		 * expected that there are a small number of refs that match the same object id.
+		 * In such case it's better to not even use this method (as the caller might
+		 * expect it to be efficient).
+		 */
+		public void seekPastPrefix(String prefixName) throws IOException {
+		    throw new UnsupportedOperationException();
+		}
+
+		@Override
 		public Ref getRef() {
 			return ref;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java
deleted file mode 100644
index 5138636..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import java.io.IOException;
-
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-
-/** Update that always rejects with {@code LOCK_FAILURE}. */
-class AlwaysFailUpdate extends RefUpdate {
-	private final RefTreeDatabase refdb;
-
-	AlwaysFailUpdate(RefTreeDatabase refdb, String name) {
-		super(new ObjectIdRef.Unpeeled(Ref.Storage.NEW, name, null));
-		this.refdb = refdb;
-		setCheckConflicting(false);
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected RefDatabase getRefDatabase() {
-		return refdb;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected Repository getRepository() {
-		return refdb.getRepository();
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected boolean tryLock(boolean deref) throws IOException {
-		return false;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected void unlock() {
-		// No locks are held here.
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected Result doUpdate(Result desiredResult) {
-		return Result.LOCK_FAILURE;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected Result doDelete(Result desiredResult) {
-		return Result.LOCK_FAILURE;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected Result doLink(String target) {
-		return Result.LOCK_FAILURE;
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java
deleted file mode 100644
index bb06a9e..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java
+++ /dev/null
@@ -1,309 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import static org.eclipse.jgit.lib.Constants.encode;
-import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
-import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
-import static org.eclipse.jgit.lib.Ref.Storage.NETWORK;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
-
-import java.io.IOException;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceiveCommand.Result;
-
-/**
- * Command to create, update or delete an entry inside a
- * {@link org.eclipse.jgit.internal.storage.reftree.RefTree}.
- * <p>
- * Unlike {@link org.eclipse.jgit.transport.ReceiveCommand} (which can only
- * update a reference to an {@link org.eclipse.jgit.lib.ObjectId}), a RefTree
- * Command can also create, modify or delete symbolic references to a target
- * reference.
- * <p>
- * RefTree Commands may wrap a {@code ReceiveCommand} to allow callers to
- * process an existing ReceiveCommand against a RefTree.
- * <p>
- * Commands should be passed into
- * {@link org.eclipse.jgit.internal.storage.reftree.RefTree#apply(java.util.Collection)}
- * for processing.
- */
-public class Command {
-	/**
-	 * Set unprocessed commands as failed due to transaction aborted.
-	 * <p>
-	 * If a command is still
-	 * {@link org.eclipse.jgit.transport.ReceiveCommand.Result#NOT_ATTEMPTED} it
-	 * will be set to
-	 * {@link org.eclipse.jgit.transport.ReceiveCommand.Result#REJECTED_OTHER_REASON}.
-	 * If {@code why} is non-null its contents will be used as the message for
-	 * the first command status.
-	 *
-	 * @param commands
-	 *            commands to mark as failed.
-	 * @param why
-	 *            optional message to set on the first aborted command.
-	 */
-	public static void abort(Iterable<Command> commands, @Nullable String why) {
-		if (why == null || why.isEmpty()) {
-			why = JGitText.get().transactionAborted;
-		}
-		for (Command c : commands) {
-			if (c.getResult() == NOT_ATTEMPTED) {
-				c.setResult(REJECTED_OTHER_REASON, why);
-				why = JGitText.get().transactionAborted;
-			}
-		}
-	}
-
-	private final Ref oldRef;
-	private final Ref newRef;
-	private final ReceiveCommand cmd;
-	private Result result;
-
-	/**
-	 * Create a command to create, update or delete a reference.
-	 * <p>
-	 * At least one of {@code oldRef} or {@code newRef} must be supplied.
-	 *
-	 * @param oldRef
-	 *            expected value. Null if the ref should not exist.
-	 * @param newRef
-	 *            desired value, must be peeled if not null and not symbolic.
-	 *            Null to delete the ref.
-	 */
-	public Command(@Nullable Ref oldRef, @Nullable Ref newRef) {
-		this.oldRef = oldRef;
-		this.newRef = newRef;
-		this.cmd = null;
-		this.result = NOT_ATTEMPTED;
-
-		if (oldRef == null && newRef == null) {
-			throw new IllegalArgumentException();
-		}
-		if (newRef != null && !newRef.isPeeled() && !newRef.isSymbolic()) {
-			throw new IllegalArgumentException();
-		}
-		if (oldRef != null && newRef != null
-				&& !oldRef.getName().equals(newRef.getName())) {
-			throw new IllegalArgumentException();
-		}
-	}
-
-	/**
-	 * Construct a RefTree command wrapped around a ReceiveCommand.
-	 *
-	 * @param rw
-	 *            walk instance to peel the {@code newId}.
-	 * @param cmd
-	 *            command received from a push client.
-	 * @throws org.eclipse.jgit.errors.MissingObjectException
-	 *             {@code oldId} or {@code newId} is missing.
-	 * @throws java.io.IOException
-	 *             {@code oldId} or {@code newId} cannot be peeled.
-	 */
-	public Command(RevWalk rw, ReceiveCommand cmd)
-			throws MissingObjectException, IOException {
-		this.oldRef = toRef(rw, cmd.getOldId(), cmd.getOldSymref(),
-				cmd.getRefName(), false);
-		this.newRef = toRef(rw, cmd.getNewId(), cmd.getNewSymref(),
-				cmd.getRefName(), true);
-		this.cmd = cmd;
-	}
-
-	static Ref toRef(RevWalk rw, ObjectId id, @Nullable String target,
-			String name, boolean mustExist)
-			throws MissingObjectException, IOException {
-		if (target != null) {
-			return new SymbolicRef(name,
-					new ObjectIdRef.Unpeeled(NETWORK, target, id));
-		} else if (ObjectId.zeroId().equals(id)) {
-			return null;
-		}
-
-		try {
-			RevObject o = rw.parseAny(id);
-			if (o instanceof RevTag) {
-				RevObject p = rw.peel(o);
-				return new ObjectIdRef.PeeledTag(NETWORK, name, id, p.copy());
-			}
-			return new ObjectIdRef.PeeledNonTag(NETWORK, name, id);
-		} catch (MissingObjectException e) {
-			if (mustExist) {
-				throw e;
-			}
-			return new ObjectIdRef.Unpeeled(NETWORK, name, id);
-		}
-	}
-
-	/**
-	 * Get name of the reference affected by this command.
-	 *
-	 * @return name of the reference affected by this command.
-	 */
-	public String getRefName() {
-		if (cmd != null) {
-			return cmd.getRefName();
-		} else if (newRef != null) {
-			return newRef.getName();
-		}
-		return oldRef.getName();
-	}
-
-	/**
-	 * Set the result of this command.
-	 *
-	 * @param result
-	 *            the command result.
-	 */
-	public void setResult(Result result) {
-		setResult(result, null);
-	}
-
-	/**
-	 * Set the result of this command.
-	 *
-	 * @param result
-	 *            the command result.
-	 * @param why
-	 *            optional message explaining the result status.
-	 */
-	public void setResult(Result result, @Nullable String why) {
-		if (cmd != null) {
-			cmd.setResult(result, why);
-		} else {
-			this.result = result;
-		}
-	}
-
-	/**
-	 * Get result of executing this command.
-	 *
-	 * @return result of executing this command.
-	 */
-	public Result getResult() {
-		return cmd != null ? cmd.getResult() : result;
-	}
-
-	/**
-	 * Get optional message explaining command failure.
-	 *
-	 * @return optional message explaining command failure.
-	 */
-	@Nullable
-	public String getMessage() {
-		return cmd != null ? cmd.getMessage() : null;
-	}
-
-	/**
-	 * Old peeled reference.
-	 *
-	 * @return the old reference; null if the command is creating the reference.
-	 */
-	@Nullable
-	public Ref getOldRef() {
-		return oldRef;
-	}
-
-	/**
-	 * New peeled reference.
-	 *
-	 * @return the new reference; null if the command is deleting the reference.
-	 */
-	@Nullable
-	public Ref getNewRef() {
-		return newRef;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public String toString() {
-		StringBuilder s = new StringBuilder();
-		append(s, oldRef, "CREATE"); //$NON-NLS-1$
-		s.append(' ');
-		append(s, newRef, "DELETE"); //$NON-NLS-1$
-		s.append(' ').append(getRefName());
-		s.append(' ').append(getResult());
-		if (getMessage() != null) {
-			s.append(' ').append(getMessage());
-		}
-		return s.toString();
-	}
-
-	private static void append(StringBuilder s, Ref r, String nullName) {
-		if (r == null) {
-			s.append(nullName);
-		} else if (r.isSymbolic()) {
-			s.append(r.getTarget().getName());
-		} else {
-			ObjectId id = r.getObjectId();
-			if (id != null) {
-				s.append(id.name());
-			}
-		}
-	}
-
-	/**
-	 * Check the entry is consistent with either the old or the new ref.
-	 *
-	 * @param entry
-	 *            current entry; null if the entry does not exist.
-	 * @return true if entry matches {@link #getOldRef()} or
-	 *         {@link #getNewRef()}; otherwise false.
-	 */
-	boolean checkRef(@Nullable DirCacheEntry entry) {
-		if (entry != null && entry.getRawMode() == 0) {
-			entry = null;
-		}
-		return check(entry, oldRef) || check(entry, newRef);
-	}
-
-	private static boolean check(@Nullable DirCacheEntry cur,
-			@Nullable Ref exp) {
-		if (cur == null) {
-			// Does not exist, ok if oldRef does not exist.
-			return exp == null;
-		} else if (exp == null) {
-			// Expected to not exist, but currently exists, fail.
-			return false;
-		}
-
-		if (exp.isSymbolic()) {
-			String dst = exp.getTarget().getName();
-			return cur.getRawMode() == TYPE_SYMLINK
-					&& cur.getObjectId().equals(symref(dst));
-		}
-
-		return cur.getRawMode() == TYPE_GITLINK
-				&& cur.getObjectId().equals(exp.getObjectId());
-	}
-
-	static ObjectId symref(String s) {
-		@SuppressWarnings("resource")
-		ObjectInserter.Formatter fmt = new ObjectInserter.Formatter();
-		return fmt.idFor(OBJ_BLOB, encode(s));
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java
deleted file mode 100644
index 6f12e9c..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java
+++ /dev/null
@@ -1,384 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import static org.eclipse.jgit.lib.Constants.R_REFS;
-import static org.eclipse.jgit.lib.Constants.encode;
-import static org.eclipse.jgit.lib.FileMode.GITLINK;
-import static org.eclipse.jgit.lib.FileMode.SYMLINK;
-import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
-import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
-import static org.eclipse.jgit.lib.Ref.Storage.NEW;
-import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
-import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.dircache.DirCache;
-import org.eclipse.jgit.dircache.DirCacheBuilder;
-import org.eclipse.jgit.dircache.DirCacheEditor;
-import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
-import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
-import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.DirCacheNameConflictException;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.util.RawParseUtils;
-
-/**
- * Tree of references in the reference graph.
- * <p>
- * The root corresponds to the {@code "refs/"} subdirectory, for example the
- * default reference {@code "refs/heads/master"} is stored at path
- * {@code "heads/master"} in a {@code RefTree}.
- * <p>
- * Normal references are stored as {@link org.eclipse.jgit.lib.FileMode#GITLINK}
- * tree entries. The ObjectId in the tree entry is the ObjectId the reference
- * refers to.
- * <p>
- * Symbolic references are stored as
- * {@link org.eclipse.jgit.lib.FileMode#SYMLINK} entries, with the blob storing
- * the name of the target reference.
- * <p>
- * Annotated tags also store the peeled object using a {@code GITLINK} entry
- * with the suffix <code>" ^"</code> (space carrot), for example
- * {@code "tags/v1.0"} stores the annotated tag object, while
- * <code>"tags/v1.0 ^"</code> stores the commit the tag annotates.
- * <p>
- * {@code HEAD} is a special case and stored as {@code "..HEAD"}.
- */
-public class RefTree {
-	/** Suffix applied to GITLINK to indicate its the peeled value of a tag. */
-	public static final String PEELED_SUFFIX = " ^"; //$NON-NLS-1$
-	static final String ROOT_DOTDOT = ".."; //$NON-NLS-1$
-
-	/**
-	 * Create an empty reference tree.
-	 *
-	 * @return a new empty reference tree.
-	 */
-	public static RefTree newEmptyTree() {
-		return new RefTree(DirCache.newInCore());
-	}
-
-	/**
-	 * Load a reference tree.
-	 *
-	 * @param reader
-	 *            reader to scan the reference tree with.
-	 * @param tree
-	 *            the tree to read.
-	 * @return the ref tree read from the commit.
-	 * @throws java.io.IOException
-	 *             the repository cannot be accessed through the reader.
-	 * @throws org.eclipse.jgit.errors.CorruptObjectException
-	 *             a tree object is corrupt and cannot be read.
-	 * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
-	 *             a tree object wasn't actually a tree.
-	 * @throws org.eclipse.jgit.errors.MissingObjectException
-	 *             a reference tree object doesn't exist.
-	 */
-	public static RefTree read(ObjectReader reader, RevTree tree)
-			throws MissingObjectException, IncorrectObjectTypeException,
-			CorruptObjectException, IOException {
-		return new RefTree(DirCache.read(reader, tree));
-	}
-
-	private DirCache contents;
-	private Map<ObjectId, String> pendingBlobs;
-
-	private RefTree(DirCache dc) {
-		this.contents = dc;
-	}
-
-	/**
-	 * Read one reference.
-	 * <p>
-	 * References are always returned peeled
-	 * ({@link org.eclipse.jgit.lib.Ref#isPeeled()} is true). If the reference
-	 * points to an annotated tag, the returned reference will be peeled and
-	 * contain {@link org.eclipse.jgit.lib.Ref#getPeeledObjectId()}.
-	 * <p>
-	 * If the reference is a symbolic reference and the chain depth is less than
-	 * {@link org.eclipse.jgit.lib.RefDatabase#MAX_SYMBOLIC_REF_DEPTH} the
-	 * returned reference is resolved. If the chain depth is longer, the
-	 * symbolic reference is returned without resolving.
-	 *
-	 * @param reader
-	 *            to access objects necessary to read the requested reference.
-	 * @param name
-	 *            name of the reference to read.
-	 * @return the reference; null if it does not exist.
-	 * @throws java.io.IOException
-	 *             cannot read a symbolic reference target.
-	 */
-	@Nullable
-	public Ref exactRef(ObjectReader reader, String name) throws IOException {
-		Ref r = readRef(reader, name);
-		if (r == null) {
-			return null;
-		} else if (r.isSymbolic()) {
-			return resolve(reader, r, 0);
-		}
-
-		DirCacheEntry p = contents.getEntry(peeledPath(name));
-		if (p != null && p.getRawMode() == TYPE_GITLINK) {
-			return new ObjectIdRef.PeeledTag(PACKED, r.getName(),
-					r.getObjectId(), p.getObjectId());
-		}
-		return r;
-	}
-
-	private Ref readRef(ObjectReader reader, String name) throws IOException {
-		DirCacheEntry e = contents.getEntry(refPath(name));
-		return e != null ? toRef(reader, e, name) : null;
-	}
-
-	private Ref toRef(ObjectReader reader, DirCacheEntry e, String name)
-			throws IOException {
-		int mode = e.getRawMode();
-		if (mode == TYPE_GITLINK) {
-			ObjectId id = e.getObjectId();
-			return new ObjectIdRef.PeeledNonTag(PACKED, name, id);
-		}
-
-		if (mode == TYPE_SYMLINK) {
-			ObjectId id = e.getObjectId();
-			String n = pendingBlobs != null ? pendingBlobs.get(id) : null;
-			if (n == null) {
-				byte[] bin = reader.open(id, OBJ_BLOB).getCachedBytes();
-				n = RawParseUtils.decode(bin);
-			}
-			Ref dst = new ObjectIdRef.Unpeeled(NEW, n, null);
-			return new SymbolicRef(name, dst);
-		}
-
-		return null; // garbage file or something; not a reference.
-	}
-
-	private Ref resolve(ObjectReader reader, Ref ref, int depth)
-			throws IOException {
-		if (ref.isSymbolic() && depth < MAX_SYMBOLIC_REF_DEPTH) {
-			Ref r = readRef(reader, ref.getTarget().getName());
-			if (r == null) {
-				return ref;
-			}
-			Ref dst = resolve(reader, r, depth + 1);
-			return new SymbolicRef(ref.getName(), dst);
-		}
-		return ref;
-	}
-
-	/**
-	 * Attempt a batch of commands against this RefTree.
-	 * <p>
-	 * The batch is applied atomically, either all commands apply at once, or
-	 * they all reject and the RefTree is left unmodified.
-	 * <p>
-	 * On success (when this method returns {@code true}) the command results
-	 * are left as-is (probably {@code NOT_ATTEMPTED}). Result fields are set
-	 * only when this method returns {@code false} to indicate failure.
-	 *
-	 * @param cmdList
-	 *            to apply. All commands should still have result NOT_ATTEMPTED.
-	 * @return true if the commands applied; false if they were rejected.
-	 */
-	public boolean apply(Collection<Command> cmdList) {
-		try {
-			DirCacheEditor ed = contents.editor();
-			for (Command cmd : cmdList) {
-				if (!isValidRef(cmd)) {
-					cmd.setResult(REJECTED_OTHER_REASON,
-							JGitText.get().funnyRefname);
-					Command.abort(cmdList, null);
-					return false;
-				}
-				apply(ed, cmd);
-			}
-			ed.finish();
-			return true;
-		} catch (DirCacheNameConflictException e) {
-			String r1 = refName(e.getPath1());
-			String r2 = refName(e.getPath2());
-			for (Command cmd : cmdList) {
-				if (r1.equals(cmd.getRefName())
-						|| r2.equals(cmd.getRefName())) {
-					cmd.setResult(LOCK_FAILURE);
-					break;
-				}
-			}
-			Command.abort(cmdList, null);
-			return false;
-		} catch (LockFailureException e) {
-			Command.abort(cmdList, null);
-			return false;
-		}
-	}
-
-	private static boolean isValidRef(Command cmd) {
-		String n = cmd.getRefName();
-		return HEAD.equals(n) || Repository.isValidRefName(n);
-	}
-
-	private void apply(DirCacheEditor ed, Command cmd) {
-		String path = refPath(cmd.getRefName());
-		Ref oldRef = cmd.getOldRef();
-		final Ref newRef = cmd.getNewRef();
-
-		if (newRef == null) {
-			checkRef(contents.getEntry(path), cmd);
-			ed.add(new DeletePath(path));
-			cleanupPeeledRef(ed, oldRef);
-			return;
-		}
-
-		if (newRef.isSymbolic()) {
-			final String dst = newRef.getTarget().getName();
-			ed.add(new PathEdit(path) {
-				@Override
-				public void apply(DirCacheEntry ent) {
-					checkRef(ent, cmd);
-					ObjectId id = Command.symref(dst);
-					ent.setFileMode(SYMLINK);
-					ent.setObjectId(id);
-					if (pendingBlobs == null) {
-						pendingBlobs = new HashMap<>(4);
-					}
-					pendingBlobs.put(id, dst);
-				}
-			}.setReplace(false));
-			cleanupPeeledRef(ed, oldRef);
-			return;
-		}
-
-		ed.add(new PathEdit(path) {
-			@Override
-			public void apply(DirCacheEntry ent) {
-				checkRef(ent, cmd);
-				ent.setFileMode(GITLINK);
-				ent.setObjectId(newRef.getObjectId());
-			}
-		}.setReplace(false));
-
-		if (newRef.getPeeledObjectId() != null) {
-			ed.add(new PathEdit(peeledPath(newRef.getName())) {
-				@Override
-				public void apply(DirCacheEntry ent) {
-					ent.setFileMode(GITLINK);
-					ent.setObjectId(newRef.getPeeledObjectId());
-				}
-			}.setReplace(false));
-		} else {
-			cleanupPeeledRef(ed, oldRef);
-		}
-	}
-
-	private static void checkRef(@Nullable DirCacheEntry ent, Command cmd) {
-		if (!cmd.checkRef(ent)) {
-			cmd.setResult(LOCK_FAILURE);
-			throw new LockFailureException();
-		}
-	}
-
-	private static void cleanupPeeledRef(DirCacheEditor ed, Ref ref) {
-		if (ref != null && !ref.isSymbolic()
-				&& (!ref.isPeeled() || ref.getPeeledObjectId() != null)) {
-			ed.add(new DeletePath(peeledPath(ref.getName())));
-		}
-	}
-
-	/**
-	 * Convert a path name in a RefTree to the reference name known by Git.
-	 *
-	 * @param path
-	 *            name read from the RefTree structure, for example
-	 *            {@code "heads/master"}.
-	 * @return reference name for the path, {@code "refs/heads/master"}.
-	 */
-	public static String refName(String path) {
-		if (path.startsWith(ROOT_DOTDOT)) {
-			return path.substring(2);
-		}
-		return R_REFS + path;
-	}
-
-	static String refPath(String name) {
-		if (name.startsWith(R_REFS)) {
-			return name.substring(R_REFS.length());
-		}
-		return ROOT_DOTDOT + name;
-	}
-
-	private static String peeledPath(String name) {
-		return refPath(name) + PEELED_SUFFIX;
-	}
-
-	/**
-	 * Write this reference tree.
-	 *
-	 * @param inserter
-	 *            inserter to use when writing trees to the object database.
-	 *            Caller is responsible for flushing the inserter before trying
-	 *            to read the objects, or exposing them through a reference.
-	 * @return the top level tree.
-	 * @throws java.io.IOException
-	 *             a tree could not be written.
-	 */
-	public ObjectId writeTree(ObjectInserter inserter) throws IOException {
-		if (pendingBlobs != null) {
-			for (String s : pendingBlobs.values()) {
-				inserter.insert(OBJ_BLOB, encode(s));
-			}
-			pendingBlobs = null;
-		}
-		return contents.writeTree(inserter);
-	}
-
-	/**
-	 * Create a deep copy of this RefTree.
-	 *
-	 * @return a deep copy of this RefTree.
-	 */
-	public RefTree copy() {
-		RefTree r = new RefTree(DirCache.newInCore());
-		DirCacheBuilder b = r.contents.builder();
-		for (int i = 0; i < contents.getEntryCount(); i++) {
-			b.add(new DirCacheEntry(contents.getEntry(i)));
-		}
-		b.finish();
-		if (pendingBlobs != null) {
-			r.pendingBlobs = new HashMap<>(pendingBlobs);
-		}
-		return r;
-	}
-
-	private static class LockFailureException extends RuntimeException {
-		private static final long serialVersionUID = 1L;
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java
deleted file mode 100644
index b154b95..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
-import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE;
-import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
-
-import java.io.IOException;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-/** Batch update a {@link RefTreeDatabase}. */
-class RefTreeBatch extends BatchRefUpdate {
-	private final RefTreeDatabase refdb;
-	private Ref src;
-	private ObjectId parentCommitId;
-	private ObjectId parentTreeId;
-	private RefTree tree;
-	private PersonIdent author;
-	private ObjectId newCommitId;
-
-	RefTreeBatch(RefTreeDatabase refdb) {
-		super(refdb);
-		this.refdb = refdb;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public void execute(RevWalk rw, ProgressMonitor monitor)
-			throws IOException {
-		List<Command> todo = new ArrayList<>(getCommands().size());
-		for (ReceiveCommand c : getCommands()) {
-			if (!isAllowNonFastForwards()) {
-				if (c.getType() == UPDATE) {
-					c.updateType(rw);
-				}
-				if (c.getType() == UPDATE_NONFASTFORWARD) {
-					c.setResult(REJECTED_NONFASTFORWARD);
-					if (isAtomic()) {
-						ReceiveCommand.abort(getCommands());
-						return;
-					}
-					continue;
-				}
-			}
-			todo.add(new Command(rw, c));
-		}
-		init(rw);
-		execute(rw, todo);
-	}
-
-	void init(RevWalk rw) throws IOException {
-		src = refdb.getBootstrap().exactRef(refdb.getTxnCommitted());
-		if (src != null && src.getObjectId() != null) {
-			RevCommit c = rw.parseCommit(src.getObjectId());
-			parentCommitId = c;
-			parentTreeId = c.getTree();
-			tree = RefTree.read(rw.getObjectReader(), c.getTree());
-		} else {
-			parentCommitId = ObjectId.zeroId();
-			try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
-				parentTreeId = fmt.idFor(OBJ_TREE, new byte[] {});
-			}
-			tree = RefTree.newEmptyTree();
-		}
-	}
-
-	@Nullable
-	Ref exactRef(ObjectReader reader, String name) throws IOException {
-		return tree.exactRef(reader, name);
-	}
-
-	/**
-	 * Execute an update from {@link RefTreeUpdate} or {@link RefTreeRename}.
-	 *
-	 * @param rw
-	 *            current RevWalk handling the update or rename.
-	 * @param todo
-	 *            commands to execute. Must never be a bootstrap reference name.
-	 * @throws IOException
-	 *             the storage system is unable to read or write data.
-	 */
-	void execute(RevWalk rw, List<Command> todo) throws IOException {
-		for (Command c : todo) {
-			if (c.getResult() != NOT_ATTEMPTED) {
-				Command.abort(todo, null);
-				return;
-			}
-			if (refdb.conflictsWithBootstrap(c.getRefName())) {
-				c.setResult(REJECTED_OTHER_REASON, MessageFormat
-						.format(JGitText.get().invalidRefName, c.getRefName()));
-				Command.abort(todo, null);
-				return;
-			}
-		}
-
-		if (apply(todo) && newCommitId != null) {
-			commit(rw, todo);
-		}
-	}
-
-	private boolean apply(List<Command> todo) throws IOException {
-		if (!tree.apply(todo)) {
-			// apply set rejection information on commands.
-			return false;
-		}
-
-		Repository repo = refdb.getRepository();
-		try (ObjectInserter ins = repo.newObjectInserter()) {
-			CommitBuilder b = new CommitBuilder();
-			b.setTreeId(tree.writeTree(ins));
-			if (parentTreeId.equals(b.getTreeId())) {
-				for (Command c : todo) {
-					c.setResult(OK);
-				}
-				return true;
-			}
-			if (!parentCommitId.equals(ObjectId.zeroId())) {
-				b.setParentId(parentCommitId);
-			}
-
-			author = getRefLogIdent();
-			if (author == null) {
-				author = new PersonIdent(repo);
-			}
-			b.setAuthor(author);
-			b.setCommitter(author);
-			b.setMessage(getRefLogMessage());
-			newCommitId = ins.insert(b);
-			ins.flush();
-		}
-		return true;
-	}
-
-	private void commit(RevWalk rw, List<Command> todo) throws IOException {
-		ReceiveCommand commit = new ReceiveCommand(
-				parentCommitId, newCommitId,
-				refdb.getTxnCommitted());
-		updateBootstrap(rw, commit);
-
-		if (commit.getResult() == OK) {
-			for (Command c : todo) {
-				c.setResult(OK);
-			}
-		} else {
-			Command.abort(todo, commit.getResult().name());
-		}
-	}
-
-	private void updateBootstrap(RevWalk rw, ReceiveCommand commit)
-			throws IOException {
-		BatchRefUpdate u = refdb.getBootstrap().newBatchUpdate();
-		u.setAllowNonFastForwards(true);
-		u.setPushCertificate(getPushCertificate());
-		if (isRefLogDisabled()) {
-			u.disableRefLog();
-		} else {
-			u.setRefLogIdent(author);
-			u.setRefLogMessage(getRefLogMessage(), false);
-		}
-		u.addCommand(commit);
-		u.execute(rw, NullProgressMonitor.INSTANCE);
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java
deleted file mode 100644
index 34b8f2c..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
-import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Ref.Storage;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.RefRename;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.RefList;
-import org.eclipse.jgit.util.RefMap;
-
-/**
- * Reference database backed by a
- * {@link org.eclipse.jgit.internal.storage.reftree.RefTree}.
- * <p>
- * The storage for RefTreeDatabase has two parts. The main part is a native Git
- * tree object stored under the {@code refs/txn} namespace. To avoid cycles,
- * references to {@code refs/txn} are not stored in that tree object, but
- * instead in a "bootstrap" layer, which is a separate
- * {@link org.eclipse.jgit.lib.RefDatabase} such as
- * {@link org.eclipse.jgit.internal.storage.file.RefDirectory} using local
- * reference files inside of {@code $GIT_DIR/refs}.
- */
-public class RefTreeDatabase extends RefDatabase {
-	private final Repository repo;
-	private final RefDatabase bootstrap;
-	private final String txnCommitted;
-
-	@Nullable
-	private final String txnNamespace;
-	private volatile Scanner.Result refs;
-
-	/**
-	 * Create a RefTreeDb for a repository.
-	 *
-	 * @param repo
-	 *            the repository using references in this database.
-	 * @param bootstrap
-	 *            bootstrap reference database storing the references that
-	 *            anchor the
-	 *            {@link org.eclipse.jgit.internal.storage.reftree.RefTree}.
-	 */
-	public RefTreeDatabase(Repository repo, RefDatabase bootstrap) {
-		Config cfg = repo.getConfig();
-		String committed = cfg.getString("reftree", null, "committedRef"); //$NON-NLS-1$ //$NON-NLS-2$
-		if (committed == null || committed.isEmpty()) {
-			committed = "refs/txn/committed"; //$NON-NLS-1$
-		}
-
-		this.repo = repo;
-		this.bootstrap = bootstrap;
-		this.txnNamespace = initNamespace(committed);
-		this.txnCommitted = committed;
-	}
-
-	/**
-	 * Create a RefTreeDb for a repository.
-	 *
-	 * @param repo
-	 *            the repository using references in this database.
-	 * @param bootstrap
-	 *            bootstrap reference database storing the references that
-	 *            anchor the
-	 *            {@link org.eclipse.jgit.internal.storage.reftree.RefTree}.
-	 * @param txnCommitted
-	 *            name of the bootstrap reference holding the committed RefTree.
-	 */
-	public RefTreeDatabase(Repository repo, RefDatabase bootstrap,
-			String txnCommitted) {
-		this.repo = repo;
-		this.bootstrap = bootstrap;
-		this.txnNamespace = initNamespace(txnCommitted);
-		this.txnCommitted = txnCommitted;
-	}
-
-	private static String initNamespace(String committed) {
-		int s = committed.lastIndexOf('/');
-		if (s < 0) {
-			return null;
-		}
-		return committed.substring(0, s + 1); // Keep trailing '/'.
-	}
-
-	Repository getRepository() {
-		return repo;
-	}
-
-	/**
-	 * Get the bootstrap reference database
-	 *
-	 * @return the bootstrap reference database, which must be used to access
-	 *         {@link #getTxnCommitted()}, {@link #getTxnNamespace()}.
-	 */
-	public RefDatabase getBootstrap() {
-		return bootstrap;
-	}
-
-	/**
-	 * Get name of bootstrap reference anchoring committed RefTree.
-	 *
-	 * @return name of bootstrap reference anchoring committed RefTree.
-	 */
-	public String getTxnCommitted() {
-		return txnCommitted;
-	}
-
-	/**
-	 * Get namespace used by bootstrap layer.
-	 *
-	 * @return namespace used by bootstrap layer, e.g. {@code refs/txn/}. Always
-	 *         ends in {@code '/'}.
-	 */
-	@Nullable
-	public String getTxnNamespace() {
-		return txnNamespace;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public void create() throws IOException {
-		bootstrap.create();
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public boolean performsAtomicTransactions() {
-		return true;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public void refresh() {
-		bootstrap.refresh();
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public void close() {
-		refs = null;
-		bootstrap.close();
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public Ref exactRef(String name) throws IOException {
-		if (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) {
-			// Pass through names like MERGE_HEAD, ORIG_HEAD, FETCH_HEAD.
-			return bootstrap.exactRef(name);
-		} else if (conflictsWithBootstrap(name)) {
-			return null;
-		}
-
-		boolean partial = false;
-		Ref src = bootstrap.exactRef(txnCommitted);
-		Scanner.Result c = refs;
-		if (c == null || !c.refTreeId.equals(idOf(src))) {
-			c = Scanner.scanRefTree(repo, src, prefixOf(name), false);
-			partial = true;
-		}
-
-		Ref r = c.all.get(name);
-		if (r != null && r.isSymbolic()) {
-			r = c.sym.get(name);
-			if (partial && r.getObjectId() == null) {
-				// Attempting exactRef("HEAD") with partial scan will leave
-				// an unresolved symref as its target e.g. refs/heads/master
-				// was not read by the partial scan. Scan everything instead.
-				return getRefs(ALL).get(name);
-			}
-		}
-		return r;
-	}
-
-	private static String prefixOf(String name) {
-		int s = name.lastIndexOf('/');
-		if (s >= 0) {
-			return name.substring(0, s);
-		}
-		return ""; //$NON-NLS-1$
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public Map<String, Ref> getRefs(String prefix) throws IOException {
-		if (!prefix.isEmpty() && prefix.charAt(prefix.length() - 1) != '/') {
-			return new HashMap<>(0);
-		}
-
-		Ref src = bootstrap.exactRef(txnCommitted);
-		Scanner.Result c = refs;
-		if (c == null || !c.refTreeId.equals(idOf(src))) {
-			c = Scanner.scanRefTree(repo, src, prefix, true);
-			if (prefix.isEmpty()) {
-				refs = c;
-			}
-		}
-		return new RefMap(prefix, RefList.<Ref> emptyList(), c.all, c.sym);
-	}
-
-	private static ObjectId idOf(@Nullable Ref src) {
-		return src != null && src.getObjectId() != null
-				? src.getObjectId()
-				: ObjectId.zeroId();
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public List<Ref> getAdditionalRefs() throws IOException {
-		Collection<Ref> txnRefs;
-		if (txnNamespace != null) {
-			txnRefs = bootstrap.getRefsByPrefix(txnNamespace);
-		} else {
-			Ref r = bootstrap.exactRef(txnCommitted);
-			if (r != null && r.getObjectId() != null) {
-				txnRefs = Collections.singleton(r);
-			} else {
-				txnRefs = Collections.emptyList();
-			}
-		}
-
-		List<Ref> otherRefs = bootstrap.getAdditionalRefs();
-		List<Ref> all = new ArrayList<>(txnRefs.size() + otherRefs.size());
-		all.addAll(txnRefs);
-		all.addAll(otherRefs);
-		return all;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public Ref peel(Ref ref) throws IOException {
-		Ref i = ref.getLeaf();
-		ObjectId id = i.getObjectId();
-		if (i.isPeeled() || id == null) {
-			return ref;
-		}
-		try (RevWalk rw = new RevWalk(repo)) {
-			RevObject obj = rw.parseAny(id);
-			if (obj instanceof RevTag) {
-				ObjectId p = rw.peel(obj).copy();
-				i = new ObjectIdRef.PeeledTag(PACKED, i.getName(), id, p);
-			} else {
-				i = new ObjectIdRef.PeeledNonTag(PACKED, i.getName(), id);
-			}
-		}
-		return recreate(ref, i);
-	}
-
-	private static Ref recreate(Ref old, Ref leaf) {
-		if (old.isSymbolic()) {
-			Ref dst = recreate(old.getTarget(), leaf);
-			return new SymbolicRef(old.getName(), dst);
-		}
-		return leaf;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public boolean isNameConflicting(String name) throws IOException {
-		return conflictsWithBootstrap(name)
-				|| !getConflictingNames(name).isEmpty();
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public BatchRefUpdate newBatchUpdate() {
-		return new RefTreeBatch(this);
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public RefUpdate newUpdate(String name, boolean detach) throws IOException {
-		if (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) {
-			return bootstrap.newUpdate(name, detach);
-		}
-		if (conflictsWithBootstrap(name)) {
-			return new AlwaysFailUpdate(this, name);
-		}
-
-		Ref r = exactRef(name);
-		if (r == null) {
-			r = new ObjectIdRef.Unpeeled(Storage.NEW, name, null);
-		}
-
-		boolean detaching = detach && r.isSymbolic();
-		if (detaching) {
-			r = new ObjectIdRef.Unpeeled(LOOSE, name, r.getObjectId());
-		}
-
-		RefTreeUpdate u = new RefTreeUpdate(this, r);
-		if (detaching) {
-			u.setDetachingSymbolicRef();
-		}
-		return u;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	public RefRename newRename(String fromName, String toName)
-			throws IOException {
-		RefUpdate from = newUpdate(fromName, true);
-		RefUpdate to = newUpdate(toName, true);
-		return new RefTreeRename(this, from, to);
-	}
-
-	boolean conflictsWithBootstrap(String name) {
-		if (txnNamespace != null && name.startsWith(txnNamespace)) {
-			return true;
-		} else if (txnCommitted.equals(name)) {
-			return true;
-		}
-
-		if (name.indexOf('/') < 0 && !HEAD.equals(name)) {
-			return true;
-		}
-
-		if (name.length() > txnCommitted.length()
-				&& name.charAt(txnCommitted.length()) == '/'
-				&& name.startsWith(txnCommitted)) {
-			return true;
-		}
-		return false;
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java
deleted file mode 100644
index eec0da2..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import org.eclipse.jgit.lib.RefDatabase;
-
-/**
- * Magic reference name logic for RefTrees.
- */
-public class RefTreeNames {
-	/**
-	 * Suffix used on a {@link RefTreeDatabase#getTxnNamespace()} for user data.
-	 * <p>
-	 * A {@link RefTreeDatabase}'s namespace may include a subspace (e.g.
-	 * {@code "refs/txn/stage/"}) containing commit objects from the usual user
-	 * portion of the repository (e.g. {@code "refs/heads/"}). These should be
-	 * packed by the garbage collector alongside other user content rather than
-	 * with the RefTree.
-	 */
-	private static final String STAGE = "stage/"; //$NON-NLS-1$
-
-	/**
-	 * Determine if the reference is likely to be a RefTree.
-	 *
-	 * @param refdb
-	 *            database instance.
-	 * @param ref
-	 *            reference name.
-	 * @return {@code true} if the reference is a RefTree.
-	 */
-	public static boolean isRefTree(RefDatabase refdb, String ref) {
-		if (refdb instanceof RefTreeDatabase) {
-			RefTreeDatabase b = (RefTreeDatabase) refdb;
-			if (ref.equals(b.getTxnCommitted())) {
-				return true;
-			}
-
-			String namespace = b.getTxnNamespace();
-			if (namespace != null
-					&& ref.startsWith(namespace)
-					&& !ref.startsWith(namespace + STAGE)) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	private RefTreeNames() {
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java
deleted file mode 100644
index ccf8f75..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.RefUpdate.Result.REJECTED;
-import static org.eclipse.jgit.lib.RefUpdate.Result.RENAMED;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefRename;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-/** Single reference rename to {@link RefTreeDatabase}. */
-class RefTreeRename extends RefRename {
-	private final RefTreeDatabase refdb;
-
-	RefTreeRename(RefTreeDatabase refdb, RefUpdate src, RefUpdate dst) {
-		super(src, dst);
-		this.refdb = refdb;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected Result doRename() throws IOException {
-		try (RevWalk rw = new RevWalk(refdb.getRepository())) {
-			RefTreeBatch batch = new RefTreeBatch(refdb);
-			batch.setRefLogIdent(getRefLogIdent());
-			batch.setRefLogMessage(getRefLogMessage(), false);
-			batch.init(rw);
-
-			Ref head = batch.exactRef(rw.getObjectReader(), HEAD);
-			Ref oldRef = batch.exactRef(rw.getObjectReader(), source.getName());
-			if (oldRef == null) {
-				return REJECTED;
-			}
-
-			Ref newRef = asNew(oldRef);
-			List<Command> mv = new ArrayList<>(3);
-			mv.add(new Command(oldRef, null));
-			mv.add(new Command(null, newRef));
-			if (head != null && head.isSymbolic()
-					&& head.getTarget().getName().equals(oldRef.getName())) {
-				mv.add(new Command(
-					head,
-					new SymbolicRef(head.getName(), newRef)));
-			}
-			batch.execute(rw, mv);
-			return RefTreeUpdate.translate(mv.get(1).getResult(), RENAMED);
-		}
-	}
-
-	private Ref asNew(Ref src) {
-		String name = destination.getName();
-		if (src.isSymbolic()) {
-			return new SymbolicRef(name, src.getTarget());
-		}
-
-		ObjectId peeled = src.getPeeledObjectId();
-		if (peeled != null) {
-			return new ObjectIdRef.PeeledTag(
-					src.getStorage(),
-					name,
-					src.getObjectId(),
-					peeled);
-		}
-
-		return new ObjectIdRef.PeeledNonTag(
-				src.getStorage(),
-				name,
-				src.getObjectId());
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java
deleted file mode 100644
index 6e6ccd9..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
-import static org.eclipse.jgit.lib.Ref.Storage.NEW;
-
-import java.io.IOException;
-import java.util.Collections;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-/** Single reference update to {@link RefTreeDatabase}. */
-class RefTreeUpdate extends RefUpdate {
-	private final RefTreeDatabase refdb;
-	private RevWalk rw;
-	private RefTreeBatch batch;
-	private Ref oldRef;
-
-	RefTreeUpdate(RefTreeDatabase refdb, Ref ref) {
-		super(ref);
-		this.refdb = refdb;
-		setCheckConflicting(false); // Done automatically by doUpdate.
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected RefDatabase getRefDatabase() {
-		return refdb;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected Repository getRepository() {
-		return refdb.getRepository();
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected boolean tryLock(boolean deref) throws IOException {
-		rw = new RevWalk(getRepository());
-		batch = new RefTreeBatch(refdb);
-		batch.init(rw);
-		oldRef = batch.exactRef(rw.getObjectReader(), getName());
-		if (oldRef != null && oldRef.getObjectId() != null) {
-			setOldObjectId(oldRef.getObjectId());
-		} else if (oldRef == null && getExpectedOldObjectId() != null) {
-			setOldObjectId(ObjectId.zeroId());
-		}
-		return true;
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected void unlock() {
-		batch = null;
-		if (rw != null) {
-			rw.close();
-			rw = null;
-		}
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected Result doUpdate(Result desiredResult) throws IOException {
-		return run(newRef(getName(), getNewObjectId()), desiredResult);
-	}
-
-	private Ref newRef(String name, ObjectId id)
-			throws MissingObjectException, IOException {
-		RevObject o = rw.parseAny(id);
-		if (o instanceof RevTag) {
-			RevObject p = rw.peel(o);
-			return new ObjectIdRef.PeeledTag(LOOSE, name, id, p.copy());
-		}
-		return new ObjectIdRef.PeeledNonTag(LOOSE, name, id);
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected Result doDelete(Result desiredResult) throws IOException {
-		return run(null, desiredResult);
-	}
-
-	/** {@inheritDoc} */
-	@Override
-	protected Result doLink(String target) throws IOException {
-		Ref dst = new ObjectIdRef.Unpeeled(NEW, target, null);
-		SymbolicRef n = new SymbolicRef(getName(), dst);
-		Result desiredResult = getRef().getStorage() == NEW
-			? Result.NEW
-			: Result.FORCED;
-		return run(n, desiredResult);
-	}
-
-	private Result run(@Nullable Ref newRef, Result desiredResult)
-			throws IOException {
-		Command c = new Command(oldRef, newRef);
-		batch.setRefLogIdent(getRefLogIdent());
-		batch.setRefLogMessage(getRefLogMessage(), isRefLogIncludingResult());
-		batch.execute(rw, Collections.singletonList(c));
-		return translate(c.getResult(), desiredResult);
-	}
-
-	static Result translate(ReceiveCommand.Result r, Result desiredResult) {
-		switch (r) {
-		case OK:
-			return desiredResult;
-
-		case LOCK_FAILURE:
-			return Result.LOCK_FAILURE;
-
-		case NOT_ATTEMPTED:
-			return Result.NOT_ATTEMPTED;
-
-		case REJECTED_MISSING_OBJECT:
-			return Result.IO_FAILURE;
-
-		case REJECTED_CURRENT_BRANCH:
-			return Result.REJECTED_CURRENT_BRANCH;
-
-		case REJECTED_OTHER_REASON:
-		case REJECTED_NOCREATE:
-		case REJECTED_NODELETE:
-		case REJECTED_NONFASTFORWARD:
-		default:
-			return Result.REJECTED;
-		}
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java
deleted file mode 100644
index 3f51229..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * Copyright (C) 2016, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-package org.eclipse.jgit.internal.storage.reftree;
-
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import static org.eclipse.jgit.lib.Constants.R_REFS;
-import static org.eclipse.jgit.lib.Constants.encode;
-import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
-import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
-import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
-import static org.eclipse.jgit.lib.Ref.Storage.NEW;
-import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
-import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH;
-
-import java.io.IOException;
-
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.AbstractTreeIterator;
-import org.eclipse.jgit.treewalk.CanonicalTreeParser;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.Paths;
-import org.eclipse.jgit.util.RawParseUtils;
-import org.eclipse.jgit.util.RefList;
-
-/** A tree parser that extracts references from a {@link RefTree}. */
-class Scanner {
-	private static final int MAX_SYMLINK_BYTES = 10 << 10;
-	private static final byte[] BINARY_R_REFS = encode(R_REFS);
-	private static final byte[] REFS_DOT_DOT = encode("refs/.."); //$NON-NLS-1$
-
-	static class Result {
-		final ObjectId refTreeId;
-		final RefList<Ref> all;
-		final RefList<Ref> sym;
-
-		Result(ObjectId id, RefList<Ref> all, RefList<Ref> sym) {
-			this.refTreeId = id;
-			this.all = all;
-			this.sym = sym;
-		}
-	}
-
-	/**
-	 * Scan a {@link RefTree} and parse entries into {@link Ref} instances.
-	 *
-	 * @param repo
-	 *            source repository containing the commit and tree objects that
-	 *            make up the RefTree.
-	 * @param src
-	 *            bootstrap reference such as {@code refs/txn/committed} to read
-	 *            the reference tree tip from. The current ObjectId will be
-	 *            included in {@link Result#refTreeId}.
-	 * @param prefix
-	 *            if non-empty a reference prefix to scan only a subdirectory.
-	 *            For example {@code prefix = "refs/heads/"} will limit the scan
-	 *            to only the {@code "heads"} directory of the RefTree, avoiding
-	 *            other directories like {@code "tags"}. Empty string reads all
-	 *            entries in the RefTree.
-	 * @param recursive
-	 *            if true recurse into subdirectories of the reference tree;
-	 *            false to read only one level. Callers may use false during an
-	 *            implementation of {@code exactRef(String)} where only one
-	 *            reference is needed out of a specific subtree.
-	 * @return sorted list of references after parsing.
-	 * @throws IOException
-	 *             tree cannot be accessed from the repository.
-	 */
-	static Result scanRefTree(Repository repo, @Nullable Ref src, String prefix,
-			boolean recursive) throws IOException {
-		RefList.Builder<Ref> all = new RefList.Builder<>();
-		RefList.Builder<Ref> sym = new RefList.Builder<>();
-
-		ObjectId srcId;
-		if (src != null && src.getObjectId() != null) {
-			try (ObjectReader reader = repo.newObjectReader()) {
-				srcId = src.getObjectId();
-				scan(reader, srcId, prefix, recursive, all, sym);
-			}
-		} else {
-			srcId = ObjectId.zeroId();
-		}
-
-		RefList<Ref> aList = all.toRefList();
-		for (int idx = 0; idx < sym.size();) {
-			Ref s = sym.get(idx);
-			Ref r = resolve(s, 0, aList);
-			if (r != null) {
-				sym.set(idx++, r);
-			} else {
-				// Remove broken symbolic reference, they don't exist.
-				sym.remove(idx);
-				int rm = aList.find(s.getName());
-				if (0 <= rm) {
-					aList = aList.remove(rm);
-				}
-			}
-		}
-		return new Result(srcId, aList, sym.toRefList());
-	}
-
-	private static void scan(ObjectReader reader, AnyObjectId srcId,
-			String prefix, boolean recursive,
-			RefList.Builder<Ref> all, RefList.Builder<Ref> sym)
-					throws IncorrectObjectTypeException, IOException {
-		CanonicalTreeParser p = createParserAtPath(reader, srcId, prefix);
-		if (p == null) {
-			return;
-		}
-
-		while (!p.eof()) {
-			int mode = p.getEntryRawMode();
-			if (mode == TYPE_TREE) {
-				if (recursive) {
-					p = p.createSubtreeIterator(reader);
-				} else {
-					p = p.next();
-				}
-				continue;
-			}
-
-			if (!curElementHasPeelSuffix(p)) {
-				Ref r = toRef(reader, mode, p);
-				if (r != null) {
-					all.add(r);
-					if (r.isSymbolic()) {
-						sym.add(r);
-					}
-				}
-			} else if (mode == TYPE_GITLINK) {
-				peel(all, p);
-			}
-			p = p.next();
-		}
-	}
-
-	private static CanonicalTreeParser createParserAtPath(ObjectReader reader,
-			AnyObjectId srcId, String prefix) throws IOException {
-		ObjectId root = toTree(reader, srcId);
-		if (prefix.isEmpty()) {
-			return new CanonicalTreeParser(BINARY_R_REFS, reader, root);
-		}
-
-		String dir = RefTree.refPath(Paths.stripTrailingSeparator(prefix));
-		TreeWalk tw = TreeWalk.forPath(reader, dir, root);
-		if (tw == null || !tw.isSubtree()) {
-			return null;
-		}
-
-		ObjectId id = tw.getObjectId(0);
-		return new CanonicalTreeParser(encode(prefix), reader, id);
-	}
-
-	private static Ref resolve(Ref ref, int depth, RefList<Ref> refs)
-			throws IOException {
-		if (!ref.isSymbolic()) {
-			return ref;
-		} else if (MAX_SYMBOLIC_REF_DEPTH <= depth) {
-			return null;
-		}
-
-		Ref r = refs.get(ref.getTarget().getName());
-		if (r == null) {
-			return ref;
-		}
-
-		Ref dst = resolve(r, depth + 1, refs);
-		if (dst == null) {
-			return null;
-		}
-		return new SymbolicRef(ref.getName(), dst);
-	}
-
-	private static RevTree toTree(ObjectReader reader, AnyObjectId id)
-			throws IOException {
-		try (RevWalk rw = new RevWalk(reader)) {
-			return rw.parseTree(id);
-		}
-	}
-
-	private static boolean curElementHasPeelSuffix(AbstractTreeIterator itr) {
-		int n = itr.getEntryPathLength();
-		byte[] c = itr.getEntryPathBuffer();
-		return n > 2 && c[n - 2] == ' ' && c[n - 1] == '^';
-	}
-
-	private static void peel(RefList.Builder<Ref> all, CanonicalTreeParser p) {
-		String name = refName(p, true);
-		for (int idx = all.size() - 1; 0 <= idx; idx--) {
-			Ref r = all.get(idx);
-			int cmp = r.getName().compareTo(name);
-			if (cmp == 0) {
-				all.set(idx, new ObjectIdRef.PeeledTag(PACKED, r.getName(),
-						r.getObjectId(), p.getEntryObjectId()));
-				break;
-			} else if (cmp < 0) {
-				// Stray peeled name without matching base name; skip entry.
-				break;
-			}
-		}
-	}
-
-	private static Ref toRef(ObjectReader reader, int mode,
-			CanonicalTreeParser p) throws IOException {
-		if (mode == TYPE_GITLINK) {
-			String name = refName(p, false);
-			ObjectId id = p.getEntryObjectId();
-			return new ObjectIdRef.PeeledNonTag(PACKED, name, id);
-
-		} else if (mode == TYPE_SYMLINK) {
-			ObjectId id = p.getEntryObjectId();
-			byte[] bin = reader.open(id, OBJ_BLOB)
-					.getCachedBytes(MAX_SYMLINK_BYTES);
-			String dst = RawParseUtils.decode(bin);
-			Ref trg = new ObjectIdRef.Unpeeled(NEW, dst, null);
-			String name = refName(p, false);
-			return new SymbolicRef(name, trg);
-		}
-		return null;
-	}
-
-	private static String refName(CanonicalTreeParser p, boolean peel) {
-		byte[] buf = p.getEntryPathBuffer();
-		int len = p.getEntryPathLength();
-		if (peel) {
-			len -= 2;
-		}
-		int ptr = 0;
-		if (RawParseUtils.match(buf, ptr, REFS_DOT_DOT) > 0) {
-			ptr = 7;
-		}
-		return RawParseUtils.decode(buf, ptr, len);
-	}
-
-	private Scanner() {
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java
index 49f26c7..dae7173 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java
@@ -22,11 +22,12 @@
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Date;
 import java.util.LinkedHashSet;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
@@ -53,6 +54,7 @@
  * In general this class is not thread-safe. So any consumer needs to take care
  * of synchronization!
  *
+ * @see <a href="https://curl.se/docs/http-cookies.html">Cookie file format</a>
  * @see <a href="http://www.cookiecentral.com/faq/#3.5">Netscape Cookie File
  *      Format</a>
  * @see <a href=
@@ -92,7 +94,7 @@
 
 	private byte[] hash;
 
-	final Date creationDate;
+	private final Instant createdAt;
 
 	private Set<HttpCookie> cookies = null;
 
@@ -104,13 +106,13 @@
 	 *            where to find the cookie file
 	 */
 	public NetscapeCookieFile(Path path) {
-		this(path, new Date());
+		this(path, Instant.now());
 	}
 
-	NetscapeCookieFile(Path path, Date creationDate) {
+	NetscapeCookieFile(Path path, Instant createdAt) {
 		this.path = path;
 		this.snapshot = FileSnapshot.DIRTY;
-		this.creationDate = creationDate;
+		this.createdAt = createdAt;
 	}
 
 	/**
@@ -142,7 +144,7 @@
 		if (cookies == null || refresh) {
 			try {
 				byte[] in = getFileContentIfModified();
-				Set<HttpCookie> newCookies = parseCookieFile(in, creationDate);
+				Set<HttpCookie> newCookies = parseCookieFile(in, createdAt);
 				if (cookies != null) {
 					cookies = mergeCookies(newCookies, cookies);
 				} else {
@@ -168,9 +170,9 @@
 	 *
 	 * @param input
 	 *            the file content to parse
-	 * @param creationDate
-	 *            the date for the creation of the cookies (used to calculate
-	 *            the maxAge based on the expiration date given within the file)
+	 * @param createdAt
+	 *            cookie creation time; used to calculate the maxAge based on
+	 *            the expiration date given within the file
 	 * @return the set of parsed cookies from the given file (even expired
 	 *         ones). If there is more than one cookie with the same name in
 	 *         this file the last one overwrites the first one!
@@ -180,7 +182,7 @@
 	 *             if the given file does not have a proper format
 	 */
 	private static Set<HttpCookie> parseCookieFile(@NonNull byte[] input,
-			@NonNull Date creationDate)
+			@NonNull Instant createdAt)
 			throws IOException, IllegalArgumentException {
 
 		String decoded = RawParseUtils.decode(StandardCharsets.US_ASCII, input);
@@ -190,7 +192,7 @@
 				new StringReader(decoded))) {
 			String line;
 			while ((line = reader.readLine()) != null) {
-				HttpCookie cookie = parseLine(line, creationDate);
+				HttpCookie cookie = parseLine(line, createdAt);
 				if (cookie != null) {
 					cookies.add(cookie);
 				}
@@ -200,7 +202,7 @@
 	}
 
 	private static HttpCookie parseLine(@NonNull String line,
-			@NonNull Date creationDate) {
+			@NonNull Instant createdAt) {
 		if (line.isEmpty() || (line.startsWith("#") //$NON-NLS-1$
 				&& !line.startsWith(HTTP_ONLY_PREAMBLE))) {
 			return null;
@@ -236,7 +238,12 @@
 		cookie.setSecure(Boolean.parseBoolean(cookieLineParts[3]));
 
 		long expires = Long.parseLong(cookieLineParts[4]);
-		long maxAge = (expires - creationDate.getTime()) / 1000;
+		// Older versions stored milliseconds. This heuristic to detect that
+		// will cause trouble in the year 33658. :-)
+		if (cookieLineParts[4].length() == 13) {
+			expires = TimeUnit.MILLISECONDS.toSeconds(expires);
+		}
+		long maxAge = expires - createdAt.getEpochSecond();
 		if (maxAge <= 0) {
 			return null; // skip expired cookies
 		}
@@ -245,7 +252,7 @@
 	}
 
 	/**
-	 * Read the underying file and return its content but only in case it has
+	 * Read the underlying file and return its content but only in case it has
 	 * been modified since the last access.
 	 * <p>
 	 * Internally calculates the hash and maintains {@link FileSnapshot}s to
@@ -333,7 +340,7 @@
 						path);
 				// reread new changes if necessary
 				Set<HttpCookie> cookiesFromFile = NetscapeCookieFile
-						.parseCookieFile(cookieFileContent, creationDate);
+						.parseCookieFile(cookieFileContent, createdAt);
 				this.cookies = mergeCookies(cookiesFromFile, cookies);
 			}
 		} catch (FileNotFoundException e) {
@@ -343,7 +350,7 @@
 		ByteArrayOutputStream output = new ByteArrayOutputStream();
 		try (Writer writer = new OutputStreamWriter(output,
 				StandardCharsets.US_ASCII)) {
-			write(writer, cookies, url, creationDate);
+			write(writer, cookies, url, createdAt);
 		}
 		LockFile lockFile = new LockFile(path.toFile());
 		for (int retryCount = 0; retryCount < LOCK_ACQUIRE_MAX_RETRY_COUNT; retryCount++) {
@@ -377,24 +384,23 @@
 	 * @param url
 	 *            the url for which to write the cookie (to derive the default
 	 *            values for certain cookie attributes)
-	 * @param creationDate
-	 *            the date when the cookie has been created. Important for
-	 *            calculation the cookie expiration time (calculated from
-	 *            cookie's maxAge and this creation time)
+	 * @param createdAt
+	 *            cookie creation time; used to calculate a cookie's expiration
+	 *            time
 	 * @throws IOException
 	 *             if an I/O error occurs
 	 */
 	static void write(@NonNull Writer writer,
 			@NonNull Collection<HttpCookie> cookies, @NonNull URL url,
-			@NonNull Date creationDate) throws IOException {
+			@NonNull Instant createdAt) throws IOException {
 		for (HttpCookie cookie : cookies) {
-			writeCookie(writer, cookie, url, creationDate);
+			writeCookie(writer, cookie, url, createdAt);
 		}
 	}
 
 	private static void writeCookie(@NonNull Writer writer,
 			@NonNull HttpCookie cookie, @NonNull URL url,
-			@NonNull Date creationDate) throws IOException {
+			@NonNull Instant createdAt) throws IOException {
 		if (cookie.getMaxAge() <= 0) {
 			return; // skip expired cookies
 		}
@@ -422,7 +428,7 @@
 		final String expirationDate;
 		// whenCreated field is not accessible in HttpCookie
 		expirationDate = String
-				.valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000));
+				.valueOf(createdAt.getEpochSecond() + cookie.getMaxAge());
 		writer.write(expirationDate);
 		writer.write(COLUMN_SEPARATOR);
 		writer.write(cookie.getName());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
index 98c63cd..c514270 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
@@ -23,7 +23,6 @@
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
@@ -224,8 +223,17 @@
 		entries.put(DEFAULT_NAME, defaults);
 
 		while ((line = reader.readLine()) != null) {
+			// OpenSsh ignores trailing comments on a line. Anything after the
+			// first # on a line is trimmed away (yes, even if the hash is
+			// inside quotes).
+			//
+			// See https://github.com/openssh/openssh-portable/commit/2bcbf679
+			int i = line.indexOf('#');
+			if (i >= 0) {
+				line = line.substring(0, i);
+			}
 			line = line.trim();
-			if (line.isEmpty() || line.startsWith("#")) { //$NON-NLS-1$
+			if (line.isEmpty()) {
 				continue;
 			}
 			String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
@@ -484,12 +492,30 @@
 			LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
 		}
 
+		/**
+		 * OpenSSH has renamed some config keys. This maps old names to new
+		 * names.
+		 */
+		private static final Map<String, String> ALIASES = new TreeMap<>(
+				String.CASE_INSENSITIVE_ORDER);
+
+		static {
+			// See https://github.com/openssh/openssh-portable/commit/ee9c0da80
+			ALIASES.put("PubkeyAcceptedKeyTypes", //$NON-NLS-1$
+					SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
+		}
+
 		private Map<String, String> options;
 
 		private Map<String, List<String>> multiOptions;
 
 		private Map<String, List<String>> listOptions;
 
+		private static String toKey(String key) {
+			String k = ALIASES.get(key);
+			return k != null ? k : key;
+		}
+
 		/**
 		 * Retrieves the value of a single-valued key, or the first if the key
 		 * has multiple values. Keys are case-insensitive, so
@@ -501,15 +527,15 @@
 		 */
 		@Override
 		public String getValue(String key) {
-			String result = options != null ? options.get(key) : null;
+			String k = toKey(key);
+			String result = options != null ? options.get(k) : null;
 			if (result == null) {
 				// Let's be lenient and return at least the first value from
 				// a list-valued or multi-valued key.
-				List<String> values = listOptions != null ? listOptions.get(key)
+				List<String> values = listOptions != null ? listOptions.get(k)
 						: null;
 				if (values == null) {
-					values = multiOptions != null ? multiOptions.get(key)
-							: null;
+					values = multiOptions != null ? multiOptions.get(k) : null;
 				}
 				if (values != null && !values.isEmpty()) {
 					result = values.get(0);
@@ -529,10 +555,11 @@
 		 */
 		@Override
 		public List<String> getValues(String key) {
-			List<String> values = listOptions != null ? listOptions.get(key)
+			String k = toKey(key);
+			List<String> values = listOptions != null ? listOptions.get(k)
 					: null;
 			if (values == null) {
-				values = multiOptions != null ? multiOptions.get(key) : null;
+				values = multiOptions != null ? multiOptions.get(k) : null;
 			}
 			if (values == null || values.isEmpty()) {
 				return new ArrayList<>();
@@ -551,34 +578,35 @@
 		 *            to set or add
 		 */
 		public void setValue(String key, String value) {
+			String k = toKey(key);
 			if (value == null) {
 				if (multiOptions != null) {
-					multiOptions.remove(key);
+					multiOptions.remove(k);
 				}
 				if (listOptions != null) {
-					listOptions.remove(key);
+					listOptions.remove(k);
 				}
 				if (options != null) {
-					options.remove(key);
+					options.remove(k);
 				}
 				return;
 			}
-			if (MULTI_KEYS.contains(key)) {
+			if (MULTI_KEYS.contains(k)) {
 				if (multiOptions == null) {
 					multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
 				}
-				List<String> values = multiOptions.get(key);
+				List<String> values = multiOptions.get(k);
 				if (values == null) {
 					values = new ArrayList<>(4);
-					multiOptions.put(key, values);
+					multiOptions.put(k, values);
 				}
 				values.add(value);
 			} else {
 				if (options == null) {
 					options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
 				}
-				if (!options.containsKey(key)) {
-					options.put(key, value);
+				if (!options.containsKey(k)) {
+					options.put(k, value);
 				}
 			}
 		}
@@ -595,20 +623,21 @@
 			if (values.isEmpty()) {
 				return;
 			}
+			String k = toKey(key);
 			// Check multi-valued keys first; because of the replacement
 			// strategy, they must take precedence over list-valued keys
 			// which always follow the "first occurrence wins" strategy.
 			//
 			// Note that SendEnv is a multi-valued list-valued key. (It's
 			// rather immaterial for JGit, though.)
-			if (MULTI_KEYS.contains(key)) {
+			if (MULTI_KEYS.contains(k)) {
 				if (multiOptions == null) {
 					multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
 				}
-				List<String> items = multiOptions.get(key);
+				List<String> items = multiOptions.get(k);
 				if (items == null) {
 					items = new ArrayList<>(values);
-					multiOptions.put(key, items);
+					multiOptions.put(k, items);
 				} else {
 					items.addAll(values);
 				}
@@ -616,8 +645,8 @@
 				if (listOptions == null) {
 					listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
 				}
-				if (!listOptions.containsKey(key)) {
-					listOptions.put(key, values);
+				if (!listOptions.containsKey(k)) {
+					listOptions.put(k, values);
 				}
 			}
 		}
@@ -630,7 +659,7 @@
 		 * @return {@code true} if the key is a list-valued key.
 		 */
 		public static boolean isListKey(String key) {
-			return LIST_KEYS.contains(key.toUpperCase(Locale.ROOT));
+			return LIST_KEYS.contains(toKey(key));
 		}
 
 		void merge(HostEntry entry) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
index e51995f..b2242a1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
@@ -28,6 +28,8 @@
 import java.util.LinkedList;
 import java.util.List;
 
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.InvalidRefNameException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.internal.JGitText;
@@ -38,6 +40,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;
 import org.eclipse.jgit.util.SystemReader;
 
 /**
@@ -107,6 +110,8 @@
 
 	private File workTree;
 
+	private String initialBranch = Constants.MASTER;
+
 	/** Directories limiting the search for a Git repository. */
 	private List<File> ceilingDirectories;
 
@@ -350,6 +355,43 @@
 	}
 
 	/**
+	 * Set the initial branch of the new repository. If not specified
+	 * ({@code null} or empty), fall back to the default name (currently
+	 * master).
+	 *
+	 * @param branch
+	 *            initial branch name of the new repository. If {@code null} or
+	 *            empty the configured default branch will be used.
+	 * @return {@code this}
+	 * @throws InvalidRefNameException
+	 *             if the branch name is not valid
+	 *
+	 * @since 5.11
+	 */
+	public B setInitialBranch(String branch) throws InvalidRefNameException {
+		if (StringUtils.isEmptyOrNull(branch)) {
+			this.initialBranch = Constants.MASTER;
+		} else {
+			if (!Repository.isValidRefName(Constants.R_HEADS + branch)) {
+				throw new InvalidRefNameException(MessageFormat
+						.format(JGitText.get().branchNameInvalid, branch));
+			}
+			this.initialBranch = branch;
+		}
+		return self();
+	}
+
+	/**
+	 * Get the initial branch of the new repository.
+	 *
+	 * @return the initial branch of the new repository.
+	 * @since 5.11
+	 */
+	public @NonNull String getInitialBranch() {
+		return initialBranch;
+	}
+
+	/**
 	 * Read standard Git environment variables and configure from those.
 	 * <p>
 	 * This method tries to read the standard Git environment variables, such as
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
index 4f93fda..1665f05 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
- * Copyright (C) 2006-2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2006, 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2006, 2020, 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
@@ -16,14 +16,11 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
-import java.text.MessageFormat;
 import java.util.List;
 
-import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.util.References;
 
 /**
@@ -37,7 +34,7 @@
  * and obtain a {@link org.eclipse.jgit.revwalk.RevCommit} instance by calling
  * {@link org.eclipse.jgit.revwalk.RevWalk#parseCommit(AnyObjectId)}.
  */
-public class CommitBuilder {
+public class CommitBuilder extends ObjectBuilder {
 	private static final ObjectId[] EMPTY_OBJECTID_LIST = new ObjectId[0];
 
 	private static final byte[] htree = Constants.encodeASCII("tree"); //$NON-NLS-1$
@@ -50,28 +47,17 @@
 
 	private static final byte[] hgpgsig = Constants.encodeASCII("gpgsig"); //$NON-NLS-1$
 
-	private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$
-
 	private ObjectId treeId;
 
 	private ObjectId[] parentIds;
 
-	private PersonIdent author;
-
 	private PersonIdent committer;
 
-	private GpgSignature gpgSignature;
-
-	private String message;
-
-	private Charset encoding;
-
 	/**
 	 * Initialize an empty commit.
 	 */
 	public CommitBuilder() {
 		parentIds = EMPTY_OBJECTID_LIST;
-		encoding = UTF_8;
 	}
 
 	/**
@@ -98,8 +84,9 @@
 	 *
 	 * @return the author of this commit (who wrote it).
 	 */
+	@Override
 	public PersonIdent getAuthor() {
-		return author;
+		return super.getAuthor();
 	}
 
 	/**
@@ -108,8 +95,9 @@
 	 * @param newAuthor
 	 *            the new author. Should not be null.
 	 */
+	@Override
 	public void setAuthor(PersonIdent newAuthor) {
-		author = newAuthor;
+		super.setAuthor(newAuthor);
 	}
 
 	/**
@@ -132,38 +120,6 @@
 	}
 
 	/**
-	 * Set the GPG signature of this commit.
-	 * <p>
-	 * Note, the signature set here will change the payload of the commit, i.e.
-	 * the output of {@link #build()} will include the signature. Thus, the
-	 * typical flow will be:
-	 * <ol>
-	 * <li>call {@link #build()} without a signature set to obtain payload</li>
-	 * <li>create {@link GpgSignature} from payload</li>
-	 * <li>set {@link GpgSignature}</li>
-	 * </ol>
-	 * </p>
-	 *
-	 * @param newSignature
-	 *            the signature to set or <code>null</code> to unset
-	 * @since 5.3
-	 */
-	public void setGpgSignature(GpgSignature newSignature) {
-		gpgSignature = newSignature;
-	}
-
-	/**
-	 * Get the GPG signature of this commit.
-	 *
-	 * @return the GPG signature of this commit, maybe <code>null</code> if the
-	 *         commit is not to be signed
-	 * @since 5.3
-	 */
-	public GpgSignature getGpgSignature() {
-		return gpgSignature;
-	}
-
-	/**
 	 * Get the ancestors of this commit.
 	 *
 	 * @return the ancestors of this commit. Never null.
@@ -239,25 +195,6 @@
 	}
 
 	/**
-	 * Get the complete commit message.
-	 *
-	 * @return the complete commit message.
-	 */
-	public String getMessage() {
-		return message;
-	}
-
-	/**
-	 * Set the commit message.
-	 *
-	 * @param newMessage
-	 *            the commit message. Should not be null.
-	 */
-	public void setMessage(String newMessage) {
-		message = newMessage;
-	}
-
-	/**
 	 * Set the encoding for the commit information.
 	 *
 	 * @param encodingName
@@ -267,37 +204,10 @@
 	 */
 	@Deprecated
 	public void setEncoding(String encodingName) {
-		encoding = Charset.forName(encodingName);
+		setEncoding(Charset.forName(encodingName));
 	}
 
-	/**
-	 * Set the encoding for the commit information.
-	 *
-	 * @param enc
-	 *            the encoding to use.
-	 */
-	public void setEncoding(Charset enc) {
-		encoding = enc;
-	}
-
-	/**
-	 * Get the encoding that should be used for the commit message text.
-	 *
-	 * @return the encoding that should be used for the commit message text.
-	 */
-	public Charset getEncoding() {
-		return encoding;
-	}
-
-	/**
-	 * Format this builder's state as a commit object.
-	 *
-	 * @return this object in the canonical commit format, suitable for storage
-	 *         in a repository.
-	 * @throws java.io.UnsupportedEncodingException
-	 *             the encoding specified by {@link #getEncoding()} is not
-	 *             supported by this Java runtime.
-	 */
+	@Override
 	public byte[] build() throws UnsupportedEncodingException {
 		ByteArrayOutputStream os = new ByteArrayOutputStream();
 		OutputStreamWriter w = new OutputStreamWriter(os, getEncoding());
@@ -326,19 +236,16 @@
 			w.flush();
 			os.write('\n');
 
-			if (getGpgSignature() != null) {
+			GpgSignature signature = getGpgSignature();
+			if (signature != null) {
 				os.write(hgpgsig);
 				os.write(' ');
-				writeGpgSignatureString(getGpgSignature().toExternalString(), os);
+				writeMultiLineHeader(signature.toExternalString(), os,
+						true);
 				os.write('\n');
 			}
 
-			if (!References.isSameObject(getEncoding(), UTF_8)) {
-				os.write(hencoding);
-				os.write(' ');
-				os.write(Constants.encodeASCII(getEncoding().name()));
-				os.write('\n');
-			}
+			writeEncoding(getEncoding(), os);
 
 			os.write('\n');
 
@@ -356,58 +263,6 @@
 	}
 
 	/**
-	 * Writes signature to output as per <a href=
-	 * "https://github.com/git/git/blob/master/Documentation/technical/signature-format.txt#L66,L89">gpgsig
-	 * header</a>.
-	 * <p>
-	 * CRLF and CR will be sanitized to LF and signature will have a hanging
-	 * indent of one space starting with line two. A trailing line break is
-	 * <em>not</em> written; the caller is supposed to terminate the GPG
-	 * signature header by writing a single newline.
-	 * </p>
-	 *
-	 * @param in
-	 *            signature string with line breaks
-	 * @param out
-	 *            output stream
-	 * @throws IOException
-	 *             thrown by the output stream
-	 * @throws IllegalArgumentException
-	 *             if the signature string contains non 7-bit ASCII chars
-	 */
-	static void writeGpgSignatureString(String in, OutputStream out)
-			throws IOException, IllegalArgumentException {
-		int length = in.length();
-		for (int i = 0; i < length; ++i) {
-			char ch = in.charAt(i);
-			switch (ch) {
-			case '\r':
-				if (i + 1 < length && in.charAt(i + 1) == '\n') {
-					++i;
-				}
-				if (i + 1 < length) {
-					out.write('\n');
-					out.write(' ');
-				}
-				break;
-			case '\n':
-				if (i + 1 < length) {
-					out.write('\n');
-					out.write(' ');
-				}
-				break;
-			default:
-				// sanity check
-				if (ch > 127)
-					throw new IllegalArgumentException(MessageFormat
-							.format(JGitText.get().notASCIIString, in));
-				out.write(ch);
-				break;
-			}
-		}
-	}
-
-	/**
 	 * Format this builder's state as a commit object.
 	 *
 	 * @return this object in the canonical commit format, suitable for storage
@@ -439,7 +294,7 @@
 		}
 
 		r.append("author ");
-		r.append(author != null ? author.toString() : "NOT_SET");
+		r.append(getAuthor() != null ? getAuthor().toString() : "NOT_SET");
 		r.append("\n");
 
 		r.append("committer ");
@@ -447,17 +302,20 @@
 		r.append("\n");
 
 		r.append("gpgSignature ");
-		r.append(gpgSignature != null ? gpgSignature.toString() : "NOT_SET");
+		GpgSignature signature = getGpgSignature();
+		r.append(signature != null ? signature.toString()
+				: "NOT_SET");
 		r.append("\n");
 
-		if (encoding != null && !References.isSameObject(encoding, UTF_8)) {
+		Charset encoding = getEncoding();
+		if (!References.isSameObject(encoding, UTF_8)) {
 			r.append("encoding ");
 			r.append(encoding.name());
 			r.append("\n");
 		}
 
 		r.append("\n");
-		r.append(message != null ? message : "");
+		r.append(getMessage() != null ? getMessage() : "");
 		r.append("}");
 		return r.toString();
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index 834fff5..03c1ef9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -105,7 +105,15 @@
 	public static final String CONFIG_KEY_FORMAT = "format";
 
 	/**
+	 * The "program" key
+	 *
+	 * @since 5.11
+	 */
+	public static final String CONFIG_KEY_PROGRAM = "program";
+
+	/**
 	 * The "signingKey" key
+	 *
 	 * @since 5.2
 	 */
 	public static final String CONFIG_KEY_SIGNINGKEY = "signingKey";
@@ -117,13 +125,29 @@
 	public static final String CONFIG_COMMIT_SECTION = "commit";
 
 	/**
+	 * The "tag" section
+	 *
+	 * @since 5.11
+	 */
+	public static final String CONFIG_TAG_SECTION = "tag";
+
+	/**
 	 * The "gpgSign" key
+	 *
 	 * @since 5.2
 	 */
 	public static final String CONFIG_KEY_GPGSIGN = "gpgSign";
 
 	/**
+	 * The "forceSignAnnotated" key
+	 *
+	 * @since 5.11
+	 */
+	public static final String CONFIG_KEY_FORCE_SIGN_ANNOTATED = "forceSignAnnotated";
+
+	/**
 	 * The "hooksPath" key.
+	 *
 	 * @since 5.6
 	 */
 	public static final String CONFIG_KEY_HOOKS_PATH = "hooksPath";
@@ -538,12 +562,6 @@
 	public static final String CONFIG_REF_STORAGE_REFTABLE = "reftable";
 
 	/**
-	 * The "reftree" refStorage format
-	 * @since 5.7
-	 */
-	public static final String CONFIG_REFSTORAGE_REFTREE = "reftree";
-
-	/**
 	 * The "jmx" section
 	 * @since 5.1.13
 	 */
@@ -697,4 +715,17 @@
 	 */
 	public static final String CONFIG_KEY_VERSION = "version";
 
+	/**
+	 * The "init" section
+	 *
+	 * @since 5.11
+	 */
+	public static final String CONFIG_INIT_SECTION = "init";
+
+	/**
+	 * The "defaultBranch" key
+	 *
+	 * @since 5.11
+	 */
+	public static final String CONFIG_KEY_DEFAULT_BRANCH = "defaultbranch";
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
index c1527bc..427a235 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Salesforce. and others
+ * Copyright (C) 2018, 2021 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
@@ -43,16 +43,65 @@
 		}
 	}
 
-	private final Config config;
+	private final GpgFormat keyFormat;
+
+	private final String signingKey;
+
+	private final String program;
+
+	private final boolean signCommits;
+
+	private final boolean signAllTags;
+
+	private final boolean forceAnnotated;
 
 	/**
-	 * Create a new GPG config, which will read configuration from config.
+	 * Create a {@link GpgConfig} with the given parameters and default
+	 * {@code true} for signing commits and {@code false} for tags.
+	 *
+	 * @param keySpec
+	 *            to use
+	 * @param format
+	 *            to use
+	 * @param gpgProgram
+	 *            to use
+	 * @since 5.11
+	 */
+	public GpgConfig(String keySpec, GpgFormat format, String gpgProgram) {
+		keyFormat = format;
+		signingKey = keySpec;
+		program = gpgProgram;
+		signCommits = true;
+		signAllTags = false;
+		forceAnnotated = false;
+	}
+
+	/**
+	 * Create a new GPG config that reads the configuration from config.
 	 *
 	 * @param config
 	 *            the config to read from
 	 */
 	public GpgConfig(Config config) {
-		this.config = config;
+		keyFormat = config.getEnum(GpgFormat.values(),
+				ConfigConstants.CONFIG_GPG_SECTION, null,
+				ConfigConstants.CONFIG_KEY_FORMAT, GpgFormat.OPENPGP);
+		signingKey = config.getString(ConfigConstants.CONFIG_USER_SECTION, null,
+				ConfigConstants.CONFIG_KEY_SIGNINGKEY);
+
+		String exe = config.getString(ConfigConstants.CONFIG_GPG_SECTION,
+				keyFormat.toConfigValue(), ConfigConstants.CONFIG_KEY_PROGRAM);
+		if (exe == null) {
+			exe = config.getString(ConfigConstants.CONFIG_GPG_SECTION, null,
+					ConfigConstants.CONFIG_KEY_PROGRAM);
+		}
+		program = exe;
+		signCommits = config.getBoolean(ConfigConstants.CONFIG_COMMIT_SECTION,
+				ConfigConstants.CONFIG_KEY_GPGSIGN, false);
+		signAllTags = config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION,
+				ConfigConstants.CONFIG_KEY_GPGSIGN, false);
+		forceAnnotated = config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION,
+				ConfigConstants.CONFIG_KEY_FORCE_SIGN_ANNOTATED, false);
 	}
 
 	/**
@@ -61,9 +110,19 @@
 	 * @return the {@link org.eclipse.jgit.lib.GpgConfig.GpgFormat}
 	 */
 	public GpgFormat getKeyFormat() {
-		return config.getEnum(GpgFormat.values(),
-				ConfigConstants.CONFIG_GPG_SECTION, null,
-				ConfigConstants.CONFIG_KEY_FORMAT, GpgFormat.OPENPGP);
+		return keyFormat;
+	}
+
+	/**
+	 * Retrieves the value of the configured GPG program to use, as defined by
+	 * gpg.openpgp.program, gpg.x509.program (depending on the defined
+	 * {@link #getKeyFormat() format}), or gpg.program.
+	 *
+	 * @return the program string configured, or {@code null} if none
+	 * @since 5.11
+	 */
+	public String getProgram() {
+		return program;
 	}
 
 	/**
@@ -72,8 +131,7 @@
 	 * @return the value of user.signingKey (may be <code>null</code>)
 	 */
 	public String getSigningKey() {
-		return config.getString(ConfigConstants.CONFIG_USER_SECTION, null,
-				ConfigConstants.CONFIG_KEY_SIGNINGKEY);
+		return signingKey;
 	}
 
 	/**
@@ -82,7 +140,29 @@
 	 * @return the value of commit.gpgSign (defaults to <code>false</code>)
 	 */
 	public boolean isSignCommits() {
-		return config.getBoolean(ConfigConstants.CONFIG_COMMIT_SECTION,
-				ConfigConstants.CONFIG_KEY_GPGSIGN, false);
+		return signCommits;
+	}
+
+	/**
+	 * Retrieves the value of git config {@code tag.gpgSign}.
+	 *
+	 * @return the value of {@code tag.gpgSign}; by default {@code false}
+	 *
+	 * @since 5.11
+	 */
+	public boolean isSignAllTags() {
+		return signAllTags;
+	}
+
+	/**
+	 * Retrieves the value of git config {@code tag.forceSignAnnotated}.
+	 *
+	 * @return the value of {@code tag.forceSignAnnotated}; by default
+	 *         {@code false}
+	 *
+	 * @since 5.11
+	 */
+	public boolean isSignAnnotated() {
+		return forceAnnotated;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java
new file mode 100644
index 0000000..074f465
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 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.lib;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
+import org.eclipse.jgit.transport.CredentialsProvider;
+
+/**
+ * Creates GPG signatures for Git objects.
+ *
+ * @since 5.11
+ */
+public interface GpgObjectSigner {
+
+	/**
+	 * Signs the specified object.
+	 *
+	 * <p>
+	 * Implementors should obtain the payload for signing from the specified
+	 * object via {@link ObjectBuilder#build()} and create a proper
+	 * {@link GpgSignature}. The generated signature must be set on the
+	 * specified {@code object} (see
+	 * {@link ObjectBuilder#setGpgSignature(GpgSignature)}).
+	 * </p>
+	 * <p>
+	 * Any existing signature on the object must be discarded prior obtaining
+	 * the payload via {@link ObjectBuilder#build()}.
+	 * </p>
+	 *
+	 * @param object
+	 *            the object to sign (must not be {@code null} and must be
+	 *            complete to allow proper calculation of payload)
+	 * @param gpgSigningKey
+	 *            the signing key to locate (passed as is to the GPG signing
+	 *            tool as is; eg., value of <code>user.signingkey</code>)
+	 * @param committer
+	 *            the signing identity (to help with key lookup in case signing
+	 *            key is not specified)
+	 * @param credentialsProvider
+	 *            provider to use when querying for signing key credentials (eg.
+	 *            passphrase)
+	 * @param config
+	 *            GPG settings from the git config
+	 * @throws CanceledException
+	 *             when signing was canceled (eg., user aborted when entering
+	 *             passphrase)
+	 * @throws UnsupportedSigningFormatException
+	 *             if a config is given and the wanted key format is not
+	 *             supported
+	 */
+	void signObject(@NonNull ObjectBuilder object,
+			@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
+			CredentialsProvider credentialsProvider, GpgConfig config)
+			throws CanceledException, UnsupportedSigningFormatException;
+
+	/**
+	 * Indicates if a signing key is available for the specified committer
+	 * and/or signing key.
+	 *
+	 * @param gpgSigningKey
+	 *            the signing key to locate (passed as is to the GPG signing
+	 *            tool as is; eg., value of <code>user.signingkey</code>)
+	 * @param committer
+	 *            the signing identity (to help with key lookup in case signing
+	 *            key is not specified)
+	 * @param credentialsProvider
+	 *            provider to use when querying for signing key credentials (eg.
+	 *            passphrase)
+	 * @param config
+	 *            GPG settings from the git config
+	 * @return <code>true</code> if a signing key is available,
+	 *         <code>false</code> otherwise
+	 * @throws CanceledException
+	 *             when signing was canceled (eg., user aborted when entering
+	 *             passphrase)
+	 * @throws UnsupportedSigningFormatException
+	 *             if a config is given and the wanted key format is not
+	 *             supported
+	 */
+	public abstract boolean canLocateSigningKey(@Nullable String gpgSigningKey,
+			@NonNull PersonIdent committer,
+			CredentialsProvider credentialsProvider, GpgConfig config)
+			throws CanceledException, UnsupportedSigningFormatException;
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java
new file mode 100644
index 0000000..a7a39c9
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib;
+
+import java.io.IOException;
+import java.util.Date;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.revwalk.RevObject;
+
+/**
+ * A {@code GpgVerifier} can verify GPG signatures on git commits and tags.
+ *
+ * @since 5.11
+ */
+public interface GpgSignatureVerifier {
+
+	/**
+	 * Verifies the signature on a signed commit or tag.
+	 *
+	 * @param object
+	 *            to verify
+	 * @param config
+	 *            the {@link GpgConfig} to use
+	 * @return a {@link SignatureVerification} describing the outcome of the
+	 *         verification, or {@code null} if the object was not signed
+	 * @throws IOException
+	 *             if an error occurs getting a public key
+	 * @throws org.eclipse.jgit.api.errors.JGitInternalException
+	 *             if signature verification fails
+	 */
+	@Nullable
+	SignatureVerification verifySignature(@NonNull RevObject object,
+			@NonNull GpgConfig config) throws IOException;
+
+
+	/**
+	 * Verifies a given signature for given data.
+	 *
+	 * @param data
+	 *            the signature is for
+	 * @param signatureData
+	 *            the ASCII-armored signature
+	 * @return a {@link SignatureVerification} describing the outcome
+	 * @throws IOException
+	 *             if the signature cannot be parsed
+	 * @throws JGitInternalException
+	 *             if signature verification fails
+	 */
+	public SignatureVerification verify(byte[] data, byte[] signatureData)
+			throws IOException;
+
+	/**
+	 * Retrieves the name of this verifier. This should be a short string
+	 * identifying the engine that verified the signature, like "gpg" if GPG is
+	 * used, or "bc" for a BouncyCastle implementation.
+	 *
+	 * @return the name
+	 */
+	@NonNull
+	String getName();
+
+	/**
+	 * A {@link GpgSignatureVerifier} may cache public keys to speed up
+	 * verifying signatures on multiple objects. This clears this cache, if any.
+	 */
+	void clear();
+
+	/**
+	 * A {@code SignatureVerification} returns data about a (positively or
+	 * negatively) verified signature.
+	 */
+	interface SignatureVerification {
+
+		// Data about the signature.
+
+		@NonNull
+		Date getCreationDate();
+
+		// Data from the signature used to find a public key.
+
+		/**
+		 * Obtains the signer as stored in the signature, if known.
+		 *
+		 * @return the signer, or {@code null} if unknown
+		 */
+		String getSigner();
+
+		/**
+		 * Obtains the short or long fingerprint of the public key as stored in
+		 * the signature, if known.
+		 *
+		 * @return the fingerprint, or {@code null} if unknown
+		 */
+		String getKeyFingerprint();
+
+		// Some information about the found public key.
+
+		/**
+		 * Obtains the OpenPGP user ID associated with the key.
+		 *
+		 * @return the user id, or {@code null} if unknown
+		 */
+		String getKeyUser();
+
+		/**
+		 * Tells whether the public key used for this signature verification was
+		 * expired when the signature was created.
+		 *
+		 * @return {@code true} if the key was expired already, {@code false}
+		 *         otherwise
+		 */
+		boolean isExpired();
+
+		/**
+		 * Obtains the trust level of the public key used to verify the
+		 * signature.
+		 *
+		 * @return the trust level
+		 */
+		@NonNull
+		TrustLevel getTrustLevel();
+
+		// The verification result.
+
+		/**
+		 * Tells whether the signature verification was successful.
+		 *
+		 * @return {@code true} if the signature was verified successfully;
+		 *         {@code false} if not.
+		 */
+		boolean getVerified();
+
+		/**
+		 * Obtains a human-readable message giving additional information about
+		 * the outcome of the verification.
+		 *
+		 * @return the message, or {@code null} if none set.
+		 */
+		String getMessage();
+	}
+
+	/**
+	 * The owner's trust in a public key.
+	 */
+	enum TrustLevel {
+		UNKNOWN, NEVER, MARGINAL, FULL, ULTIMATE
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
new file mode 100644
index 0000000..4b1dbed
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib;
+
+import java.util.Iterator;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@code GpgSignatureVerifierFactory} creates {@link GpgSignatureVerifier} instances.
+ *
+ * @since 5.11
+ */
+public abstract class GpgSignatureVerifierFactory {
+
+	private static final Logger LOG = LoggerFactory
+			.getLogger(GpgSignatureVerifierFactory.class);
+
+	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);
+		}
+		return null;
+	}
+
+	/**
+	 * Retrieves the default factory.
+	 *
+	 * @return the default factory or {@code null} if none set
+	 */
+	public static GpgSignatureVerifierFactory getDefault() {
+		return defaultFactory;
+	}
+
+	/**
+	 * Sets the default factory.
+	 *
+	 * @param factory
+	 *            the new default factory
+	 */
+	public static void setDefault(GpgSignatureVerifierFactory factory) {
+		defaultFactory = factory;
+	}
+
+	/**
+	 * Creates a new {@link GpgSignatureVerifier}.
+	 *
+	 * @return the new {@link GpgSignatureVerifier}
+	 */
+	public abstract GpgSignatureVerifier getVerifier();
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java
new file mode 100644
index 0000000..4b7054f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2020, 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.lib;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Objects;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.util.References;
+
+/**
+ * Common base class for {@link CommitBuilder} and {@link TagBuilder}.
+ *
+ * @since 5.11
+ */
+public abstract class ObjectBuilder {
+
+	/** Byte representation of "encoding". */
+	private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$
+
+	private PersonIdent author;
+
+	private GpgSignature gpgSignature;
+
+	private String message;
+
+	private Charset encoding = StandardCharsets.UTF_8;
+
+	/**
+	 * Retrieves the author of this object.
+	 *
+	 * @return the author of this object, or {@code null} if not set yet
+	 */
+	protected PersonIdent getAuthor() {
+		return author;
+	}
+
+	/**
+	 * Sets the author (name, email address, and date) of this object.
+	 *
+	 * @param newAuthor
+	 *            the new author, must be non-{@code null}
+	 */
+	protected void setAuthor(PersonIdent newAuthor) {
+		author = Objects.requireNonNull(newAuthor);
+	}
+
+	/**
+	 * Sets the GPG signature of this object.
+	 * <p>
+	 * Note, the signature set here will change the payload of the object, i.e.
+	 * the output of {@link #build()} will include the signature. Thus, the
+	 * typical flow will be:
+	 * <ol>
+	 * <li>call {@link #build()} without a signature set to obtain payload</li>
+	 * <li>create {@link GpgSignature} from payload</li>
+	 * <li>set {@link GpgSignature}</li>
+	 * </ol>
+	 * </p>
+	 *
+	 * @param gpgSignature
+	 *            the signature to set or {@code null} to unset
+	 * @since 5.3
+	 */
+	public void setGpgSignature(@Nullable GpgSignature gpgSignature) {
+		this.gpgSignature = gpgSignature;
+	}
+
+	/**
+	 * Retrieves the GPG signature of this object.
+	 *
+	 * @return the GPG signature of this object, or {@code null} if the object
+	 *         is not signed
+	 * @since 5.3
+	 */
+	@Nullable
+	public GpgSignature getGpgSignature() {
+		return gpgSignature;
+	}
+
+	/**
+	 * Retrieves the complete message of the object.
+	 *
+	 * @return the complete message; can be {@code null}.
+	 */
+	@Nullable
+	public String getMessage() {
+		return message;
+	}
+
+	/**
+	 * Sets the message (commit message, or message of an annotated tag).
+	 *
+	 * @param message
+	 *            the message.
+	 */
+	public void setMessage(@Nullable String message) {
+		this.message = message;
+	}
+
+	/**
+	 * Retrieves the encoding that should be used for the message text.
+	 *
+	 * @return the encoding that should be used for the message text.
+	 */
+	@NonNull
+	public Charset getEncoding() {
+		return encoding;
+	}
+
+	/**
+	 * Sets the encoding for the object message.
+	 *
+	 * @param encoding
+	 *            the encoding to use.
+	 */
+	public void setEncoding(@NonNull Charset encoding) {
+		this.encoding = encoding;
+	}
+
+	/**
+	 * Format this builder's state as a git object.
+	 *
+	 * @return this object in the canonical git format, suitable for storage in
+	 *         a repository.
+	 * @throws java.io.UnsupportedEncodingException
+	 *             the encoding specified by {@link #getEncoding()} is not
+	 *             supported by this Java runtime.
+	 */
+	@NonNull
+	public abstract byte[] build() throws UnsupportedEncodingException;
+
+	/**
+	 * Writes signature to output as per <a href=
+	 * "https://github.com/git/git/blob/master/Documentation/technical/signature-format.txt#L66,L89">gpgsig
+	 * header</a>.
+	 * <p>
+	 * CRLF and CR will be sanitized to LF and signature will have a hanging
+	 * indent of one space starting with line two. A trailing line break is
+	 * <em>not</em> written; the caller is supposed to terminate the GPG
+	 * signature header by writing a single newline.
+	 * </p>
+	 *
+	 * @param in
+	 *            signature string with line breaks
+	 * @param out
+	 *            output stream
+	 * @param enforceAscii
+	 *            whether to throw {@link IllegalArgumentException} if non-ASCII
+	 *            characters are encountered
+	 * @throws IOException
+	 *             thrown by the output stream
+	 * @throws IllegalArgumentException
+	 *             if the signature string contains non 7-bit ASCII chars and
+	 *             {@code enforceAscii == true}
+	 */
+	static void writeMultiLineHeader(@NonNull String in,
+			@NonNull OutputStream out, boolean enforceAscii)
+			throws IOException, IllegalArgumentException {
+		int length = in.length();
+		for (int i = 0; i < length; ++i) {
+			char ch = in.charAt(i);
+			switch (ch) {
+			case '\r':
+				if (i + 1 < length && in.charAt(i + 1) == '\n') {
+					++i;
+				}
+				if (i + 1 < length) {
+					out.write('\n');
+					out.write(' ');
+				}
+				break;
+			case '\n':
+				if (i + 1 < length) {
+					out.write('\n');
+					out.write(' ');
+				}
+				break;
+			default:
+				// sanity check
+				if (ch > 127 && enforceAscii)
+					throw new IllegalArgumentException(MessageFormat
+							.format(JGitText.get().notASCIIString, in));
+				out.write(ch);
+				break;
+			}
+		}
+	}
+
+	/**
+	 * Writes an "encoding" header.
+	 *
+	 * @param encoding
+	 *            to write
+	 * @param out
+	 *            to write to
+	 * @throws IOException
+	 *             if writing fails
+	 */
+	static void writeEncoding(@NonNull Charset encoding,
+			@NonNull OutputStream out) throws IOException {
+		if (!References.isSameObject(encoding, UTF_8)) {
+			out.write(hencoding);
+			out.write(' ');
+			out.write(Constants.encodeASCII(encoding.name()));
+			out.write('\n');
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java
index 6bb6ae5..718ed89 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java
@@ -17,9 +17,18 @@
 import java.util.List;
 import java.util.Set;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.revwalk.BitmappedObjectReachabilityChecker;
+import org.eclipse.jgit.internal.revwalk.BitmappedReachabilityChecker;
+import org.eclipse.jgit.internal.revwalk.PedestrianObjectReachabilityChecker;
+import org.eclipse.jgit.internal.revwalk.PedestrianReachabilityChecker;
+import org.eclipse.jgit.revwalk.ObjectReachabilityChecker;
+import org.eclipse.jgit.revwalk.ObjectWalk;
+import org.eclipse.jgit.revwalk.ReachabilityChecker;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 /**
  * Reads an {@link org.eclipse.jgit.lib.ObjectDatabase} for a single thread.
@@ -408,6 +417,54 @@
 	}
 
 	/**
+	 * Create a reachability checker that will use bitmaps if possible.
+	 *
+	 * @param rw
+	 *            revwalk for use by the reachability checker
+	 * @return the most efficient reachability checker for this repository.
+	 * @throws IOException
+	 *             if it cannot open any of the underlying indices.
+	 *
+	 * @since 5.11
+	 */
+	@NonNull
+	public ReachabilityChecker createReachabilityChecker(RevWalk rw)
+			throws IOException {
+		if (getBitmapIndex() != null) {
+			return new BitmappedReachabilityChecker(rw);
+		}
+
+		return new PedestrianReachabilityChecker(true, rw);
+	}
+
+	/**
+	 * Create an object reachability checker that will use bitmaps if possible.
+	 *
+	 * This reachability checker accepts any object as target. For checks
+	 * exclusively between commits, use
+	 * {@link #createReachabilityChecker(RevWalk)}.
+	 *
+	 * @param ow
+	 *            objectwalk for use by the reachability checker
+	 * @return the most efficient object reachability checker for this
+	 *         repository.
+	 *
+	 * @throws IOException
+	 *             if it cannot open any of the underlying indices.
+	 *
+	 * @since 5.11
+	 */
+	@NonNull
+	public ObjectReachabilityChecker createObjectReachabilityChecker(
+			ObjectWalk ow) throws IOException {
+		if (getBitmapIndex() != null) {
+			return new BitmappedObjectReachabilityChecker(ow);
+		}
+
+		return new PedestrianObjectReachabilityChecker(ow);
+	}
+
+	/**
 	 * Get the {@link org.eclipse.jgit.lib.ObjectInserter} from which this
 	 * reader was created using {@code inserter.newReader()}
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
index 6832c9c..7b7bdeb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
@@ -21,6 +21,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
 
@@ -414,6 +417,31 @@
 	}
 
 	/**
+	 * Returns refs whose names start with a given prefix excluding all refs that
+	 * start with one of the given prefixes.
+	 *
+	 * <p>
+	 * The default implementation is not efficient. Implementors of {@link RefDatabase}
+	 * should override this method directly if a better implementation is possible.
+	 * 
+	 * @param include string that names of refs should start with; may be empty.
+	 * @param excludes strings that names of refs can't start with; may be empty.
+	 * @return immutable list of refs whose names start with {@code prefix} and none
+	 *         of the strings in {@code exclude}.
+	 * @throws java.io.IOException the reference space cannot be accessed.
+	 * @since 5.11
+	 */
+	@NonNull
+	public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes)
+			throws IOException {
+		Stream<Ref> refs = getRefs(include).values().stream();
+		for(String exclude: excludes) {
+			refs = refs.filter(r -> !r.getName().startsWith(exclude));
+		}
+		return Collections.unmodifiableList(refs.collect(Collectors.toList()));
+	}
+
+	/**
 	 * Returns refs whose names start with one of the given prefixes.
 	 * <p>
 	 * The default implementation uses {@link #getRefsByPrefix(String)}.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
index a7a832c..1e8a6c9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -127,6 +127,8 @@
 	/** If not bare, the index file caching the working file states. */
 	private final File indexFile;
 
+	private final String initialBranch;
+
 	/**
 	 * Initialize a new repository instance.
 	 *
@@ -138,6 +140,7 @@
 		fs = options.getFS();
 		workTree = options.getWorkTree();
 		indexFile = options.getIndexFile();
+		initialBranch = options.getInitialBranch();
 	}
 
 	/**
@@ -1034,6 +1037,16 @@
 	}
 
 	/**
+	 * Get the initial branch name of a new repository
+	 *
+	 * @return the initial branch name of a new repository
+	 * @since 5.11
+	 */
+	protected @NonNull String getInitialBranch() {
+		return initialBranch;
+	}
+
+	/**
 	 * Objects known to exist but not expressed by {@link #getAllRefs()}.
 	 * <p>
 	 * When a repository borrows objects from another repository, it can
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
index d36ccd5..41f291b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
@@ -14,6 +14,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -193,7 +194,7 @@
 		cache.configureEviction(repositoryCacheConfig);
 	}
 
-	private final ConcurrentHashMap<Key, Repository> cacheMap;
+	private final Map<Key, Repository> cacheMap;
 
 	private final Lock[] openLocks;
 
@@ -201,6 +202,8 @@
 
 	private volatile long expireAfter;
 
+	private final Object schedulerLock = new Lock();
+
 	private RepositoryCache() {
 		cacheMap = new ConcurrentHashMap<>();
 		openLocks = new Lock[4];
@@ -214,7 +217,7 @@
 			RepositoryCacheConfig repositoryCacheConfig) {
 		expireAfter = repositoryCacheConfig.getExpireAfter();
 		ScheduledThreadPoolExecutor scheduler = WorkQueue.getExecutor();
-		synchronized (scheduler) {
+		synchronized (schedulerLock) {
 			if (cleanupTask != null) {
 				cleanupTask.cancel(false);
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java
index 71f0115..facb4a5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2006-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2006, 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
- * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> and others
+ * Copyright (C) 2010, 2020, Chris Aniszczyk <caniszczyk@gmail.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -17,8 +17,13 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
 
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.util.References;
 
 /**
  * Mutable builder to construct an annotated tag recording a project state.
@@ -30,17 +35,22 @@
  * and obtain a {@link org.eclipse.jgit.revwalk.RevTag} instance by calling
  * {@link org.eclipse.jgit.revwalk.RevWalk#parseTag(AnyObjectId)}.
  */
-public class TagBuilder {
+public class TagBuilder extends ObjectBuilder {
+
+	private static final byte[] hobject = Constants.encodeASCII("object"); //$NON-NLS-1$
+
+	private static final byte[] htype = Constants.encodeASCII("type"); //$NON-NLS-1$
+
+	private static final byte[] htag = Constants.encodeASCII("tag"); //$NON-NLS-1$
+
+	private static final byte[] htagger = Constants.encodeASCII("tagger"); //$NON-NLS-1$
+
 	private ObjectId object;
 
 	private int type = Constants.OBJ_BAD;
 
 	private String tag;
 
-	private PersonIdent tagger;
-
-	private String message;
-
 	/**
 	 * Get the type of object this tag refers to.
 	 *
@@ -109,7 +119,7 @@
 	 * @return creator of this tag. May be null.
 	 */
 	public PersonIdent getTagger() {
-		return tagger;
+		return getAuthor();
 	}
 
 	/**
@@ -119,26 +129,7 @@
 	 *            the creator. May be null.
 	 */
 	public void setTagger(PersonIdent taggerIdent) {
-		tagger = taggerIdent;
-	}
-
-	/**
-	 * Get the complete commit message.
-	 *
-	 * @return the complete commit message.
-	 */
-	public String getMessage() {
-		return message;
-	}
-
-	/**
-	 * Set the tag's message.
-	 *
-	 * @param newMessage
-	 *            the tag's message.
-	 */
-	public void setMessage(String newMessage) {
-		message = newMessage;
+		setAuthor(taggerIdent);
 	}
 
 	/**
@@ -147,31 +138,65 @@
 	 * @return this object in the canonical annotated tag format, suitable for
 	 *         storage in a repository.
 	 */
-	public byte[] build() {
+	@Override
+	public byte[] build() throws UnsupportedEncodingException {
 		ByteArrayOutputStream os = new ByteArrayOutputStream();
 		try (OutputStreamWriter w = new OutputStreamWriter(os,
-				UTF_8)) {
-			w.write("object "); //$NON-NLS-1$
-			getObjectId().copyTo(w);
-			w.write('\n');
+				getEncoding())) {
 
-			w.write("type "); //$NON-NLS-1$
-			w.write(Constants.typeString(getObjectType()));
-			w.write("\n"); //$NON-NLS-1$
+			os.write(hobject);
+			os.write(' ');
+			getObjectId().copyTo(os);
+			os.write('\n');
 
-			w.write("tag "); //$NON-NLS-1$
+			os.write(htype);
+			os.write(' ');
+			os.write(Constants
+					.encodeASCII(Constants.typeString(getObjectType())));
+			os.write('\n');
+
+			os.write(htag);
+			os.write(' ');
 			w.write(getTag());
-			w.write("\n"); //$NON-NLS-1$
+			w.flush();
+			os.write('\n');
 
 			if (getTagger() != null) {
-				w.write("tagger "); //$NON-NLS-1$
+				os.write(htagger);
+				os.write(' ');
 				w.write(getTagger().toExternalString());
-				w.write('\n');
+				w.flush();
+				os.write('\n');
 			}
 
-			w.write('\n');
-			if (getMessage() != null)
-				w.write(getMessage());
+			writeEncoding(getEncoding(), os);
+
+			os.write('\n');
+			String msg = getMessage();
+			if (msg != null) {
+				w.write(msg);
+				w.flush();
+			}
+
+			GpgSignature signature = getGpgSignature();
+			if (signature != null) {
+				if (msg != null && !msg.isEmpty() && !msg.endsWith("\n")) { //$NON-NLS-1$
+					// If signed, the message *must* end with a linefeed
+					// character, otherwise signature verification will fail.
+					// (The signature will have been computed over the payload
+					// containing the message without LF, but will be verified
+					// against a payload with the LF.) The signature must start
+					// on a new line.
+					throw new JGitInternalException(
+							JGitText.get().signedTagMessageNoLf);
+				}
+				String externalForm = signature.toExternalString();
+				w.write(externalForm);
+				w.flush();
+				if (!externalForm.endsWith("\n")) { //$NON-NLS-1$
+					os.write('\n');
+				}
+			}
 		} catch (IOException err) {
 			// This should never occur, the only way to get it above is
 			// for the ByteArrayOutputStream to throw, but it doesn't.
@@ -185,10 +210,17 @@
 	 * Format this builder's state as an annotated tag object.
 	 *
 	 * @return this object in the canonical annotated tag format, suitable for
-	 *         storage in a repository.
+	 *         storage in a repository, or {@code null} if the tag cannot be
+	 *         encoded
+	 * @deprecated since 5.11; use {@link #build()} instead
 	 */
+	@Deprecated
 	public byte[] toByteArray() {
-		return build();
+		try {
+			return build();
+		} catch (UnsupportedEncodingException e) {
+			return null;
+		}
 	}
 
 	/** {@inheritDoc} */
@@ -211,14 +243,23 @@
 		r.append(tag != null ? tag : "NOT_SET");
 		r.append("\n");
 
-		if (tagger != null) {
+		if (getTagger() != null) {
 			r.append("tagger ");
-			r.append(tagger);
+			r.append(getTagger());
+			r.append("\n");
+		}
+
+		Charset encoding = getEncoding();
+		if (!References.isSameObject(encoding, UTF_8)) {
+			r.append("encoding ");
+			r.append(encoding.name());
 			r.append("\n");
 		}
 
 		r.append("\n");
-		r.append(message != null ? message : "");
+		r.append(getMessage() != null ? getMessage() : "");
+		GpgSignature signature = getGpgSignature();
+		r.append(signature != null ? signature.toExternalString() : "");
 		r.append("}");
 		return r.toString();
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index 6c217fd..4bfb38d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -703,18 +703,21 @@
 			// conflict between ours and theirs. file/folder conflicts between
 			// base/index/workingTree and something else are not relevant or
 			// detected later
-			if (nonTree(modeO) && !nonTree(modeT)) {
+			if (nonTree(modeO) != nonTree(modeT)) {
+				if (ignoreConflicts) {
+					// In case of merge failures, ignore this path instead of reporting unmerged, so
+					// a caller can use virtual commit. This will not result in files with conflict
+					// markers in the index/working tree. The actual diff on the path will be
+					// computed directly on children.
+					enterSubtree = false;
+					return true;
+				}
 				if (nonTree(modeB))
 					add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
-				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
-				unmergedPaths.add(tw.getPathString());
-				enterSubtree = false;
-				return true;
-			}
-			if (nonTree(modeT) && !nonTree(modeO)) {
-				if (nonTree(modeB))
-					add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
-				add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
+				if (nonTree(modeO))
+					add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
+				if (nonTree(modeT))
+					add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
 				unmergedPaths.add(tw.getPathString());
 				enterSubtree = false;
 				return true;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java b/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java
index d7dd3be..881873d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java
@@ -11,6 +11,7 @@
 package org.eclipse.jgit.nls;
 
 import java.util.Locale;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.jgit.errors.TranslationBundleLoadingException;
@@ -110,7 +111,8 @@
 	}
 
 	private final Locale locale;
-	private final ConcurrentHashMap<Class, TranslationBundle> map = new ConcurrentHashMap<>();
+
+	private final Map<Class, TranslationBundle> map = new ConcurrentHashMap<>();
 
 	private NLS(Locale locale) {
 		this.locale = locale;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java
index b875be9..0cabf07 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java
@@ -93,7 +93,7 @@
 			head = n;
 		} else {
 			Entry p = q.next;
-			while (p != null && p.commit.commitTime > when) {
+			while (p != null && p.commit.commitTime >= when) {
 				q = p;
 				p = q.next;
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
index 4c7a6f5..e6f9580 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
@@ -172,14 +172,14 @@
 	 *             when the index fails to load.
 	 *
 	 * @since 5.8
+	 * @deprecated use
+	 *             {@code ObjectReader#createObjectReachabilityChecker(ObjectWalk)}
+	 *             instead.
 	 */
-	public ObjectReachabilityChecker createObjectReachabilityChecker()
+	@Deprecated
+	public final ObjectReachabilityChecker createObjectReachabilityChecker()
 			throws IOException {
-		if (reader.getBitmapIndex() != null) {
-			return new BitmappedObjectReachabilityChecker(this);
-		}
-
-		return new PedestrianObjectReachabilityChecker(this);
+		return reader.createObjectReachabilityChecker(this);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
index cac2571..b9d1450 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2008-2009, Google Inc.
+ * Copyright (C) 2008, 2009, Google Inc.
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2021, 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
@@ -18,7 +18,9 @@
 import java.nio.charset.Charset;
 import java.nio.charset.IllegalCharsetNameException;
 import java.nio.charset.UnsupportedCharsetException;
+import java.util.Arrays;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -35,6 +37,10 @@
  * An annotated tag.
  */
 public class RevTag extends RevObject {
+
+	private static final byte[] hSignature = Constants
+			.encodeASCII("-----BEGIN PGP SIGNATURE-----"); //$NON-NLS-1$
+
 	/**
 	 * Parse an annotated tag from its canonical format.
 	 *
@@ -171,6 +177,71 @@
 		return RawParseUtils.parsePersonIdent(raw, nameB);
 	}
 
+	private static int nextStart(byte[] prefix, byte[] buffer, int from) {
+		int stop = buffer.length - prefix.length + 1;
+		int ptr = from;
+		if (ptr > 0) {
+			ptr = RawParseUtils.nextLF(buffer, ptr - 1);
+		}
+		while (ptr < stop) {
+			int lineStart = ptr;
+			boolean found = true;
+			for (byte element : prefix) {
+				if (element != buffer[ptr++]) {
+					found = false;
+					break;
+				}
+			}
+			if (found) {
+				return lineStart;
+			}
+			do {
+				ptr = RawParseUtils.nextLF(buffer, ptr);
+			} while (ptr < stop && buffer[ptr] == '\n');
+		}
+		return -1;
+	}
+
+	private int getSignatureStart() {
+		byte[] raw = buffer;
+		int msgB = RawParseUtils.tagMessage(raw, 0);
+		if (msgB < 0) {
+			return msgB;
+		}
+		// Find the last signature start and return the rest
+		int start = nextStart(hSignature, raw, msgB);
+		if (start < 0) {
+			return start;
+		}
+		int next = RawParseUtils.nextLF(raw, start);
+		while (next < raw.length) {
+			int newStart = nextStart(hSignature, raw, next);
+			if (newStart < 0) {
+				break;
+			}
+			start = newStart;
+			next = RawParseUtils.nextLF(raw, start);
+		}
+		return start;
+	}
+
+	/**
+	 * Parse the GPG signature from the raw buffer.
+	 *
+	 * @return contents of the GPG signature; {@code null} if the tag was not
+	 *         signed.
+	 * @since 5.11
+	 */
+	@Nullable
+	public final byte[] getRawGpgSignature() {
+		byte[] raw = buffer;
+		int start = getSignatureStart();
+		if (start < 0) {
+			return null;
+		}
+		return Arrays.copyOfRange(raw, start, raw.length);
+	}
+
 	/**
 	 * Parse the complete tag message and decode it to a string.
 	 * <p>
@@ -187,7 +258,12 @@
 		if (msgB < 0) {
 			return ""; //$NON-NLS-1$
 		}
-		return RawParseUtils.decode(guessEncoding(), raw, msgB, raw.length);
+		int signatureStart = getSignatureStart();
+		int end = signatureStart < 0 ? raw.length : signatureStart;
+		if (end == msgB) {
+			return ""; //$NON-NLS-1$
+		}
+		return RawParseUtils.decode(guessEncoding(), raw, msgB, end);
 	}
 
 	/**
@@ -213,6 +289,16 @@
 		}
 
 		int msgE = RawParseUtils.endOfParagraph(raw, msgB);
+		int signatureStart = getSignatureStart();
+		if (signatureStart >= msgB && msgE > signatureStart) {
+			msgE = signatureStart;
+			if (msgE > msgB) {
+				msgE--;
+			}
+			if (msgB == msgE) {
+				return ""; //$NON-NLS-1$
+			}
+		}
 		String str = RawParseUtils.decode(guessEncoding(), raw, msgB, msgE);
 		if (RevCommit.hasLF(raw, msgB, msgE)) {
 			str = StringUtils.replaceLineBreaksWithSpace(str);
@@ -258,6 +344,22 @@
 	}
 
 	/**
+	 * Obtain the raw unparsed tag body (<b>NOTE - THIS IS NOT A COPY</b>).
+	 * <p>
+	 * This method is exposed only to provide very fast, efficient access to
+	 * this tag's message buffer. Applications relying on this buffer should be
+	 * very careful to ensure they do not modify its contents during their use
+	 * of it.
+	 *
+	 * @return the raw unparsed tag body. This is <b>NOT A COPY</b>. Do not
+	 *         alter the returned array.
+	 * @since 5.11
+	 */
+	public final byte[] getRawBuffer() {
+		return buffer;
+	}
+
+	/**
 	 * Discard the message buffer to reduce memory usage.
 	 * <p>
 	 * After discarding the memory usage of the {@code RevTag} is reduced to
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 6b62fcd..631d861 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
@@ -236,13 +236,13 @@
 	 *             if it cannot open any of the underlying indices.
 	 *
 	 * @since 5.4
+	 * @deprecated use {@code ObjectReader#createReachabilityChecker(RevWalk)}
+	 *             instead.
 	 */
-	public ReachabilityChecker createReachabilityChecker() throws IOException {
-		if (reader.getBitmapIndex() != null) {
-			return new BitmappedReachabilityChecker(this);
-		}
-
-		return new PedestrianReachabilityChecker(true, this);
+	@Deprecated
+	public final ReachabilityChecker createReachabilityChecker()
+			throws IOException {
+		return reader.createReachabilityChecker(this);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
index 3a36398..3826bf7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 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
@@ -13,7 +13,12 @@
 
 package org.eclipse.jgit.transport;
 
+import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_LS_REFS;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
+import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_PEELED;
+import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_SYMREF_TARGET;
+import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_1;
+import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2;
 
 import java.io.EOFException;
 import java.io.IOException;
@@ -22,23 +27,29 @@
 import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
 import org.eclipse.jgit.errors.PackProtocolException;
 import org.eclipse.jgit.errors.RemoteRepositoryException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.io.InterruptTimer;
 import org.eclipse.jgit.util.io.TimeoutInputStream;
 import org.eclipse.jgit.util.io.TimeoutOutputStream;
@@ -92,17 +103,27 @@
 	protected boolean statelessRPC;
 
 	/** Capability tokens advertised by the remote side. */
-	private final Set<String> remoteCapablities = new HashSet<>();
+	private final Map<String, String> remoteCapabilities = new HashMap<>();
 
 	/** Extra objects the remote has, but which aren't offered as refs. */
 	protected final Set<ObjectId> additionalHaves = new HashSet<>();
 
+	private TransferConfig.ProtocolVersion protocol = TransferConfig.ProtocolVersion.V0;
+
 	BasePackConnection(PackTransport packTransport) {
 		transport = (Transport) packTransport;
 		local = transport.local;
 		uri = transport.uri;
 	}
 
+	TransferConfig.ProtocolVersion getProtocolVersion() {
+		return protocol;
+	}
+
+	void setProtocolVersion(@NonNull TransferConfig.ProtocolVersion protocol) {
+		this.protocol = protocol;
+	}
+
 	/**
 	 * Configure this connection with the directional pipes.
 	 *
@@ -147,12 +168,15 @@
 	 * {@link #close()} and the exception is wrapped (if necessary) and thrown
 	 * as a {@link org.eclipse.jgit.errors.TransportException}.
 	 *
+	 * @return {@code true} if the refs were read; {@code false} otherwise
+	 *         indicating that {@link #lsRefs} must be called
+	 *
 	 * @throws org.eclipse.jgit.errors.TransportException
 	 *             the reference list could not be scanned.
 	 */
-	protected void readAdvertisedRefs() throws TransportException {
+	protected boolean readAdvertisedRefs() throws TransportException {
 		try {
-			readAdvertisedRefsImpl();
+			return readAdvertisedRefsImpl();
 		} catch (TransportException err) {
 			close();
 			throw err;
@@ -162,35 +186,79 @@
 		}
 	}
 
-	private void readAdvertisedRefsImpl() throws IOException {
-		final LinkedHashMap<String, Ref> avail = new LinkedHashMap<>();
-		for (;;) {
+	private String readLine() throws IOException {
+		String line = pckIn.readString();
+		if (PacketLineIn.isEnd(line)) {
+			return null;
+		}
+		if (line.startsWith("ERR ")) { //$NON-NLS-1$
+			// This is a customized remote service error.
+			// Users should be informed about it.
+			throw new RemoteRepositoryException(uri, line.substring(4));
+		}
+		return line;
+	}
+
+	private boolean readAdvertisedRefsImpl() throws IOException {
+		final Map<String, Ref> avail = new LinkedHashMap<>();
+		final Map<String, String> symRefs = new LinkedHashMap<>();
+		for (boolean first = true;; first = false) {
 			String line;
 
-			try {
-				line = pckIn.readString();
-			} catch (EOFException eof) {
-				if (avail.isEmpty())
-					throw noRepository();
-				throw eof;
-			}
-			if (PacketLineIn.isEnd(line))
-				break;
-
-			if (line.startsWith("ERR ")) { //$NON-NLS-1$
-				// This is a customized remote service error.
-				// Users should be informed about it.
-				throw new RemoteRepositoryException(uri, line.substring(4));
-			}
-
-			if (avail.isEmpty()) {
+			if (first) {
+				boolean isV1 = false;
+				try {
+					line = readLine();
+				} catch (EOFException e) {
+					TransportException noRepo = noRepository();
+					noRepo.initCause(e);
+					throw noRepo;
+				}
+				if (line != null && VERSION_1.equals(line)) {
+					// Same as V0, except for this extra line. We shouldn't get
+					// it since we never request V1.
+					setProtocolVersion(TransferConfig.ProtocolVersion.V0);
+					isV1 = true;
+					line = readLine();
+				}
+				if (line == null) {
+					break;
+				}
 				final int nul = line.indexOf('\0');
 				if (nul >= 0) {
-					// The first line (if any) may contain "hidden"
-					// capability values after a NUL byte.
-					remoteCapablities.addAll(
-							Arrays.asList(line.substring(nul + 1).split(" "))); //$NON-NLS-1$
+					// Protocol V0: The first line (if any) may contain
+					// "hidden" capability values after a NUL byte.
+					for (String capability : line.substring(nul + 1)
+							.split(" ")) { //$NON-NLS-1$
+						if (capability.startsWith(CAPABILITY_SYMREF_PREFIX)) {
+							String[] parts = capability
+									.substring(
+											CAPABILITY_SYMREF_PREFIX.length())
+									.split(":", 2); //$NON-NLS-1$
+							if (parts.length == 2) {
+								symRefs.put(parts[0], parts[1]);
+							}
+						} else {
+							addCapability(capability);
+						}
+					}
 					line = line.substring(0, nul);
+					setProtocolVersion(TransferConfig.ProtocolVersion.V0);
+				} else if (!isV1 && VERSION_2.equals(line)) {
+					// Protocol V2: remaining lines are capabilities as
+					// key=value pairs
+					setProtocolVersion(TransferConfig.ProtocolVersion.V2);
+					readCapabilitiesV2();
+					// Break out here so that stateless RPC transports get a
+					// chance to set up the output stream.
+					return false;
+				} else {
+					setProtocolVersion(TransferConfig.ProtocolVersion.V0);
+				}
+			} else {
+				line = readLine();
+				if (line == null) {
+					break;
 				}
 			}
 
@@ -199,73 +267,214 @@
 				throw invalidRefAdvertisementLine(line);
 			}
 			String name = line.substring(41, line.length());
-			if (avail.isEmpty() && name.equals("capabilities^{}")) { //$NON-NLS-1$
-				// special line from git-receive-pack to show
+			if (first && name.equals("capabilities^{}")) { //$NON-NLS-1$
+				// special line from git-receive-pack (protocol V0) to show
 				// capabilities when there are no refs to advertise
 				continue;
 			}
 
-			final ObjectId id;
-			try {
-				id  = ObjectId.fromString(line.substring(0, 40));
-			} catch (InvalidObjectIdException e) {
-				PackProtocolException ppe = invalidRefAdvertisementLine(line);
-				ppe.initCause(e);
-				throw ppe;
-			}
+			final ObjectId id = toId(line, line.substring(0, 40));
 			if (name.equals(".have")) { //$NON-NLS-1$
 				additionalHaves.add(id);
-			} else if (name.endsWith("^{}")) { //$NON-NLS-1$
-				name = name.substring(0, name.length() - 3);
-				final Ref prior = avail.get(name);
-				if (prior == null)
-					throw new PackProtocolException(uri, MessageFormat.format(
-							JGitText.get().advertisementCameBefore, name, name));
-
-				if (prior.getPeeledObjectId() != null)
-					throw duplicateAdvertisement(name + "^{}"); //$NON-NLS-1$
-
-				avail.put(name, new ObjectIdRef.PeeledTag(
-						Ref.Storage.NETWORK, name, prior.getObjectId(), id));
 			} else {
-				final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag(
-						Ref.Storage.NETWORK, name, id));
-				if (prior != null)
-					throw duplicateAdvertisement(name);
+				processLineV1(name, id, avail);
 			}
 		}
-		updateWithSymRefs(avail, extractSymRefsFromCapabilities(remoteCapablities));
+		updateWithSymRefs(avail, symRefs);
 		available(avail);
+		return true;
 	}
 
 	/**
-	 * Finds values in the given capabilities of the form:
+	 * Issue a protocol V2 ls-refs command and read its response.
 	 *
-	 * <pre>
-	 * symref=<em>source</em>:<em>target</em>
-	 * </pre>
-	 *
-	 * And returns a Map of source->target entries.
-	 *
-	 * @param capabilities
-	 *            the capabilities lines
-	 * @return a Map of the symref entries from capabilities
-	 * @throws NullPointerException
-	 *             if capabilities, or any entry in it, is null
+	 * @param refSpecs
+	 *            to produce ref prefixes from if the server supports git
+	 *            protocol V2
+	 * @param additionalPatterns
+	 *            to use for ref prefixes if the server supports git protocol V2
+	 * @throws TransportException
+	 *             if the command could not be run or its output not be read
 	 */
-	static Map<String, String> extractSymRefsFromCapabilities(Collection<String> capabilities) {
+	protected void lsRefs(Collection<RefSpec> refSpecs,
+			String... additionalPatterns) throws TransportException {
+		try {
+			lsRefsImpl(refSpecs, additionalPatterns);
+		} catch (TransportException err) {
+			close();
+			throw err;
+		} catch (IOException | RuntimeException err) {
+			close();
+			throw new TransportException(err.getMessage(), err);
+		}
+	}
+
+	private void lsRefsImpl(Collection<RefSpec> refSpecs,
+			String... additionalPatterns) throws IOException {
+		pckOut.writeString("command=" + COMMAND_LS_REFS); //$NON-NLS-1$
+		// Add the user-agent
+		String agent = UserAgent.get();
+		if (agent != null && isCapableOf(OPTION_AGENT)) {
+			pckOut.writeString(OPTION_AGENT + '=' + agent);
+		}
+		pckOut.writeDelim();
+		pckOut.writeString("peel"); //$NON-NLS-1$
+		pckOut.writeString("symrefs"); //$NON-NLS-1$
+		for (String refPrefix : getRefPrefixes(refSpecs, additionalPatterns)) {
+			pckOut.writeString("ref-prefix " + refPrefix); //$NON-NLS-1$
+		}
+		pckOut.end();
+		final Map<String, Ref> avail = new LinkedHashMap<>();
 		final Map<String, String> symRefs = new LinkedHashMap<>();
-		for (String option : capabilities) {
-			if (option.startsWith(CAPABILITY_SYMREF_PREFIX)) {
-				String[] symRef = option
-						.substring(CAPABILITY_SYMREF_PREFIX.length())
-						.split(":", 2); //$NON-NLS-1$
-				if (symRef.length == 2) {
-					symRefs.put(symRef[0], symRef[1]);
-				}
+		for (;;) {
+			String line = readLine();
+			if (line == null) {
+				break;
+			}
+			// Expecting to get a line in the form "sha1 refname"
+			if (line.length() < 41 || line.charAt(40) != ' ') {
+				throw invalidRefAdvertisementLine(line);
+			}
+			String name = line.substring(41, line.length());
+			final ObjectId id = toId(line, line.substring(0, 40));
+			if (name.equals(".have")) { //$NON-NLS-1$
+				additionalHaves.add(id);
+			} else {
+				processLineV2(line, id, name, avail, symRefs);
 			}
 		}
-		return symRefs;
+		updateWithSymRefs(avail, symRefs);
+		available(avail);
+	}
+
+	private Collection<String> getRefPrefixes(Collection<RefSpec> refSpecs,
+			String... additionalPatterns) {
+		if (refSpecs.isEmpty() && (additionalPatterns == null
+				|| additionalPatterns.length == 0)) {
+			return Collections.emptyList();
+		}
+		Set<String> patterns = new HashSet<>();
+		if (additionalPatterns != null) {
+			Arrays.stream(additionalPatterns).filter(Objects::nonNull)
+					.forEach(patterns::add);
+		}
+		for (RefSpec spec : refSpecs) {
+			// TODO: for now we only do protocol V2 for fetch. For push
+			// RefSpecs, the logic would need to be different. (At the
+			// minimum, take spec.getDestination().)
+			String src = spec.getSource();
+			if (ObjectId.isId(src)) {
+				continue;
+			}
+			if (spec.isWildcard()) {
+				patterns.add(src.substring(0, src.indexOf('*')));
+			} else {
+				patterns.add(src);
+				patterns.add(Constants.R_REFS + src);
+				patterns.add(Constants.R_HEADS + src);
+				patterns.add(Constants.R_TAGS + src);
+			}
+		}
+		return patterns;
+	}
+
+	private void readCapabilitiesV2() throws IOException {
+		// In git protocol V2, capabilities are different. If it's a key-value
+		// pair, the key may be a command name, and the value a space-separated
+		// list of capabilities for that command. We still store it in the same
+		// map as for protocol v0/v1. Protocol v2 code has to account for this.
+		for (;;) {
+			String line = readLine();
+			if (line == null) {
+				break;
+			}
+			addCapability(line);
+		}
+	}
+
+	private void addCapability(String capability) {
+		String parts[] = capability.split("=", 2); //$NON-NLS-1$
+		if (parts.length == 2) {
+			remoteCapabilities.put(parts[0], parts[1]);
+		}
+		remoteCapabilities.put(capability, null);
+	}
+
+	private ObjectId toId(String line, String value)
+			throws PackProtocolException {
+		try {
+			return ObjectId.fromString(value);
+		} catch (InvalidObjectIdException e) {
+			PackProtocolException ppe = invalidRefAdvertisementLine(line);
+			ppe.initCause(e);
+			throw ppe;
+		}
+	}
+
+	private void processLineV1(String name, ObjectId id, Map<String, Ref> avail)
+			throws IOException {
+		if (name.endsWith("^{}")) { //$NON-NLS-1$
+			name = name.substring(0, name.length() - 3);
+			final Ref prior = avail.get(name);
+			if (prior == null) {
+				throw new PackProtocolException(uri, MessageFormat.format(
+						JGitText.get().advertisementCameBefore, name, name));
+			}
+			if (prior.getPeeledObjectId() != null) {
+				throw duplicateAdvertisement(name + "^{}"); //$NON-NLS-1$
+			}
+			avail.put(name, new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name,
+					prior.getObjectId(), id));
+		} else {
+			final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag(
+					Ref.Storage.NETWORK, name, id));
+			if (prior != null) {
+				throw duplicateAdvertisement(name);
+			}
+		}
+	}
+
+	private void processLineV2(String line, ObjectId id, String rest,
+			Map<String, Ref> avail, Map<String, String> symRefs)
+			throws IOException {
+		String[] parts = rest.split(" "); //$NON-NLS-1$
+		String name = parts[0];
+		// Two attributes possible, symref-target or peeled
+		String symRefTarget = null;
+		String peeled = null;
+		for (int i = 1; i < parts.length; i++) {
+			if (parts[i].startsWith(REF_ATTR_SYMREF_TARGET)) {
+				if (symRefTarget != null) {
+					throw new PackProtocolException(uri, MessageFormat.format(
+							JGitText.get().duplicateRefAttribute, line));
+				}
+				symRefTarget = parts[i]
+						.substring(REF_ATTR_SYMREF_TARGET.length());
+			} else if (parts[i].startsWith(REF_ATTR_PEELED)) {
+				if (peeled != null) {
+					throw new PackProtocolException(uri, MessageFormat.format(
+							JGitText.get().duplicateRefAttribute, line));
+				}
+				peeled = parts[i].substring(REF_ATTR_PEELED.length());
+			}
+			if (peeled != null && symRefTarget != null) {
+				break;
+			}
+		}
+		Ref idRef;
+		if (peeled != null) {
+			idRef = new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name, id,
+					toId(line, peeled));
+		} else {
+			idRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK, name, id);
+		}
+		Ref prior = avail.put(name, idRef);
+		if (prior != null) {
+			throw duplicateAdvertisement(name);
+		}
+		if (!StringUtils.isEmptyOrNull(symRefTarget)) {
+			symRefs.put(name, symRefTarget);
+		}
 	}
 
 	/**
@@ -334,6 +543,22 @@
 				}
 			}
 		}
+		// If HEAD is still in the symRefs map here, the real ref was not
+		// reported, but we know it must point to the object reported for HEAD.
+		// So fill it in in the refMap.
+		String headRefName = symRefs.get(Constants.HEAD);
+		if (headRefName != null && !refMap.containsKey(headRefName)) {
+			Ref headRef = refMap.get(Constants.HEAD);
+			if (headRef != null) {
+				ObjectId headObj = headRef.getObjectId();
+				headRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK,
+						headRefName, headObj);
+				refMap.put(headRefName, headRef);
+				headRef = new SymbolicRef(Constants.HEAD, headRef);
+				refMap.put(Constants.HEAD, headRef);
+				symRefs.remove(Constants.HEAD);
+			}
+		}
 	}
 
 	/**
@@ -357,7 +582,7 @@
 	 * @return whether this option is supported
 	 */
 	protected boolean isCapableOf(String option) {
-		return remoteCapablities.contains(option);
+		return remoteCapabilities.containsKey(option);
 	}
 
 	/**
@@ -378,6 +603,17 @@
 	}
 
 	/**
+	 * Return a capability value.
+	 *
+	 * @param option
+	 *            to get
+	 * @return the value stored, if any.
+	 */
+	protected String getCapability(String option) {
+		return remoteCapabilities.get(option);
+	}
+
+	/**
 	 * Add user agent capability
 	 *
 	 * @param b
@@ -385,7 +621,7 @@
 	 */
 	protected void addUserAgentCapability(StringBuilder b) {
 		String a = UserAgent.get();
-		if (a != null && UserAgent.hasAgent(remoteCapablities)) {
+		if (a != null && remoteCapabilities.get(OPTION_AGENT) != null) {
 			b.append(' ').append(OPTION_AGENT).append('=').append(a);
 		}
 	}
@@ -393,7 +629,8 @@
 	/** {@inheritDoc} */
 	@Override
 	public String getPeerUserAgent() {
-		return UserAgent.getAgent(remoteCapablities, super.getPeerUserAgent());
+		String agent = remoteCapabilities.get(OPTION_AGENT);
+		return agent != null ? agent : super.getPeerUserAgent();
 	}
 
 	private PackProtocolException duplicateAdvertisement(String name) {
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 a2fb51f..d344dea 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, 2010 Google Inc.
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 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
@@ -16,18 +16,21 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.text.MessageFormat;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Set;
 
 import org.eclipse.jgit.errors.PackProtocolException;
+import org.eclipse.jgit.errors.RemoteRepositoryException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.file.PackLock;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
@@ -44,6 +47,7 @@
 import org.eclipse.jgit.revwalk.filter.RevFilter;
 import org.eclipse.jgit.transport.GitProtocolConstants.MultiAck;
 import org.eclipse.jgit.transport.PacketLineIn.AckNackResult;
+import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.TemporaryBuffer;
 
 /**
@@ -207,7 +211,10 @@
 
 	private int maxHaves;
 
-	/** RPC state, if {@link BasePackConnection#statelessRPC} is true. */
+	/**
+	 * RPC state, if {@link BasePackConnection#statelessRPC} is true or protocol
+	 * V2 is used.
+	 */
 	private TemporaryBuffer.Heap state;
 
 	private PacketLineOut pckState;
@@ -321,6 +328,13 @@
 		return Collections.<PackLock> emptyList();
 	}
 
+	private void clearState() {
+		walk.dispose();
+		reachableCommits = null;
+		state = null;
+		pckState = null;
+	}
+
 	/**
 	 * Execute common ancestor negotiation and fetch the objects.
 	 *
@@ -349,18 +363,34 @@
 			markRefsAdvertised();
 			markReachable(have, maxTimeWanted(want));
 
+			if (TransferConfig.ProtocolVersion.V2
+					.equals(getProtocolVersion())) {
+				// Protocol V2 always is a "stateless" protocol, even over a
+				// bidirectional pipe: the server serves one "fetch" request and
+				// then forgets anything it has learned, so the next fetch
+				// request has to re-send all wants and previously determined
+				// common objects as "have"s again.
+				state = new TemporaryBuffer.Heap(Integer.MAX_VALUE);
+				pckState = new PacketLineOut(state);
+				try {
+					doFetchV2(monitor, want, outputStream);
+				} finally {
+					clearState();
+				}
+				return;
+			}
+			// Protocol V0/1
 			if (statelessRPC) {
 				state = new TemporaryBuffer.Heap(Integer.MAX_VALUE);
 				pckState = new PacketLineOut(state);
 			}
-
-			if (sendWants(want)) {
+			PacketLineOut output = statelessRPC ? pckState : pckOut;
+			if (sendWants(want, output)) {
+				output.end();
+				outNeedsEnd = false;
 				negotiate(monitor);
 
-				walk.dispose();
-				reachableCommits = null;
-				state = null;
-				pckState = null;
+				clearState();
 
 				receivePack(monitor, outputStream);
 			}
@@ -373,6 +403,185 @@
 		}
 	}
 
+	private void doFetchV2(ProgressMonitor monitor, Collection<Ref> want,
+			OutputStream outputStream) throws IOException, CancelledException {
+		sideband = true;
+		negotiateBegin();
+
+		pckState.writeString("command=" + GitProtocolConstants.COMMAND_FETCH); //$NON-NLS-1$
+		// Capabilities are sent as command arguments in protocol V2
+		String agent = UserAgent.get();
+		if (agent != null && isCapableOf(GitProtocolConstants.OPTION_AGENT)) {
+			pckState.writeString(
+					GitProtocolConstants.OPTION_AGENT + '=' + agent);
+		}
+		Set<String> capabilities = new HashSet<>();
+		String advertised = getCapability(GitProtocolConstants.COMMAND_FETCH);
+		if (!StringUtils.isEmptyOrNull(advertised)) {
+			capabilities.addAll(Arrays.asList(advertised.split("\\s+"))); //$NON-NLS-1$
+		}
+		// Arguments
+		pckState.writeDelim();
+		for (String capability : getCapabilitiesV2(capabilities)) {
+			pckState.writeString(capability);
+		}
+		if (!sendWants(want, pckState)) {
+			// We already have everything we wanted.
+			return;
+		}
+		// If we send something, we always close it properly ourselves.
+		outNeedsEnd = false;
+
+		FetchStateV2 fetchState = new FetchStateV2();
+		boolean sentDone = false;
+		for (;;) {
+			// The "state" buffer contains the full fetch request with all
+			// common objects found so far.
+			state.writeTo(out, monitor);
+			sentDone = sendNextHaveBatch(fetchState, pckOut, monitor);
+			if (sentDone) {
+				break;
+			}
+			if (readAcknowledgments(fetchState, pckIn, monitor)) {
+				// We got a "ready": next should be a patch file.
+				break;
+			}
+			// Note: C git reads and requires here (and after a packfile) a
+			// "0002" packet in stateless RPC transports (https). This "response
+			// end" packet is even mentioned in the protocol V2 technical
+			// documentation. However, it is not actually part of the public
+			// protocol; it occurs only in an internal protocol wrapper in the C
+			// git implementation.
+		}
+		clearState();
+		String line = pckIn.readString();
+		// If we sent a done, we may have an error reply here.
+		if (sentDone && line.startsWith("ERR ")) { //$NON-NLS-1$
+			throw new RemoteRepositoryException(uri, line.substring(4));
+		}
+		// "shallow-info", "wanted-refs", and "packfile-uris" would have to be
+		// handled here in that order.
+		if (!GitProtocolConstants.SECTION_PACKFILE.equals(line)) {
+			throw new PackProtocolException(
+					MessageFormat.format(JGitText.get().expectedGot,
+							GitProtocolConstants.SECTION_PACKFILE, line));
+		}
+		receivePack(monitor, outputStream);
+	}
+
+	/**
+	 * Sends the next batch of "have"s and terminates the {@code output}.
+	 *
+	 * @param fetchState
+	 *            is updated with information about the number of items written,
+	 *            and whether to expect a packfile next
+	 * @param output
+	 *            to write to
+	 * @param monitor
+	 *            for progress reporting and cancellation
+	 * @return {@code true} if a "done" was written and we should thus expect a
+	 *         packfile next
+	 * @throws IOException
+	 *             on errors
+	 * @throws CancelledException
+	 *             on cancellation
+	 */
+	private boolean sendNextHaveBatch(FetchStateV2 fetchState,
+			PacketLineOut output, ProgressMonitor monitor)
+			throws IOException, CancelledException {
+		long n = 0;
+		while (n < fetchState.havesToSend) {
+			final RevCommit c = walk.next();
+			if (c == null) {
+				break;
+			}
+			output.writeString("have " + c.getId().name() + '\n'); //$NON-NLS-1$
+			n++;
+			if (n % 10 == 0 && monitor.isCancelled()) {
+				throw new CancelledException();
+			}
+		}
+		fetchState.havesTotal += n;
+		if (n == 0
+				|| (fetchState.hadAcks
+						&& fetchState.havesWithoutAck > MAX_HAVES)
+				|| fetchState.havesTotal > maxHaves) {
+			output.writeString("done\n"); //$NON-NLS-1$
+			output.end();
+			return true;
+		}
+		// Increment only after the test above. Of course we have no ACKs yet
+		// for the newly added "have"s, so it makes no sense to count them
+		// against the MAX_HAVES limit.
+		fetchState.havesWithoutAck += n;
+		output.end();
+		fetchState.incHavesToSend(statelessRPC);
+		return false;
+	}
+
+	/**
+	 * Reads and processes acknowledgments, adding ACKed objects as "have"s to
+	 * the global state {@link TemporaryBuffer}.
+	 *
+	 * @param fetchState
+	 *            to update
+	 * @param input
+	 *            to read from
+	 * @param monitor
+	 *            for progress reporting and cancellation
+	 * @return {@code true} if a "ready" was received and a packfile is expected
+	 *         next
+	 * @throws IOException
+	 *             on errors
+	 * @throws CancelledException
+	 *             on cancellation
+	 */
+	private boolean readAcknowledgments(FetchStateV2 fetchState,
+			PacketLineIn input, ProgressMonitor monitor)
+			throws IOException, CancelledException {
+		String line = input.readString();
+		if (!GitProtocolConstants.SECTION_ACKNOWLEDGMENTS.equals(line)) {
+			throw new PackProtocolException(MessageFormat.format(
+					JGitText.get().expectedGot,
+					GitProtocolConstants.SECTION_ACKNOWLEDGMENTS, line));
+		}
+		MutableObjectId returnedId = new MutableObjectId();
+		line = input.readString();
+		boolean gotReady = false;
+		long n = 0;
+		while (!PacketLineIn.isEnd(line) && !PacketLineIn.isDelimiter(line)) {
+			AckNackResult ack = PacketLineIn.parseACKv2(line, returnedId);
+			// If we got a "ready", we just skip the remaining lines after
+			// having checked them for being valid. (Normally, the "ready"
+			// should be the last line anyway.)
+			if (!gotReady) {
+				if (ack == AckNackResult.ACK_COMMON) {
+					// markCommon appends the object to the "state"
+					markCommon(walk.parseAny(returnedId), ack, true);
+					fetchState.havesWithoutAck = 0;
+					fetchState.hadAcks = true;
+				} else if (ack == AckNackResult.ACK_READY) {
+					gotReady = true;
+				}
+			}
+			n++;
+			if (n % 10 == 0 && monitor.isCancelled()) {
+				throw new CancelledException();
+			}
+			line = input.readString();
+		}
+		if (gotReady) {
+			if (!PacketLineIn.isDelimiter(line)) {
+				throw new PackProtocolException(MessageFormat
+						.format(JGitText.get().expectedGot, "0001", line)); //$NON-NLS-1$
+			}
+		} else if (!PacketLineIn.isEnd(line)) {
+			throw new PackProtocolException(MessageFormat
+					.format(JGitText.get().expectedGot, "0000", line)); //$NON-NLS-1$
+		}
+		return gotReady;
+	}
+
 	/** {@inheritDoc} */
 	@Override
 	public void close() {
@@ -456,8 +665,8 @@
 		}
 	}
 
-	private boolean sendWants(Collection<Ref> want) throws IOException {
-		final PacketLineOut p = statelessRPC ? pckState : pckOut;
+	private boolean sendWants(Collection<Ref> want, PacketLineOut p)
+			throws IOException {
 		boolean first = true;
 		for (Ref r : want) {
 			ObjectId objectId = r.getObjectId();
@@ -479,10 +688,11 @@
 			final StringBuilder line = new StringBuilder(46);
 			line.append("want "); //$NON-NLS-1$
 			line.append(objectId.name());
-			if (first) {
+			if (first && TransferConfig.ProtocolVersion.V0
+					.equals(getProtocolVersion())) {
 				line.append(enableCapabilities());
-				first = false;
 			}
+			first = false;
 			line.append('\n');
 			p.writeString(line.toString());
 		}
@@ -492,11 +702,34 @@
 		if (!filterSpec.isNoOp()) {
 			p.writeString(filterSpec.filterLine());
 		}
-		p.end();
-		outNeedsEnd = false;
 		return true;
 	}
 
+	private Set<String> getCapabilitiesV2(Set<String> advertisedCapabilities)
+			throws TransportException {
+		Set<String> capabilities = new LinkedHashSet<>();
+		// Protocol V2 is implicitly capable of all these.
+		if (noProgress) {
+			capabilities.add(OPTION_NO_PROGRESS);
+		}
+		if (includeTags) {
+			capabilities.add(OPTION_INCLUDE_TAG);
+		}
+		if (allowOfsDelta) {
+			capabilities.add(OPTION_OFS_DELTA);
+		}
+		if (thinPack) {
+			capabilities.add(OPTION_THIN_PACK);
+		}
+		if (!filterSpec.isNoOp()
+				&& !advertisedCapabilities.contains(OPTION_FILTER)) {
+			throw new PackProtocolException(uri,
+					JGitText.get().filterRequiresCapability);
+		}
+		// The FilterSpec will be added later in sendWants().
+		return capabilities;
+	}
+
 	private String enableCapabilities() throws TransportException {
 		final StringBuilder line = new StringBuilder();
 		if (noProgress)
@@ -622,7 +855,7 @@
 					// we need to continue to talk about other parts of
 					// our local history.
 					//
-					markCommon(walk.parseAny(ackId), anr);
+					markCommon(walk.parseAny(ackId), anr, statelessRPC);
 					receivedAck = true;
 					receivedContinue = true;
 					havesSinceLastContinue = 0;
@@ -757,16 +990,10 @@
 		}
 	}
 
-	private void markCommon(RevObject obj, AckNackResult anr)
+	private void markCommon(RevObject obj, AckNackResult anr, boolean useState)
 			throws IOException {
-		if (statelessRPC && anr == AckNackResult.ACK_COMMON && !obj.has(STATE)) {
-			StringBuilder s;
-
-			s = new StringBuilder(6 + Constants.OBJECT_ID_STRING_LENGTH);
-			s.append("have "); //$NON-NLS-1$
-			s.append(obj.name());
-			s.append('\n');
-			pckState.writeString(s.toString());
+		if (useState && anr == AckNackResult.ACK_COMMON && !obj.has(STATE)) {
+			pckState.writeString("have " + obj.name() + '\n'); //$NON-NLS-1$
 			obj.add(STATE);
 		}
 		obj.add(COMMON);
@@ -806,4 +1033,31 @@
 	private static class CancelledException extends Exception {
 		private static final long serialVersionUID = 1L;
 	}
+
+	private static class FetchStateV2 {
+
+		long havesToSend = 32;
+
+		long havesTotal;
+
+		// Set to true if we got at least one ACK in protocol V2.
+		boolean hadAcks;
+
+		// Counts haves without ACK. Use as cutoff for negotiation only once
+		// hadAcks == true.
+		long havesWithoutAck;
+
+		void incHavesToSend(boolean statelessRPC) {
+			if (statelessRPC) {
+				// Increase this quicker since connection setup costs accumulate
+				if (havesToSend < 16384) {
+					havesToSend *= 2;
+				} else {
+					havesToSend = havesToSend * 11 / 10;
+				}
+			} else {
+				havesToSend += 32;
+			}
+		}
+	}
 }
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 0f1892a..34bad6e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 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
@@ -48,6 +48,7 @@
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.revwalk.ObjectWalk;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.StringUtils;
 
 class FetchProcess {
 	/** Transport we will fetch over. */
@@ -79,7 +80,8 @@
 		toFetch = f;
 	}
 
-	void execute(ProgressMonitor monitor, FetchResult result)
+	void execute(ProgressMonitor monitor, FetchResult result,
+			String initialBranch)
 			throws NotSupportedException, TransportException {
 		askFor.clear();
 		localUpdates.clear();
@@ -87,24 +89,64 @@
 		packLocks.clear();
 		localRefs = null;
 
+		Throwable e1 = null;
 		try {
-			executeImp(monitor, result);
+			executeImp(monitor, result, initialBranch);
+		} catch (NotSupportedException | TransportException err) {
+			e1 = err;
+			throw err;
 		} finally {
 			try {
-			for (PackLock lock : packLocks)
-				lock.unlock();
+				for (PackLock lock : packLocks) {
+					lock.unlock();
+				}
 			} catch (IOException e) {
+				if (e1 != null) {
+					e.addSuppressed(e1);
+				}
 				throw new TransportException(e.getMessage(), e);
 			}
 		}
 	}
 
+	private boolean isInitialBranchMissing(Map<String, Ref> refsMap,
+			String initialBranch) {
+		if (StringUtils.isEmptyOrNull(initialBranch) || refsMap.isEmpty()) {
+			return false;
+		}
+		if (refsMap.containsKey(initialBranch)
+				|| refsMap.containsKey(Constants.R_HEADS + initialBranch)
+				|| refsMap.containsKey(Constants.R_TAGS + initialBranch)) {
+			return false;
+		}
+		return true;
+	}
+
 	private void executeImp(final ProgressMonitor monitor,
-			final FetchResult result) throws NotSupportedException,
-			TransportException {
-		conn = transport.openFetch();
+			final FetchResult result, String initialBranch)
+			throws NotSupportedException, TransportException {
+		final TagOpt tagopt = transport.getTagOpt();
+		String getTags = (tagopt == TagOpt.NO_TAGS) ? null : Constants.R_TAGS;
+		String getHead = null;
 		try {
-			result.setAdvertisedRefs(transport.getURI(), conn.getRefsMap());
+			// If we don't have a HEAD yet, we're cloning and need to get the
+			// upstream HEAD, too.
+			Ref head = transport.local.exactRef(Constants.HEAD);
+			ObjectId id = head != null ? head.getObjectId() : null;
+			if (id == null || id.equals(ObjectId.zeroId())) {
+				getHead = Constants.HEAD;
+			}
+		} catch (IOException e) {
+			// Ignore
+		}
+		conn = transport.openFetch(toFetch, getTags, getHead);
+		try {
+			Map<String, Ref> refsMap = conn.getRefsMap();
+			if (isInitialBranchMissing(refsMap, initialBranch)) {
+				throw new TransportException(MessageFormat.format(
+						JGitText.get().remoteBranchNotFound, initialBranch));
+			}
+			result.setAdvertisedRefs(transport.getURI(), refsMap);
 			result.peerUserAgent = conn.getPeerUserAgent();
 			final Set<Ref> matched = new HashSet<>();
 			for (RefSpec spec : toFetch) {
@@ -119,7 +161,6 @@
 			}
 
 			Collection<Ref> additionalTags = Collections.<Ref> emptyList();
-			final TagOpt tagopt = transport.getTagOpt();
 			if (tagopt == TagOpt.AUTO_FOLLOW)
 				additionalTags = expandAutoFollowTags();
 			else if (tagopt == TagOpt.FETCH_TAGS)
@@ -253,7 +294,17 @@
 		if (conn != null)
 			return;
 
-		conn = transport.openFetch();
+		// Build prefixes
+		Set<String> prefixes = new HashSet<>();
+		for (Ref toGet : askFor.values()) {
+			String src = toGet.getName();
+			prefixes.add(src);
+			prefixes.add(Constants.R_REFS + src);
+			prefixes.add(Constants.R_HEADS + src);
+			prefixes.add(Constants.R_TAGS + src);
+		}
+		conn = transport.openFetch(Collections.emptyList(),
+				prefixes.toArray(new String[0]));
 
 		// Since we opened a new connection we cannot be certain
 		// that the system we connected to has the same exact set
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
index 35e2978..36fce7a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2008-2013, Google Inc.
+ * Copyright (C) 2008, 2013 Google Inc.
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 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
@@ -247,6 +247,74 @@
 	 */
 	public static final String COMMAND_FETCH = "fetch"; //$NON-NLS-1$
 
+	/**
+	 * HTTP header to set by clients to request a specific git protocol version
+	 * in the HTTP transport.
+	 *
+	 * @since 5.11
+	 */
+	public static final String PROTOCOL_HEADER = "Git-Protocol"; //$NON-NLS-1$
+
+	/**
+	 * Environment variable to set by clients to request a specific git protocol
+	 * in the file:// and ssh:// transports.
+	 *
+	 * @since 5.11
+	 */
+	public static final String PROTOCOL_ENVIRONMENT_VARIABLE = "GIT_PROTOCOL"; //$NON-NLS-1$
+
+	/**
+	 * Protocol V2 ref advertisement attribute containing the peeled object id
+	 * for annotated tags.
+	 *
+	 * @since 5.11
+	 */
+	public static final String REF_ATTR_PEELED = "peeled:"; //$NON-NLS-1$
+
+	/**
+	 * Protocol V2 ref advertisement attribute containing the name of the ref
+	 * for symbolic refs.
+	 *
+	 * @since 5.11
+	 */
+	public static final String REF_ATTR_SYMREF_TARGET = "symref-target:"; //$NON-NLS-1$
+
+	/**
+	 * Protocol V2 acknowledgments section header.
+	 *
+	 * @since 5.11
+	 */
+	public static final String SECTION_ACKNOWLEDGMENTS = "acknowledgments"; //$NON-NLS-1$
+
+	/**
+	 * Protocol V2 packfile section header.
+	 *
+	 * @since 5.11
+	 */
+	public static final String SECTION_PACKFILE = "packfile"; //$NON-NLS-1$
+
+	/**
+	 * Protocol announcement for protocol version 1. This is the same as V0,
+	 * except for this initial line.
+	 *
+	 * @since 5.11
+	 */
+	public static final String VERSION_1 = "version 1"; //$NON-NLS-1$
+
+	/**
+	 * Protocol announcement for protocol version 2.
+	 *
+	 * @since 5.11
+	 */
+	public static final String VERSION_2 = "version 2"; //$NON-NLS-1$
+
+	/**
+	 * Protocol request for protocol version 2.
+	 *
+	 * @since 5.11
+	 */
+	public static final String VERSION_2_REQUEST = "version=2"; //$NON-NLS-1$
+
 	enum MultiAck {
 		OFF, CONTINUE, DETAILED;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpTransport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpTransport.java
index 49c8b58..febeb3c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpTransport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpTransport.java
@@ -26,7 +26,7 @@
 	 *
 	 * @since 3.3
 	 */
-	protected static HttpConnectionFactory connectionFactory = new JDKHttpConnectionFactory();
+	protected static volatile HttpConnectionFactory connectionFactory = new JDKHttpConnectionFactory();
 
 	/**
 	 * Get the {@link org.eclipse.jgit.transport.http.HttpConnectionFactory}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java
index 350311e..68c5b34 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java
@@ -103,6 +103,38 @@
 		this.limit = limit;
 	}
 
+	/**
+	 * Parses a ACK/NAK line in protocol V2.
+	 *
+	 * @param line
+	 *            to parse
+	 * @param returnedId
+	 *            in case of {@link AckNackResult#ACK_COMMON ACK_COMMON}
+	 * @return one of {@link AckNackResult#NAK NAK},
+	 *         {@link AckNackResult#ACK_COMMON ACK_COMMON}, or
+	 *         {@link AckNackResult#ACK_READY ACK_READY}
+	 * @throws IOException
+	 *             on protocol or transport errors
+	 */
+	static AckNackResult parseACKv2(String line, MutableObjectId returnedId)
+			throws IOException {
+		if ("NAK".equals(line)) { //$NON-NLS-1$
+			return AckNackResult.NAK;
+		}
+		if (line.startsWith("ACK ") && line.length() == 44) { //$NON-NLS-1$
+			returnedId.fromString(line.substring(4, 44));
+			return AckNackResult.ACK_COMMON;
+		}
+		if ("ready".equals(line)) { //$NON-NLS-1$
+			return AckNackResult.ACK_READY;
+		}
+		if (line.startsWith("ERR ")) { //$NON-NLS-1$
+			throw new PackProtocolException(line.substring(4));
+		}
+		throw new PackProtocolException(
+				MessageFormat.format(JGitText.get().expectedACKNAKGot, line));
+	}
+
 	AckNackResult readACK(MutableObjectId returnedId) throws IOException {
 		final String line = readString();
 		if (line.length() == 0)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java
index 6fc2042..77f0a7a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
- * Copyright (C) 2008-2009, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2010 Google Inc.
+ * Copyright (C) 2008, 2009 Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2008, 2020 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
@@ -33,12 +33,15 @@
  * against the underlying OutputStream.
  */
 public class PacketLineOut {
+
 	private static final Logger log = LoggerFactory.getLogger(PacketLineOut.class);
 
 	private final OutputStream out;
 
 	private final byte[] lenbuffer;
 
+	private final boolean logEnabled;
+
 	private boolean flushOnEnd;
 
 	private boolean usingSideband;
@@ -50,9 +53,24 @@
 	 *            stream.
 	 */
 	public PacketLineOut(OutputStream outputStream) {
+		this(outputStream, true);
+	}
+
+	/**
+	 * Create a new packet line writer that potentially doesn't log.
+	 *
+	 * @param outputStream
+	 *            stream.
+	 * @param enableLogging
+	 *            {@code false} to suppress all logging; {@code true} to log
+	 *            normally
+	 * @since 5.11
+	 */
+	public PacketLineOut(OutputStream outputStream, boolean enableLogging) {
 		out = outputStream;
 		lenbuffer = new byte[5];
 		flushOnEnd = true;
+		logEnabled = enableLogging;
 	}
 
 	/**
@@ -139,9 +157,15 @@
 			out.write(lenbuffer, 0, 4);
 		}
 		out.write(buf, pos, len);
-		if (log.isDebugEnabled()) {
-			String s = RawParseUtils.decode(UTF_8, buf, pos, len);
-			log.debug("git> " + s); //$NON-NLS-1$
+		if (logEnabled && log.isDebugEnabled()) {
+			// Escape a trailing \n to avoid empty lines in the log.
+			if (len > 0 && buf[pos + len - 1] == '\n') {
+				log.debug(
+						"git> " + RawParseUtils.decode(UTF_8, buf, pos, len - 1) //$NON-NLS-1$
+								+ "\\n"); //$NON-NLS-1$
+			} else {
+				log.debug("git> " + RawParseUtils.decode(UTF_8, buf, pos, len)); //$NON-NLS-1$
+			}
 		}
 	}
 
@@ -156,7 +180,9 @@
 	public void writeDelim() throws IOException {
 		formatLength(1);
 		out.write(lenbuffer, 0, 4);
-		log.debug("git> 0001"); //$NON-NLS-1$
+		if (logEnabled && log.isDebugEnabled()) {
+			log.debug("git> 0001"); //$NON-NLS-1$
+		}
 	}
 
 	/**
@@ -175,9 +201,12 @@
 	public void end() throws IOException {
 		formatLength(0);
 		out.write(lenbuffer, 0, 4);
-		log.debug("git> 0000"); //$NON-NLS-1$
-		if (flushOnEnd)
+		if (logEnabled && log.isDebugEnabled()) {
+			log.debug("git> 0000"); //$NON-NLS-1$
+		}
+		if (flushOnEnd) {
 			flush();
+		}
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
index 3adebba..c525e66 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2010, Google Inc. and others
+ * Copyright (C) 2008, 2020 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
@@ -13,6 +13,8 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SYMREF;
+import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_PEELED;
+import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_SYMREF_TARGET;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -287,7 +289,8 @@
 
 			if (useProtocolV2) {
 				String symrefPart = symrefs.containsKey(ref.getName())
-						? (" symref-target:" + symrefs.get(ref.getName())) //$NON-NLS-1$
+						? (' ' + REF_ATTR_SYMREF_TARGET
+								+ symrefs.get(ref.getName()))
 						: ""; //$NON-NLS-1$
 				String peelPart = ""; //$NON-NLS-1$
 				if (derefTags) {
@@ -296,7 +299,8 @@
 					}
 					ObjectId peeledObjectId = ref.getPeeledObjectId();
 					if (peeledObjectId != null) {
-						peelPart = " peeled:" + peeledObjectId.getName(); //$NON-NLS-1$
+						peelPart = ' ' + REF_ATTR_PEELED
+								+ peeledObjectId.getName();
 					}
 				}
 				writeOne(objectId.getName() + " " + ref.getName() + symrefPart //$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession2.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession2.java
new file mode 100644
index 0000000..23f670a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession2.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020, 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.io.IOException;
+import java.util.Map;
+
+/**
+ * A {@link RemoteSession} that supports passing environment variables to
+ * commands.
+ *
+ * @since 5.11
+ */
+public interface RemoteSession2 extends RemoteSession {
+
+	/**
+	 * Creates a new remote {@link Process} to execute the given command. The
+	 * returned process's streams exist and are connected, and execution of the
+	 * process is already started.
+	 *
+	 * @param commandName
+	 *            command to execute
+	 * @param environment
+	 *            environment variables to pass on
+	 * @param timeout
+	 *            timeout value, in seconds, for creating the remote process
+	 * @return a new remote process, already started
+	 * @throws java.io.IOException
+	 *             may be thrown in several cases. For example, on problems
+	 *             opening input or output streams or on problems connecting or
+	 *             communicating with the remote host. For the latter two cases,
+	 *             a TransportException may be thrown (a subclass of
+	 *             java.io.IOException).
+	 */
+	Process exec(String commandName, Map<String, String> environment,
+			int timeout) throws IOException;
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java
index fff2938..be55cd1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java
@@ -114,6 +114,14 @@
 	/** Key in an ssh config file. */
 	public static final String PREFERRED_AUTHENTICATIONS = "PreferredAuthentications";
 
+	/**
+	 * Key in an ssh config file; defines signature algorithms for public key
+	 * authentication as a comma-separated list.
+	 *
+	 * @since 5.11
+	 */
+	public static final String PUBKEY_ACCEPTED_ALGORITHMS = "PubkeyAcceptedAlgorithms";
+
 	/** Key in an ssh config file. */
 	public static final String PROXY_COMMAND = "ProxyCommand";
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
index 0b38159..83ffd41 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2009, Google Inc. and others
+ * Copyright (C) 2008, 2020 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
@@ -101,6 +101,9 @@
 					return v;
 				}
 			}
+			if ("1".equals(name)) { //$NON-NLS-1$
+				return V0;
+			}
 			return null;
 		}
 	}
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 2ddd0a6..5b781ac 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2008-2009, Google Inc.
+ * Copyright (C) 2008, 2009 Google Inc.
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 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
@@ -39,6 +39,7 @@
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.AbortedByHookException;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
@@ -774,6 +775,10 @@
 	private PrintStream hookOutRedirect;
 
 	private PrePushHook prePush;
+
+	@Nullable
+	TransferConfig.ProtocolVersion protocol;
+
 	/**
 	 * Create a new transport instance.
 	 *
@@ -789,6 +794,7 @@
 		final TransferConfig tc = local.getConfig().get(TransferConfig.KEY);
 		this.local = local;
 		this.uri = uri;
+		this.protocol = tc.protocolVersion;
 		this.objectChecker = tc.newObjectChecker();
 		this.credentialsProvider = CredentialsProvider.getDefault();
 		prePush = Hooks.prePush(local, hookOutRedirect);
@@ -1225,9 +1231,52 @@
 	 *             the remote connection could not be established or object
 	 *             copying (if necessary) failed or update specification was
 	 *             incorrect.
+	 * @since 5.11
 	 */
 	public FetchResult fetch(final ProgressMonitor monitor,
-			Collection<RefSpec> toFetch) throws NotSupportedException,
+			Collection<RefSpec> toFetch)
+			throws NotSupportedException, TransportException {
+		return fetch(monitor, toFetch, null);
+	}
+
+	/**
+	 * Fetch objects and refs from the remote repository to the local one.
+	 * <p>
+	 * This is a utility function providing standard fetch behavior. Local
+	 * tracking refs associated with the remote repository are automatically
+	 * updated if this transport was created from a
+	 * {@link org.eclipse.jgit.transport.RemoteConfig} with fetch RefSpecs
+	 * defined.
+	 *
+	 * @param monitor
+	 *            progress monitor to inform the user about our processing
+	 *            activity. Must not be null. Use
+	 *            {@link org.eclipse.jgit.lib.NullProgressMonitor} if progress
+	 *            updates are not interesting or necessary.
+	 * @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.
+	 * @param branch
+	 *            the initial branch to check out when cloning the repository.
+	 *            Can be specified as ref name (<code>refs/heads/master</code>),
+	 *            branch name (<code>master</code>) or tag name
+	 *            (<code>v1.2.3</code>). The default is to use the branch
+	 *            pointed to by the cloned repository's HEAD and can be
+	 *            requested by passing {@code null} or <code>HEAD</code>.
+	 * @return information describing the tracking refs updated.
+	 * @throws org.eclipse.jgit.errors.NotSupportedException
+	 *             this transport implementation does not support fetching
+	 *             objects.
+	 * @throws org.eclipse.jgit.errors.TransportException
+	 *             the remote connection could not be established or object
+	 *             copying (if necessary) failed or update specification was
+	 *             incorrect.
+	 * @since 5.11
+	 */
+	public FetchResult fetch(final ProgressMonitor monitor,
+			Collection<RefSpec> toFetch, String branch)
+			throws NotSupportedException,
 			TransportException {
 		if (toFetch == null || toFetch.isEmpty()) {
 			// If the caller did not ask for anything use the defaults.
@@ -1257,7 +1306,7 @@
 		}
 
 		final FetchResult result = new FetchResult();
-		new FetchProcess(this, toFetch).execute(monitor, result);
+		new FetchProcess(this, toFetch).execute(monitor, result, branch);
 
 		local.autoGC(monitor);
 
@@ -1453,6 +1502,43 @@
 			TransportException;
 
 	/**
+	 * Begins a new connection for fetching from the remote repository.
+	 * <p>
+	 * If the transport has no local repository, the fetch connection can only
+	 * be used for reading remote refs.
+	 * </p>
+	 * <p>
+	 * If the server supports git protocol V2, the {@link RefSpec}s and the
+	 * additional patterns, if any, are used to restrict the server's ref
+	 * advertisement to matching refs only.
+	 * </p>
+	 * <p>
+	 * Transports that want to support git protocol V2 <em>must</em> override
+	 * this; the default implementation ignores its arguments and calls
+	 * {@link #openFetch()}.
+	 * </p>
+	 *
+	 * @param refSpecs
+	 *            that will be fetched via
+	 *            {@link FetchConnection#fetch(ProgressMonitor, Collection, java.util.Set, OutputStream)} later
+	 * @param additionalPatterns
+	 *            that will be set as ref prefixes if the server supports git
+	 *            protocol V2; {@code null} values are ignored
+	 *
+	 * @return a fresh connection to fetch from the remote repository.
+	 * @throws org.eclipse.jgit.errors.NotSupportedException
+	 *             the implementation does not support fetching.
+	 * @throws org.eclipse.jgit.errors.TransportException
+	 *             the remote connection could not be established.
+	 * @since 5.11
+	 */
+	public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+			String... additionalPatterns)
+			throws NotSupportedException, TransportException {
+		return openFetch();
+	}
+
+	/**
 	 * Begins a new connection for pushing into the remote repository.
 	 *
 	 * @return a fresh connection to push into the remote repository.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java
index 820ec1a..a1914b6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 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
@@ -22,6 +22,7 @@
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.UnknownHostException;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Set;
@@ -94,6 +95,13 @@
 		return new TcpFetchConnection();
 	}
 
+	@Override
+	public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+			String... additionalPatterns)
+			throws NotSupportedException, TransportException {
+		return new TcpFetchConnection(refSpecs, additionalPatterns);
+	}
+
 	/** {@inheritDoc} */
 	@Override
 	public PushConnection openPush() throws TransportException {
@@ -113,7 +121,6 @@
 		final Socket s = new Socket();
 		try {
 			final InetAddress host = InetAddress.getByName(uri.getHost());
-			s.bind(null);
 			s.connect(new InetSocketAddress(host, port), tms);
 		} catch (IOException c) {
 			try {
@@ -130,7 +137,8 @@
 		return s;
 	}
 
-	void service(String name, PacketLineOut pckOut)
+	void service(String name, PacketLineOut pckOut,
+			TransferConfig.ProtocolVersion gitProtocol)
 			throws IOException {
 		final StringBuilder cmd = new StringBuilder();
 		cmd.append(name);
@@ -144,6 +152,11 @@
 			cmd.append(uri.getPort());
 		}
 		cmd.append('\0');
+		if (TransferConfig.ProtocolVersion.V2.equals(gitProtocol)) {
+			cmd.append('\0');
+			cmd.append(GitProtocolConstants.VERSION_2_REQUEST);
+			cmd.append('\0');
+		}
 		pckOut.writeString(cmd.toString());
 		pckOut.flush();
 	}
@@ -152,6 +165,11 @@
 		private Socket sock;
 
 		TcpFetchConnection() throws TransportException {
+			this(Collections.emptyList());
+		}
+
+		TcpFetchConnection(Collection<RefSpec> refSpecs,
+				String... additionalPatterns) throws TransportException {
 			super(TransportGitAnon.this);
 			sock = openConnection();
 			try {
@@ -162,13 +180,19 @@
 				sOut = new BufferedOutputStream(sOut);
 
 				init(sIn, sOut);
-				service("git-upload-pack", pckOut); //$NON-NLS-1$
+				TransferConfig.ProtocolVersion gitProtocol = protocol;
+				if (gitProtocol == null) {
+					gitProtocol = TransferConfig.ProtocolVersion.V2;
+				}
+				service("git-upload-pack", pckOut, gitProtocol); //$NON-NLS-1$
 			} catch (IOException err) {
 				close();
 				throw new TransportException(uri,
 						JGitText.get().remoteHungUpUnexpectedly, err);
 			}
-			readAdvertisedRefs();
+			if (!readAdvertisedRefs()) {
+				lsRefs(refSpecs, additionalPatterns);
+			}
 		}
 
 		@Override
@@ -201,7 +225,7 @@
 				sOut = new BufferedOutputStream(sOut);
 
 				init(sIn, sOut);
-				service("git-receive-pack", pckOut); //$NON-NLS-1$
+				service("git-receive-pack", pckOut, null); //$NON-NLS-1$
 			} catch (IOException err) {
 				close();
 				throw new TransportException(uri,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java
index b9cb248..19ed4fb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 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
@@ -19,11 +19,13 @@
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 
 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
@@ -144,6 +146,13 @@
 		return new SshFetchConnection();
 	}
 
+	@Override
+	public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+			String... additionalPatterns)
+			throws NotSupportedException, TransportException {
+		return new SshFetchConnection(refSpecs, additionalPatterns);
+	}
+
 	/** {@inheritDoc} */
 	@Override
 	public PushConnection openPush() throws TransportException {
@@ -196,29 +205,38 @@
 		return SystemReader.getInstance().getenv("GIT_SSH") != null; //$NON-NLS-1$
 	}
 
-	private class ExtSession implements RemoteSession {
+	private class ExtSession implements RemoteSession2 {
+
 		@Override
 		public Process exec(String command, int timeout)
 				throws TransportException {
+			return exec(command, null, timeout);
+		}
+
+		@Override
+		public Process exec(String command, Map<String, String> environment,
+				int timeout) throws TransportException {
 			String ssh = SystemReader.getInstance().getenv("GIT_SSH"); //$NON-NLS-1$
 			boolean putty = ssh.toLowerCase(Locale.ROOT).contains("plink"); //$NON-NLS-1$
 
 			List<String> args = new ArrayList<>();
 			args.add(ssh);
-			if (putty
-					&& !ssh.toLowerCase(Locale.ROOT).contains("tortoiseplink")) //$NON-NLS-1$
+			if (putty && !ssh.toLowerCase(Locale.ROOT)
+					.contains("tortoiseplink")) {//$NON-NLS-1$
 				args.add("-batch"); //$NON-NLS-1$
+			}
 			if (0 < getURI().getPort()) {
 				args.add(putty ? "-P" : "-p"); //$NON-NLS-1$ //$NON-NLS-2$
 				args.add(String.valueOf(getURI().getPort()));
 			}
-			if (getURI().getUser() != null)
+			if (getURI().getUser() != null) {
 				args.add(getURI().getUser() + "@" + getURI().getHost()); //$NON-NLS-1$
-			else
+			} else {
 				args.add(getURI().getHost());
+			}
 			args.add(command);
 
-			ProcessBuilder pb = createProcess(args);
+			ProcessBuilder pb = createProcess(args, environment);
 			try {
 				return pb.start();
 			} catch (IOException err) {
@@ -226,9 +244,13 @@
 			}
 		}
 
-		private ProcessBuilder createProcess(List<String> args) {
+		private ProcessBuilder createProcess(List<String> args,
+				Map<String, String> environment) {
 			ProcessBuilder pb = new ProcessBuilder();
 			pb.command(args);
+			if (environment != null) {
+				pb.environment().putAll(environment);
+			}
 			File directory = local != null ? local.getDirectory() : null;
 			if (directory != null) {
 				pb.environment().put(Constants.GIT_DIR_KEY,
@@ -249,10 +271,31 @@
 		private StreamCopyThread errorThread;
 
 		SshFetchConnection() throws TransportException {
+			this(Collections.emptyList());
+		}
+
+		SshFetchConnection(Collection<RefSpec> refSpecs,
+				String... additionalPatterns) throws TransportException {
 			super(TransportGitSsh.this);
 			try {
-				process = getSession().exec(commandFor(getOptionUploadPack()),
-						getTimeout());
+				RemoteSession session = getSession();
+				TransferConfig.ProtocolVersion gitProtocol = protocol;
+				if (gitProtocol == null) {
+					gitProtocol = TransferConfig.ProtocolVersion.V2;
+				}
+				if (session instanceof RemoteSession2
+						&& TransferConfig.ProtocolVersion.V2
+								.equals(gitProtocol)) {
+					process = ((RemoteSession2) session).exec(
+							commandFor(getOptionUploadPack()), Collections
+									.singletonMap(
+											GitProtocolConstants.PROTOCOL_ENVIRONMENT_VARIABLE,
+											GitProtocolConstants.VERSION_2_REQUEST),
+							getTimeout());
+				} else {
+					process = session.exec(commandFor(getOptionUploadPack()),
+							getTimeout());
+				}
 				final MessageWriter msg = new MessageWriter();
 				setMessageWriter(msg);
 
@@ -272,7 +315,9 @@
 			}
 
 			try {
-				readAdvertisedRefs();
+				if (!readAdvertisedRefs()) {
+					lsRefs(refSpecs, additionalPatterns);
+				}
 			} catch (NoRemoteRepositoryException notFound) {
 				final String msgs = getMessages();
 				checkExecFailure(process.exitValue(), getOptionUploadPack(),
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
index 6768387..2e5d18d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
  * Copyright (C) 2013, Matthias Sohn <matthias.sohn@sap.com>
- * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2017, 2020 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
@@ -33,14 +33,15 @@
 import static org.eclipse.jgit.util.HttpSupport.METHOD_GET;
 import static org.eclipse.jgit.util.HttpSupport.METHOD_POST;
 
+import java.io.BufferedInputStream;
 import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.InterruptedIOException;
 import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
 import java.net.HttpCookie;
 import java.net.MalformedURLException;
 import java.net.Proxy;
@@ -49,10 +50,12 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
 import java.security.cert.CertPathBuilderException;
 import java.security.cert.CertPathValidatorException;
 import java.security.cert.CertificateException;
@@ -75,6 +78,7 @@
 
 import javax.net.ssl.SSLHandshakeException;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
 import org.eclipse.jgit.errors.NotSupportedException;
@@ -95,6 +99,8 @@
 import org.eclipse.jgit.transport.HttpAuthMethod.Type;
 import org.eclipse.jgit.transport.HttpConfig.HttpRedirectMode;
 import org.eclipse.jgit.transport.http.HttpConnection;
+import org.eclipse.jgit.transport.http.HttpConnectionFactory;
+import org.eclipse.jgit.transport.http.HttpConnectionFactory2;
 import org.eclipse.jgit.util.HttpSupport;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -132,6 +138,9 @@
 
 	private static final String SVC_RECEIVE_PACK = "git-receive-pack"; //$NON-NLS-1$
 
+	private static final byte[] VERSION = "version" //$NON-NLS-1$
+			.getBytes(StandardCharsets.US_ASCII);
+
 	/**
 	 * Accept-Encoding header in the HTTP request
 	 * (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html).
@@ -257,6 +266,12 @@
 
 	private boolean sslFailure = false;
 
+	private HttpConnectionFactory factory;
+
+	private HttpConnectionFactory2.GitSession gitSession;
+
+	private boolean factoryUsed;
+
 	/**
 	 * All stored cookies bound to this repo (independent of the baseUrl)
 	 */
@@ -279,6 +294,7 @@
 		sslVerify = http.isSslVerify();
 		cookieFile = getCookieFileFromConfig(http);
 		relevantCookies = filterCookies(cookieFile, baseUrl);
+		factory = HttpTransport.getConnectionFactory();
 	}
 
 	private URL toURL(URIish urish) throws MalformedURLException {
@@ -321,6 +337,7 @@
 		sslVerify = http.isSslVerify();
 		cookieFile = getCookieFileFromConfig(http);
 		relevantCookies = filterCookies(cookieFile, baseUrl);
+		factory = HttpTransport.getConnectionFactory();
 	}
 
 	/**
@@ -339,11 +356,15 @@
 
 	@SuppressWarnings("resource") // Closed by caller
 	private FetchConnection getConnection(HttpConnection c, InputStream in,
-			String service) throws IOException {
+			String service, Collection<RefSpec> refSpecs,
+			String... additionalPatterns) throws IOException {
 		BaseConnection f;
 		if (isSmartHttp(c, service)) {
-			readSmartHeaders(in, service);
-			f = new SmartHttpFetchConnection(in);
+			InputStream withMark = in.markSupported() ? in
+					: new BufferedInputStream(in);
+			readSmartHeaders(withMark, service);
+			f = new SmartHttpFetchConnection(withMark, refSpecs,
+					additionalPatterns);
 		} else {
 			// Assume this server doesn't support smart HTTP fetch
 			// and fall back on dumb object walking.
@@ -353,15 +374,98 @@
 		return (FetchConnection) f;
 	}
 
+	/**
+	 * Sets the {@link HttpConnectionFactory} to be used by this
+	 * {@link TransportHttp} instance.
+	 * <p>
+	 * If no factory is set explicitly, the {@link TransportHttp} instance uses
+	 * the {@link HttpTransport#getConnectionFactory() globally defined
+	 * factory}.
+	 * </p>
+	 *
+	 * @param customFactory
+	 *            the {@link HttpConnectionFactory} to use
+	 * @throws IllegalStateException
+	 *             if an HTTP/HTTPS connection has already been opened on this
+	 *             {@link TransportHttp} instance
+	 * @since 5.11
+	 */
+	public void setHttpConnectionFactory(
+			@NonNull HttpConnectionFactory customFactory) {
+		if (factoryUsed) {
+			throw new IllegalStateException(JGitText.get().httpFactoryInUse);
+		}
+		factory = customFactory;
+	}
+
+	/**
+	 * Retrieves the {@link HttpConnectionFactory} used by this
+	 * {@link TransportHttp} instance.
+	 *
+	 * @return the {@link HttpConnectionFactory}
+	 * @since 5.11
+	 */
+	@NonNull
+	public HttpConnectionFactory getHttpConnectionFactory() {
+		return factory;
+	}
+
+	/**
+	 * Sets preemptive Basic HTTP authentication. If the given {@code username}
+	 * or {@code password} is empty or {@code null}, no preemptive
+	 * authentication will be done. If {@code username} and {@code password} are
+	 * set, they will override authority information from the URI
+	 * ("user:password@").
+	 * <p>
+	 * If the connection encounters redirects, the pre-authentication will be
+	 * cleared if the redirect goes to a different host.
+	 * </p>
+	 *
+	 * @param username
+	 *            to use
+	 * @param password
+	 *            to use
+	 * @throws IllegalStateException
+	 *             if an HTTP/HTTPS connection has already been opened on this
+	 *             {@link TransportHttp} instance
+	 * @since 5.11
+	 */
+	public void setPreemptiveBasicAuthentication(String username,
+			String password) {
+		if (factoryUsed) {
+			throw new IllegalStateException(JGitText.get().httpPreAuthTooLate);
+		}
+		if (StringUtils.isEmptyOrNull(username)
+				|| StringUtils.isEmptyOrNull(password)) {
+			authMethod = authFromUri(currentUri);
+		} else {
+			HttpAuthMethod basic = HttpAuthMethod.Type.BASIC.method(null);
+			basic.authorize(username, password);
+			authMethod = basic;
+		}
+	}
+
 	/** {@inheritDoc} */
 	@Override
 	public FetchConnection openFetch() throws TransportException,
 			NotSupportedException {
+		return openFetch(Collections.emptyList());
+	}
+
+	@Override
+	public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+			String... additionalPatterns)
+			throws NotSupportedException, TransportException {
 		final String service = SVC_UPLOAD_PACK;
 		try {
-			final HttpConnection c = connect(service);
+			TransferConfig.ProtocolVersion gitProtocol = protocol;
+			if (gitProtocol == null) {
+				gitProtocol = TransferConfig.ProtocolVersion.V2;
+			}
+			HttpConnection c = connect(service, gitProtocol);
 			try (InputStream in = openInputStream(c)) {
-				return getConnection(c, in, service);
+				return getConnection(c, in, service, refSpecs,
+						additionalPatterns);
 			}
 		} catch (NotSupportedException | TransportException err) {
 			throw err;
@@ -456,8 +560,9 @@
 
 	private PushConnection smartPush(String service, HttpConnection c,
 			InputStream in) throws IOException, TransportException {
-		readSmartHeaders(in, service);
-		SmartHttpPushConnection p = new SmartHttpPushConnection(in);
+		BufferedInputStream inBuf = new BufferedInputStream(in);
+		readSmartHeaders(inBuf, service);
+		SmartHttpPushConnection p = new SmartHttpPushConnection(inBuf);
 		p.setPeerUserAgent(c.getHeaderField(HttpSupport.HDR_SERVER));
 		return p;
 	}
@@ -465,7 +570,10 @@
 	/** {@inheritDoc} */
 	@Override
 	public void close() {
-		// No explicit connections are maintained.
+		if (gitSession != null) {
+			gitSession.close();
+			gitSession = null;
+		}
 	}
 
 	/**
@@ -492,9 +600,40 @@
 		return new NoRemoteRepositoryException(u, text);
 	}
 
+	private HttpAuthMethod authFromUri(URIish u) {
+		String user = u.getUser();
+		String pass = u.getPass();
+		if (user != null && pass != null) {
+			try {
+				// User/password are _not_ application/x-www-form-urlencoded. In
+				// particular the "+" sign would be replaced by a space.
+				user = URLDecoder.decode(user.replace("+", "%2B"), //$NON-NLS-1$ //$NON-NLS-2$
+						StandardCharsets.UTF_8.name());
+				pass = URLDecoder.decode(pass.replace("+", "%2B"), //$NON-NLS-1$ //$NON-NLS-2$
+						StandardCharsets.UTF_8.name());
+				HttpAuthMethod basic = HttpAuthMethod.Type.BASIC.method(null);
+				basic.authorize(user, pass);
+				return basic;
+			} catch (IllegalArgumentException
+					| UnsupportedEncodingException e) {
+				LOG.warn(JGitText.get().httpUserInfoDecodeError, u);
+			}
+		}
+		return HttpAuthMethod.Type.NONE.method(null);
+	}
+
 	private HttpConnection connect(String service)
 			throws TransportException, NotSupportedException {
+		return connect(service, null);
+	}
+
+	private HttpConnection connect(String service,
+			TransferConfig.ProtocolVersion protocolVersion)
+			throws TransportException, NotSupportedException {
 		URL u = getServiceURL(service);
+		if (HttpAuthMethod.Type.NONE.equals(authMethod.getType())) {
+			authMethod = authFromUri(currentUri);
+		}
 		int authAttempts = 1;
 		int redirects = 0;
 		Collection<Type> ignoreTypes = null;
@@ -507,6 +646,11 @@
 				} else {
 					conn.setRequestProperty(HDR_ACCEPT, "*/*"); //$NON-NLS-1$
 				}
+				if (TransferConfig.ProtocolVersion.V2.equals(protocolVersion)) {
+					conn.setRequestProperty(
+							GitProtocolConstants.PROTOCOL_HEADER,
+							GitProtocolConstants.VERSION_2_REQUEST);
+				}
 				final int status = HttpSupport.response(conn);
 				processResponseCookies(conn);
 				switch (status) {
@@ -796,7 +940,13 @@
 		}
 		try {
 			URI redirectTo = new URI(location);
+			// Reset authentication if the redirect has user/password info or
+			// if the host is different.
+			boolean resetAuth = !StringUtils
+					.isEmptyOrNull(redirectTo.getUserInfo());
+			String currentHost = currentUrl.getHost();
 			redirectTo = currentUrl.toURI().resolve(redirectTo);
+			resetAuth = resetAuth || !currentHost.equals(redirectTo.getHost());
 			String redirected = redirectTo.toASCIIString();
 			if (!isValidRedirect(baseUrl, redirected, checkFor)) {
 				throw new TransportException(uri,
@@ -805,6 +955,9 @@
 			}
 			redirected = redirected.substring(0, redirected.indexOf(checkFor));
 			URIish result = new URIish(redirected);
+			if (resetAuth) {
+				authMethod = HttpAuthMethod.Type.NONE.method(null);
+			}
 			if (LOG.isInfoEnabled()) {
 				LOG.info(MessageFormat.format(JGitText.get().redirectHttp,
 						uri.setPass(null),
@@ -885,9 +1038,20 @@
 		}
 
 		final Proxy proxy = HttpSupport.proxyFor(proxySelector, u);
-		HttpConnection conn = connectionFactory.create(u, proxy);
+		factoryUsed = true;
+		HttpConnection conn = factory.create(u, proxy);
 
-		if (!sslVerify && "https".equals(u.getProtocol())) { //$NON-NLS-1$
+		if (gitSession == null && (factory instanceof HttpConnectionFactory2)) {
+			gitSession = ((HttpConnectionFactory2) factory).newSession();
+		}
+		if (gitSession != null) {
+			try {
+				gitSession.configure(conn, sslVerify);
+			} catch (GeneralSecurityException e) {
+				throw new IOException(e.getMessage(), e);
+			}
+		} else if (!sslVerify && "https".equals(u.getProtocol())) { //$NON-NLS-1$
+			// Backwards compatibility
 			HttpSupport.disableSslVerify(conn);
 		}
 
@@ -1148,20 +1312,37 @@
 
 	private void readSmartHeaders(InputStream in, String service)
 			throws IOException {
-		// A smart reply will have a '#' after the first 4 bytes, but
-		// a dumb reply cannot contain a '#' until after byte 41. Do a
+		// A smart protocol V0 reply will have a '#' after the first 4 bytes,
+		// but a dumb reply cannot contain a '#' until after byte 41. Do a
 		// quick check to make sure its a smart reply before we parse
 		// as a pkt-line stream.
 		//
-		final byte[] magic = new byte[5];
+		// There appears to be a confusion about this in protocol V2. Github
+		// sends the # service line as a git (not http) header also when
+		// protocol V2 is used. Gitlab also does so. JGit's UploadPack doesn't,
+		// and thus Gerrit also does not.
+		final byte[] magic = new byte[14];
+		if (!in.markSupported()) {
+			throw new TransportException(uri,
+					JGitText.get().inputStreamMustSupportMark);
+		}
+		in.mark(14);
 		IO.readFully(in, magic, 0, magic.length);
+		// Did we get 000dversion 2 or similar? (Canonical is 000eversion 2\n,
+		// but JGit and thus Gerrit omits the \n.)
+		if (Arrays.equals(Arrays.copyOfRange(magic, 4, 11), VERSION)
+				&& magic[12] >= '1' && magic[12] <= '9') {
+			// It's a smart server doing version 1 or greater, but not sending
+			// the # service line header. Don't consume the version line.
+			in.reset();
+			return;
+		}
 		if (magic[4] != '#') {
 			throw new TransportException(uri, MessageFormat.format(
 					JGitText.get().expectedPktLineWithService, RawParseUtils.decode(magic)));
 		}
-
-		final PacketLineIn pckIn = new PacketLineIn(new UnionInputStream(
-				new ByteArrayInputStream(magic), in));
+		in.reset();
+		final PacketLineIn pckIn = new PacketLineIn(in);
 		final String exp = "# service=" + service; //$NON-NLS-1$
 		final String act = pckIn.readString();
 		if (!exp.equals(act)) {
@@ -1327,12 +1508,24 @@
 
 		SmartHttpFetchConnection(InputStream advertisement)
 				throws TransportException {
+			this(advertisement, Collections.emptyList());
+		}
+
+		SmartHttpFetchConnection(InputStream advertisement,
+				Collection<RefSpec> refSpecs, String... additionalPatterns)
+				throws TransportException {
 			super(TransportHttp.this);
 			statelessRPC = true;
 
 			init(advertisement, DisabledOutputStream.INSTANCE);
 			outNeedsEnd = false;
-			readAdvertisedRefs();
+			if (!readAdvertisedRefs()) {
+				// Must be protocol V2
+				LongPollService service = new LongPollService(SVC_UPLOAD_PACK,
+						getProtocolVersion());
+				init(service.getInputStream(), service.getOutputStream());
+				lsRefs(refSpecs, additionalPatterns);
+			}
 		}
 
 		@Override
@@ -1340,7 +1533,8 @@
 				final Collection<Ref> want, final Set<ObjectId> have,
 				final OutputStream outputStream) throws TransportException {
 			try {
-				svc = new MultiRequestService(SVC_UPLOAD_PACK);
+				svc = new MultiRequestService(SVC_UPLOAD_PACK,
+						getProtocolVersion());
 				init(svc.getInputStream(), svc.getOutputStream());
 				super.doFetch(monitor, want, have, outputStream);
 			} finally {
@@ -1369,7 +1563,8 @@
 		protected void doPush(final ProgressMonitor monitor,
 				final Map<String, RemoteRefUpdate> refUpdates,
 				OutputStream outputStream) throws TransportException {
-			final Service svc = new MultiRequestService(SVC_RECEIVE_PACK);
+			final Service svc = new MultiRequestService(SVC_RECEIVE_PACK,
+					getProtocolVersion());
 			init(svc.getInputStream(), svc.getOutputStream());
 			super.doPush(monitor, refUpdates, outputStream);
 		}
@@ -1389,10 +1584,14 @@
 
 		protected final HttpExecuteStream execute;
 
+		protected final TransferConfig.ProtocolVersion protocolVersion;
+
 		final UnionInputStream in;
 
-		Service(String serviceName) {
+		Service(String serviceName,
+				TransferConfig.ProtocolVersion protocolVersion) {
 			this.serviceName = serviceName;
+			this.protocolVersion = protocolVersion;
 			this.requestType = "application/x-" + serviceName + "-request"; //$NON-NLS-1$ //$NON-NLS-2$
 			this.responseType = "application/x-" + serviceName + "-result"; //$NON-NLS-1$ //$NON-NLS-2$
 
@@ -1408,6 +1607,10 @@
 			conn.setDoOutput(true);
 			conn.setRequestProperty(HDR_CONTENT_TYPE, requestType);
 			conn.setRequestProperty(HDR_ACCEPT, responseType);
+			if (TransferConfig.ProtocolVersion.V2.equals(protocolVersion)) {
+				conn.setRequestProperty(GitProtocolConstants.PROTOCOL_HEADER,
+						GitProtocolConstants.VERSION_2_REQUEST);
+			}
 		}
 
 		void sendRequest() throws IOException {
@@ -1663,8 +1866,9 @@
 	class MultiRequestService extends Service {
 		boolean finalRequest;
 
-		MultiRequestService(String serviceName) {
-			super(serviceName);
+		MultiRequestService(String serviceName,
+				TransferConfig.ProtocolVersion protocolVersion) {
+			super(serviceName, protocolVersion);
 		}
 
 		/** Keep opening send-receive pairs to the given URI. */
@@ -1701,11 +1905,10 @@
 
 	/** Service for maintaining a single long-poll connection. */
 	class LongPollService extends Service {
-		/**
-		 * @param serviceName
-		 */
-		LongPollService(String serviceName) {
-			super(serviceName);
+
+		LongPollService(String serviceName,
+				TransferConfig.ProtocolVersion protocolVersion) {
+			super(serviceName, protocolVersion);
 		}
 
 		/** Only open one send-receive request. */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java
index 403f98d..77d1419 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java
@@ -1,9 +1,9 @@
 /*
  * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 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
@@ -20,6 +20,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
@@ -153,11 +154,17 @@
 	/** {@inheritDoc} */
 	@Override
 	public FetchConnection openFetch() throws TransportException {
+		return openFetch(Collections.emptyList());
+	}
+
+	@Override
+	public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+			String... additionalPatterns) throws TransportException {
 		final String up = getOptionUploadPack();
 		if (!"git-upload-pack".equals(up) //$NON-NLS-1$
-				&& !"git upload-pack".equals(up)) //$NON-NLS-1$
-			return new ForkLocalFetchConnection();
-
+				&& !"git upload-pack".equals(up)) {//$NON-NLS-1$
+			return new ForkLocalFetchConnection(refSpecs, additionalPatterns);
+		}
 		UploadPackFactory<Void> upf = (Void req,
 				Repository db) -> createUploadPack(db);
 		return new InternalFetchConnection<>(this, upf, null, openRepo());
@@ -193,6 +200,23 @@
 	 */
 	protected Process spawn(String cmd)
 			throws TransportException {
+		return spawn(cmd, null);
+	}
+
+	/**
+	 * Spawn process
+	 *
+	 * @param cmd
+	 *            command
+	 * @param protocolVersion
+	 *            to use
+	 * @return a {@link java.lang.Process} object.
+	 * @throws org.eclipse.jgit.errors.TransportException
+	 *             if any.
+	 */
+	private Process spawn(String cmd,
+			TransferConfig.ProtocolVersion protocolVersion)
+			throws TransportException {
 		try {
 			String[] args = { "." }; //$NON-NLS-1$
 			ProcessBuilder proc = local.getFS().runInShell(cmd, args);
@@ -208,7 +232,10 @@
 			env.remove("GIT_GRAFT_FILE"); //$NON-NLS-1$
 			env.remove("GIT_INDEX_FILE"); //$NON-NLS-1$
 			env.remove("GIT_NO_REPLACE_OBJECTS"); //$NON-NLS-1$
-
+			if (TransferConfig.ProtocolVersion.V2.equals(protocolVersion)) {
+				env.put(GitProtocolConstants.PROTOCOL_ENVIRONMENT_VARIABLE,
+						GitProtocolConstants.VERSION_2_REQUEST);
+			}
 			return proc.start();
 		} catch (IOException err) {
 			throw new TransportException(uri, err.getMessage(), err);
@@ -221,12 +248,21 @@
 		private Thread errorReaderThread;
 
 		ForkLocalFetchConnection() throws TransportException {
+			this(Collections.emptyList());
+		}
+
+		ForkLocalFetchConnection(Collection<RefSpec> refSpecs,
+				String... additionalPatterns) throws TransportException {
 			super(TransportLocal.this);
 
 			final MessageWriter msg = new MessageWriter();
 			setMessageWriter(msg);
 
-			uploadPack = spawn(getOptionUploadPack());
+			TransferConfig.ProtocolVersion gitProtocol = protocol;
+			if (gitProtocol == null) {
+				gitProtocol = TransferConfig.ProtocolVersion.V2;
+			}
+			uploadPack = spawn(getOptionUploadPack(), gitProtocol);
 
 			final InputStream upErr = uploadPack.getErrorStream();
 			errorReaderThread = new StreamCopyThread(upErr, msg.getRawStream());
@@ -239,7 +275,9 @@
 			upOut = new BufferedOutputStream(upOut);
 
 			init(upIn, upOut);
-			readAdvertisedRefs();
+			if (!readAdvertisedRefs()) {
+				lsRefs(refSpecs, additionalPatterns);
+			}
 		}
 
 		@Override
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
index 1242ef1..7f1ddaa 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2010, Google Inc. and others
+ * Copyright (C) 2008, 2020 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
@@ -33,6 +33,7 @@
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND_64K;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_THIN_PACK;
+import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2_REQUEST;
 import static org.eclipse.jgit.util.RefMap.toRefMap;
 
 import java.io.ByteArrayOutputStream;
@@ -709,7 +710,7 @@
 	 * @since 5.0
 	 */
 	public void setExtraParameters(Collection<String> params) {
-		this.clientRequestedV2 = params.contains("version=2"); //$NON-NLS-1$
+		this.clientRequestedV2 = params.contains(VERSION_2_REQUEST);
 	}
 
 	/**
@@ -722,7 +723,8 @@
 	}
 
 	private boolean useProtocolV2() {
-		return ProtocolVersion.V2.equals(transferConfig.protocolVersion)
+		return (transferConfig.protocolVersion == null
+			|| ProtocolVersion.V2.equals(transferConfig.protocolVersion))
 				&& clientRequestedV2;
 	}
 
@@ -1191,17 +1193,18 @@
 
 		if (req.wasDoneReceived()) {
 			processHaveLines(req.getPeerHas(), ObjectId.zeroId(),
-					new PacketLineOut(NullOutputStream.INSTANCE),
+					new PacketLineOut(NullOutputStream.INSTANCE, false),
 					accumulator);
 		} else {
-			pckOut.writeString("acknowledgments\n"); //$NON-NLS-1$
+			pckOut.writeString(
+					GitProtocolConstants.SECTION_ACKNOWLEDGMENTS + '\n');
 			for (ObjectId id : req.getPeerHas()) {
 				if (walk.getObjectReader().has(id)) {
 					pckOut.writeString("ACK " + id.getName() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
 				}
 			}
 			processHaveLines(req.getPeerHas(), ObjectId.zeroId(),
-					new PacketLineOut(NullOutputStream.INSTANCE),
+					new PacketLineOut(NullOutputStream.INSTANCE, false),
 					accumulator);
 			if (okToGiveUp()) {
 				pckOut.writeString("ready\n"); //$NON-NLS-1$
@@ -1243,7 +1246,8 @@
 			if (!pckOut.isUsingSideband()) {
 				// sendPack will write "packfile\n" for us if sideband-all is used.
 				// But sideband-all is not used, so we have to write it ourselves.
-				pckOut.writeString("packfile\n"); //$NON-NLS-1$
+				pckOut.writeString(
+						GitProtocolConstants.SECTION_PACKFILE + '\n');
 			}
 
 			accumulator.timeNegotiating = Duration
@@ -1955,8 +1959,8 @@
 							.map(objId -> objectIdToRevObject(objWalk, objId))
 							.filter(Objects::nonNull); // Ignore missing tips
 
-					ObjectReachabilityChecker reachabilityChecker = objWalk
-							.createObjectReachabilityChecker();
+					ObjectReachabilityChecker reachabilityChecker = reader
+							.createObjectReachabilityChecker(objWalk);
 					Optional<RevObject> unreachable = reachabilityChecker
 							.areAllReachable(wantsAsObjs, startersAsObjs);
 					if (unreachable.isPresent()) {
@@ -1967,8 +1971,8 @@
 			}
 
 			// All wants are commits, we can use ReachabilityChecker
-			ReachabilityChecker reachabilityChecker = walk
-					.createReachabilityChecker();
+			ReachabilityChecker reachabilityChecker = reader
+					.createReachabilityChecker(walk);
 
 			Stream<RevCommit> reachableCommits = importantRefsFirst(visibleRefs)
 					.map(UploadPack::refToObjectId)
@@ -2327,7 +2331,8 @@
 					// for us if provided a PackfileUriConfig. In this case, we
 					// are not providing a PackfileUriConfig, so we have to
 					// write this line ourselves.
-					pckOut.writeString("packfile\n"); //$NON-NLS-1$
+					pckOut.writeString(
+							GitProtocolConstants.SECTION_PACKFILE + '\n');
 				}
 			}
 			pw.writePack(pm, NullProgressMonitor.INSTANCE, packOut);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
index c44838a..a6b2045 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
@@ -510,6 +510,7 @@
 			// and attach it to the local repository so we can use
 			// all of the contained objects.
 			//
+			Throwable e1 = null;
 			try {
 				pack.downloadPack(monitor);
 			} catch (IOException err) {
@@ -519,6 +520,7 @@
 				// an alternate.
 				//
 				recordError(id, err);
+				e1 = err;
 				continue;
 			} finally {
 				// If the pack was good its in the local repository
@@ -531,6 +533,9 @@
 					if (pack.tmpIdx != null)
 						FileUtils.delete(pack.tmpIdx);
 				} catch (IOException e) {
+					if (e1 != null) {
+						e.addSuppressed(e1);
+					}
 					throw new TransportException(e.getMessage(), e);
 				}
 				packItr.remove();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java
index f2eac8d..03ef852 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java
@@ -13,6 +13,7 @@
 import static org.eclipse.jgit.transport.WalkRemoteObjectDatabase.ROOT_DIR;
 
 import java.io.BufferedOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.ArrayList;
@@ -26,6 +27,8 @@
 
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.PackFile;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -189,9 +192,8 @@
 
 	private void sendpack(final List<RemoteRefUpdate> updates,
 			final ProgressMonitor monitor) throws TransportException {
-		String pathPack = null;
-		String pathIdx = null;
-
+		PackFile pack = null;
+		PackFile idx = null;
 		try (PackWriter writer = new PackWriter(transport.getPackConfig(),
 				local.newObjectReader())) {
 
@@ -217,31 +219,33 @@
 			for (String n : dest.getPackNames())
 				packNames.put(n, n);
 
-			final String base = "pack-" + writer.computeName().name(); //$NON-NLS-1$
-			final String packName = base + ".pack"; //$NON-NLS-1$
-			pathPack = "pack/" + packName; //$NON-NLS-1$
-			pathIdx = "pack/" + base + ".idx"; //$NON-NLS-1$ //$NON-NLS-2$
+			File packDir = new File("pack"); //$NON-NLS-1$
+			pack = new PackFile(packDir, writer.computeName(),
+					PackExt.PACK);
+			idx = pack.create(PackExt.INDEX);
 
-			if (packNames.remove(packName) != null) {
+			if (packNames.remove(pack.getName()) != null) {
 				// The remote already contains this pack. We should
 				// remove the index before overwriting to prevent bad
 				// offsets from appearing to clients.
 				//
 				dest.writeInfoPacks(packNames.keySet());
-				dest.deleteFile(pathIdx);
+				dest.deleteFile(idx.getPath());
 			}
 
 			// Write the pack file, then the index, as readers look the
 			// other direction (index, then pack file).
 			//
-			String wt = "Put " + base.substring(0, 12); //$NON-NLS-1$
+			String wt = "Put " + pack.getName().substring(0, 12); //$NON-NLS-1$
 			try (OutputStream os = new BufferedOutputStream(
-					dest.writeFile(pathPack, monitor, wt + "..pack"))) { //$NON-NLS-1$
+					dest.writeFile(pack.getPath(), monitor,
+							wt + "." + pack.getPackExt().getExtension()))) { //$NON-NLS-1$
 				writer.writePack(monitor, monitor, os);
 			}
 
 			try (OutputStream os = new BufferedOutputStream(
-					dest.writeFile(pathIdx, monitor, wt + "..idx"))) { //$NON-NLS-1$
+					dest.writeFile(idx.getPath(), monitor,
+							wt + "." + idx.getPackExt().getExtension()))) { //$NON-NLS-1$
 				writer.writeIndex(os);
 			}
 
@@ -250,22 +254,22 @@
 			// and discover the most recent objects there.
 			//
 			final ArrayList<String> infoPacks = new ArrayList<>();
-			infoPacks.add(packName);
+			infoPacks.add(pack.getName());
 			infoPacks.addAll(packNames.keySet());
 			dest.writeInfoPacks(infoPacks);
 
 		} catch (IOException err) {
-			safeDelete(pathIdx);
-			safeDelete(pathPack);
+			safeDelete(idx);
+			safeDelete(pack);
 
 			throw new TransportException(uri, JGitText.get().cannotStoreObjects, err);
 		}
 	}
 
-	private void safeDelete(String path) {
+	private void safeDelete(File path) {
 		if (path != null) {
 			try {
-				dest.deleteFile(path);
+				dest.deleteFile(path.getPath());
 			} catch (IOException cleanupFailure) {
 				// Ignore the deletion failure. We probably are
 				// already failing and were just trying to pick
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory2.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory2.java
new file mode 100644
index 0000000..88abc60
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnectionFactory2.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 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.http;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A {@link HttpConnectionFactory} that supports client-side sessions that can
+ * maintain state and configure connections.
+ *
+ * @since 5.11
+ */
+public interface HttpConnectionFactory2 extends HttpConnectionFactory {
+
+	/**
+	 * Creates a new {@link GitSession} instance that can be used with
+	 * connections created by this {@link HttpConnectionFactory} instance.
+	 *
+	 * @return a new {@link GitSession}
+	 */
+	@NonNull
+	GitSession newSession();
+
+	/**
+	 * A {@code GitSession} groups the multiple HTTP connections
+	 * {@link org.eclipse.jgit.transport.TransportHttp TransportHttp} uses for
+	 * the requests it makes during a git fetch or push. A {@code GitSession}
+	 * can maintain client-side HTTPS state and can configure individual
+	 * connections.
+	 */
+	interface GitSession {
+
+		/**
+		 * Configure a just created {@link HttpConnection}.
+		 *
+		 * @param connection
+		 *            to configure; created by the same
+		 *            {@link HttpConnectionFactory} instance
+		 * @param sslVerify
+		 *            whether SSL is to be verified
+		 * @return the configured {@connection}
+		 * @throws IOException
+		 *             if the connection cannot be configured
+		 * @throws GeneralSecurityException
+		 *             if the connection cannot be configured
+		 */
+		@NonNull
+		HttpConnection configure(@NonNull HttpConnection connection,
+				boolean sslVerify) throws IOException, GeneralSecurityException;
+
+		/**
+		 * Closes the {@link GitSession}, releasing any internal state.
+		 */
+		void close();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnectionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnectionFactory.java
index b39f157..1b5d1b3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnectionFactory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/JDKHttpConnectionFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2013, 2020 Christian Halstrick <christian.halstrick@sap.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -12,6 +12,18 @@
 import java.io.IOException;
 import java.net.Proxy;
 import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.text.MessageFormat;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.transport.http.DelegatingSSLSocketFactory;
+import org.eclipse.jgit.util.HttpSupport;
 
 /**
  * A factory returning instances of
@@ -19,17 +31,70 @@
  *
  * @since 3.3
  */
-public class JDKHttpConnectionFactory implements HttpConnectionFactory {
-	/** {@inheritDoc} */
+public class JDKHttpConnectionFactory implements HttpConnectionFactory2 {
+
 	@Override
 	public HttpConnection create(URL url) throws IOException {
 		return new JDKHttpConnection(url);
 	}
 
-	/** {@inheritDoc} */
 	@Override
 	public HttpConnection create(URL url, Proxy proxy)
 			throws IOException {
 		return new JDKHttpConnection(url, proxy);
 	}
+
+	@Override
+	public GitSession newSession() {
+		return new JdkConnectionSession();
+	}
+
+	private static class JdkConnectionSession implements GitSession {
+
+		private SSLContext securityContext;
+
+		private SSLSocketFactory socketFactory;
+
+		@Override
+		public JDKHttpConnection configure(HttpConnection connection,
+				boolean sslVerify) throws GeneralSecurityException {
+			if (!(connection instanceof JDKHttpConnection)) {
+				throw new IllegalArgumentException(MessageFormat.format(
+						JGitText.get().httpWrongConnectionType,
+						JDKHttpConnection.class.getName(),
+						connection.getClass().getName()));
+			}
+			JDKHttpConnection conn = (JDKHttpConnection) connection;
+			String scheme = conn.getURL().getProtocol();
+			if (!"https".equals(scheme) || sslVerify) { //$NON-NLS-1$
+				// sslVerify == true: use the JDK defaults
+				return conn;
+			}
+			if (securityContext == null) {
+				securityContext = SSLContext.getInstance("TLS"); //$NON-NLS-1$
+				TrustManager[] trustAllCerts = {
+						new NoCheckX509TrustManager() };
+				securityContext.init(null, trustAllCerts, null);
+				socketFactory = new DelegatingSSLSocketFactory(
+						securityContext.getSocketFactory()) {
+
+					@Override
+					protected void configure(SSLSocket socket) {
+						HttpSupport.configureTLS(socket);
+					}
+				};
+			}
+			conn.setHostnameVerifier((name, session) -> true);
+			((HttpsURLConnection) conn.wrappedUrlConnection)
+					.setSSLSocketFactory(socketFactory);
+			return conn;
+		}
+
+		@Override
+		public void close() {
+			securityContext = null;
+			socketFactory = null;
+		}
+	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/NoCheckX509TrustManager.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/NoCheckX509TrustManager.java
new file mode 100644
index 0000000..5cd4fb4
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/NoCheckX509TrustManager.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016, 2020 JGit contributors
+ *
+ * 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
+ *
+ * Contributors:
+ *    Saša Živkov 2016 - initial API
+ *    Thomas Wolf 2020 - extracted from HttpSupport
+ */
+package org.eclipse.jgit.transport.http;
+
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * A {@link X509TrustManager} that doesn't verify anything. Can be used for
+ * skipping SSL checks.
+ *
+ * @since 5.11
+ */
+public class NoCheckX509TrustManager implements X509TrustManager {
+
+	@Override
+	public X509Certificate[] getAcceptedIssuers() {
+		return null;
+	}
+
+	@Override
+	public void checkClientTrusted(X509Certificate[] certs,
+			String authType) {
+		// no check
+	}
+
+	@Override
+	public void checkServerTrusted(X509Certificate[] certs,
+			String authType) {
+		// no check
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
index 72278dc..55b7d62 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
@@ -2,7 +2,7 @@
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
  * Copyright (C) 2010, Matthias Sohn <matthias.sohn@sap.com>
- * Copyright (C) 2012-2020, Robin Rosenberg and others
+ * Copyright (C) 2012-2021, Robin Rosenberg 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
@@ -63,6 +63,7 @@
 import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.Holder;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.Paths;
@@ -799,7 +800,10 @@
 			if (Constants.DOT_GIT.equals(name))
 				continue;
 			if (Constants.DOT_GIT_IGNORE.equals(name))
-				ignoreNode = new PerDirectoryIgnoreNode(e);
+				ignoreNode = new PerDirectoryIgnoreNode(
+						TreeWalk.pathOf(path, 0, pathOffset)
+								+ Constants.DOT_GIT_IGNORE,
+						e);
 			if (Constants.DOT_GIT_ATTRIBUTES.equals(name))
 				attributesNode = new PerDirectoryAttributesNode(e);
 			if (i != o)
@@ -983,8 +987,9 @@
 					return true;
 				} else if (ObjectId.zeroId().compareTo(idBuffer,
 						idOffset) == 0) {
-					return new File(repository.getWorkTree(),
-							entry.getPathString()).list().length > 0;
+					Path p = repository.getWorkTree().toPath()
+							.resolve(entry.getPathString());
+					return FileUtils.hasFiles(p);
 				}
 				return false;
 			} else if (mode == FileMode.SYMLINK.getBits())
@@ -1272,17 +1277,20 @@
 
 	/** Magic type indicating we know rules exist, but they aren't loaded. */
 	private static class PerDirectoryIgnoreNode extends IgnoreNode {
-		final Entry entry;
+		protected final Entry entry;
 
-		PerDirectoryIgnoreNode(Entry entry) {
+		private final String name;
+
+		PerDirectoryIgnoreNode(String name, Entry entry) {
 			super(Collections.<FastIgnoreRule> emptyList());
+			this.name = name;
 			this.entry = entry;
 		}
 
 		IgnoreNode load() throws IOException {
 			IgnoreNode r = new IgnoreNode();
 			try (InputStream in = entry.openInputStream()) {
-				r.parse(in);
+				r.parse(name, in);
 			}
 			return r.getRules().isEmpty() ? null : r;
 		}
@@ -1293,7 +1301,7 @@
 		final Repository repository;
 
 		RootIgnoreNode(Entry entry, Repository repository) {
-			super(entry);
+			super(entry != null ? entry.getName() : null, entry);
 			this.repository = repository;
 		}
 
@@ -1327,7 +1335,7 @@
 				throws FileNotFoundException, IOException {
 			if (FS.DETECTED.exists(exclude)) {
 				try (FileInputStream in = new FileInputStream(exclude)) {
-					r.parse(in);
+					r.parse(exclude.getAbsolutePath(), in);
 				}
 			}
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
index d8cab35..0946f64 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
@@ -22,7 +22,6 @@
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
-import java.io.PrintStream;
 import java.io.Writer;
 import java.nio.charset.Charset;
 import java.nio.file.AccessDeniedException;
@@ -337,6 +336,9 @@
 			try {
 				path = path.toAbsolutePath();
 				Path dir = Files.isDirectory(path) ? path : path.getParent();
+				if (dir == null) {
+					return FALLBACK_FILESTORE_ATTRIBUTES;
+				}
 				FileStoreAttributes cached = attrCacheByPath.get(dir);
 				if (cached != null) {
 					return cached;
@@ -511,7 +513,10 @@
 		}
 
 		private static void write(Path p, String body) throws IOException {
-			FileUtils.mkdirs(p.getParent().toFile(), true);
+			Path parent = p.getParent();
+			if (parent != null) {
+				FileUtils.mkdirs(parent.toFile(), true);
+			}
 			try (Writer w = new OutputStreamWriter(Files.newOutputStream(p),
 					UTF_8)) {
 				w.write(body);
@@ -1867,18 +1872,18 @@
 	 * @throws org.eclipse.jgit.api.errors.JGitInternalException
 	 *             if we fail to run the hook somehow. Causes may include an
 	 *             interrupted process or I/O errors.
-	 * @since 4.0
+	 * @since 5.11
 	 */
 	public ProcessResult runHookIfPresent(Repository repository,
 			final String hookName,
-			String[] args, PrintStream outRedirect, PrintStream errRedirect,
+			String[] args, OutputStream outRedirect, OutputStream errRedirect,
 			String stdinArgs) throws JGitInternalException {
 		return new ProcessResult(Status.NOT_SUPPORTED);
 	}
 
 	/**
 	 * See
-	 * {@link #runHookIfPresent(Repository, String, String[], PrintStream, PrintStream, String)}
+	 * {@link #runHookIfPresent(Repository, String, String[], OutputStream, OutputStream, String)}
 	 * . Should only be called by FS supporting shell scripts execution.
 	 *
 	 * @param repository
@@ -1903,11 +1908,11 @@
 	 * @throws org.eclipse.jgit.api.errors.JGitInternalException
 	 *             if we fail to run the hook somehow. Causes may include an
 	 *             interrupted process or I/O errors.
-	 * @since 4.0
+	 * @since 5.11
 	 */
 	protected ProcessResult internalRunHookIfPresent(Repository repository,
-			final String hookName, String[] args, PrintStream outRedirect,
-			PrintStream errRedirect, String stdinArgs)
+			final String hookName, String[] args, OutputStream outRedirect,
+			OutputStream errRedirect, String stdinArgs)
 			throws JGitInternalException {
 		File hookFile = findHook(repository, hookName);
 		if (hookFile == null || hookName == null) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
index fb63dc0..946d81c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
@@ -16,7 +16,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.io.PrintStream;
+import java.io.OutputStream;
 import java.nio.charset.Charset;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileStore;
@@ -268,7 +268,7 @@
 	/** {@inheritDoc} */
 	@Override
 	public ProcessResult runHookIfPresent(Repository repository, String hookName,
-			String[] args, PrintStream outRedirect, PrintStream errRedirect,
+			String[] args, OutputStream outRedirect, OutputStream errRedirect,
 			String stdinArgs) throws JGitInternalException {
 		return internalRunHookIfPresent(repository, hookName, args, outRedirect,
 				errRedirect, stdinArgs);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
index d53bff7..add5498 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
@@ -13,7 +13,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import java.io.File;
-import java.io.PrintStream;
+import java.io.OutputStream;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.util.ArrayList;
@@ -139,7 +139,7 @@
 	/** {@inheritDoc} */
 	@Override
 	public ProcessResult runHookIfPresent(Repository repository, String hookName,
-			String[] args, PrintStream outRedirect, PrintStream errRedirect,
+			String[] args, OutputStream outRedirect, OutputStream errRedirect,
 			String stdinArgs) throws JGitInternalException {
 		return internalRunHookIfPresent(repository, hookName, args, outRedirect,
 				errRedirect, stdinArgs);
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 aa39a44..b9dd9ba 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
@@ -43,6 +43,7 @@
 import java.util.Locale;
 import java.util.Random;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
@@ -802,6 +803,23 @@
 	}
 
 	/**
+	 * Whether the path is a directory with files in it.
+	 *
+	 * @param dir
+	 *            directory path
+	 * @return {@code true} if the given directory path contains files
+	 * @throws IOException
+	 *             on any I/O errors accessing the path
+	 *
+	 * @since 5.11
+	 */
+	public static boolean hasFiles(Path dir) throws IOException {
+		try (Stream<Path> stream = Files.list(dir)) {
+			return stream.findAny().isPresent();
+		}
+	}
+
+	/**
 	 * Whether the given file can be executed.
 	 *
 	 * @param file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
index 04b3eab..23a73fa 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
@@ -24,21 +24,18 @@
 import java.net.URLEncoder;
 import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
-import java.security.cert.X509Certificate;
 import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.Set;
 
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
 
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.transport.http.HttpConnection;
+import org.eclipse.jgit.transport.http.NoCheckX509TrustManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -301,40 +298,13 @@
 	 */
 	public static void disableSslVerify(HttpConnection conn)
 			throws IOException {
-		final TrustManager[] trustAllCerts = new TrustManager[] {
-				new DummyX509TrustManager() };
+		TrustManager[] trustAllCerts = {
+				new NoCheckX509TrustManager() };
 		try {
 			conn.configure(null, trustAllCerts, null);
-			conn.setHostnameVerifier(new DummyHostnameVerifier());
+			conn.setHostnameVerifier((name, session) -> true);
 		} catch (KeyManagementException | NoSuchAlgorithmException e) {
-			throw new IOException(e.getMessage());
-		}
-	}
-
-	private static class DummyX509TrustManager implements X509TrustManager {
-		@Override
-		public X509Certificate[] getAcceptedIssuers() {
-			return null;
-		}
-
-		@Override
-		public void checkClientTrusted(X509Certificate[] certs,
-				String authType) {
-			// no check
-		}
-
-		@Override
-		public void checkServerTrusted(X509Certificate[] certs,
-				String authType) {
-			// no check
-		}
-	}
-
-	private static class DummyHostnameVerifier implements HostnameVerifier {
-		@Override
-		public boolean verify(String hostname, SSLSession session) {
-			// always accept
-			return true;
+			throw new IOException(e.getMessage(), e);
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java
index 680d903..6d5694e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java
@@ -246,7 +246,7 @@
 	 *            buffer that must be fully populated, [off, off+len).
 	 * @param off
 	 *            position within the buffer to start writing to.
-	 * @return number of bytes in buffer or stream, whichever is shortest
+	 * @return number of bytes read
 	 * @throws java.io.IOException
 	 *             there was an error reading from the stream.
 	 */
@@ -254,8 +254,8 @@
 			throws IOException {
 		int r;
 		int len = 0;
-		while ((r = fd.read(dst, off, dst.length - off)) >= 0
-				&& len < dst.length) {
+		while (off < dst.length
+				&& (r = fd.read(dst, off, dst.length - off)) >= 0) {
 			off += r;
 			len += r;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java
new file mode 100644
index 0000000..cf06172
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.GpgSignatureVerifier.TrustLevel;
+import org.eclipse.jgit.lib.PersonIdent;
+
+/**
+ * Utilities for signature verification.
+ *
+ * @since 5.11
+ */
+public final class SignatureUtils {
+
+	private SignatureUtils() {
+		// No instantiation
+	}
+
+	/**
+	 * Writes information about a signature verification to a string.
+	 *
+	 * @param verification
+	 *            to show
+	 * @param creator
+	 *            of the object verified; used for time zone information
+	 * @param formatter
+	 *            to use for dates
+	 * @return a textual representation of the {@link SignatureVerification},
+	 *         using LF as line separator
+	 */
+	public static String toString(SignatureVerification verification,
+			PersonIdent creator, GitDateFormatter formatter) {
+		StringBuilder result = new StringBuilder();
+		// Use the creator's timezone for the signature date
+		PersonIdent dateId = new PersonIdent(creator,
+				verification.getCreationDate());
+		result.append(MessageFormat.format(JGitText.get().verifySignatureMade,
+				formatter.formatDate(dateId)));
+		result.append('\n');
+		result.append(MessageFormat.format(
+				JGitText.get().verifySignatureKey,
+				verification.getKeyFingerprint().toUpperCase(Locale.ROOT)));
+		result.append('\n');
+		if (!StringUtils.isEmptyOrNull(verification.getSigner())) {
+			result.append(
+					MessageFormat.format(JGitText.get().verifySignatureIssuer,
+							verification.getSigner()));
+			result.append('\n');
+		}
+		String msg;
+		if (verification.getVerified()) {
+			if (verification.isExpired()) {
+				msg = JGitText.get().verifySignatureExpired;
+			} else {
+				msg = JGitText.get().verifySignatureGood;
+			}
+		} else {
+			msg = JGitText.get().verifySignatureBad;
+		}
+		result.append(MessageFormat.format(msg, verification.getKeyUser()));
+		if (!TrustLevel.UNKNOWN.equals(verification.getTrustLevel())) {
+			result.append(' ' + MessageFormat
+					.format(JGitText.get().verifySignatureTrust, verification
+							.getTrustLevel().name().toLowerCase(Locale.ROOT)));
+		}
+		result.append('\n');
+		msg = verification.getMessage();
+		if (!StringUtils.isEmptyOrNull(msg)) {
+			result.append(msg);
+			result.append('\n');
+		}
+		return result.toString();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
index 447f417..54fd539 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
@@ -56,9 +56,9 @@
 
 	private static final SystemReader DEFAULT;
 
-	private static Boolean isMacOS;
+	private static volatile Boolean isMacOS;
 
-	private static Boolean isWindows;
+	private static volatile Boolean isWindows;
 
 	static {
 		SystemReader r = new Default();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java
index 1f0fedd..562eb05 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java
@@ -230,11 +230,16 @@
 		if (Integer.MAX_VALUE < len)
 			throw new OutOfMemoryError(
 					JGitText.get().lengthExceedsMaximumArraySize);
-		final byte[] out = new byte[(int) len];
+		int length = (int) len;
+		final byte[] out = new byte[length];
 		int outPtr = 0;
 		for (Block b : blocks) {
-			System.arraycopy(b.buffer, 0, out, outPtr, b.count);
-			outPtr += b.count;
+			int toCopy = Math.min(length - outPtr, b.count);
+			System.arraycopy(b.buffer, 0, out, outPtr, toCopy);
+			outPtr += toCopy;
+			if (outPtr == length) {
+				break;
+			}
 		}
 		return out;
 	}
@@ -461,6 +466,30 @@
 		}
 
 		@Override
+		public byte[] toByteArray(int limit) throws IOException {
+			if (onDiskFile == null) {
+				return super.toByteArray(limit);
+			}
+			final long len = Math.min(length(), limit);
+			if (Integer.MAX_VALUE < len) {
+				throw new OutOfMemoryError(
+						JGitText.get().lengthExceedsMaximumArraySize);
+			}
+			final byte[] out = new byte[(int) len];
+			try (FileInputStream in = new FileInputStream(onDiskFile)) {
+				int read = 0;
+				int chunk;
+				while ((chunk = in.read(out, read, out.length - read)) >= 0) {
+					read += chunk;
+					if (read == out.length) {
+						break;
+					}
+				}
+			}
+			return out;
+		}
+
+		@Override
 		public void writeTo(OutputStream os, ProgressMonitor pm)
 				throws IOException {
 			if (onDiskFile == null) {
diff --git a/pom.xml b/pom.xml
index 38b402d..b888ad3 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>5.10.1-SNAPSHOT</version>
+  <version>5.11.2-SNAPSHOT</version>
 
   <name>JGit - Parent</name>
   <url>${jgit-url}</url>
@@ -151,8 +151,8 @@
     <maven.compiler.target>1.8</maven.compiler.target>
     <bundle-manifest>${project.build.directory}/META-INF/MANIFEST.MF</bundle-manifest>
 
-    <jgit-last-release-version>5.9.0.202009080501-r</jgit-last-release-version>
-    <apache-sshd-version>2.4.0</apache-sshd-version>
+    <jgit-last-release-version>5.10.0.202012080955-r</jgit-last-release-version>
+    <apache-sshd-version>2.6.0</apache-sshd-version>
     <jsch-version>0.1.55</jsch-version>
     <jzlib-version>1.1.1</jzlib-version>
     <javaewah-version>1.1.7</javaewah-version>
@@ -162,20 +162,20 @@
     <commons-compress-version>1.19</commons-compress-version>
     <osgi-core-version>4.3.1</osgi-core-version>
     <servlet-api-version>3.1.0</servlet-api-version>
-    <jetty-version>9.4.30.v20200611</jetty-version>
-    <japicmp-version>0.14.3</japicmp-version>
-    <httpclient-version>4.5.10</httpclient-version>
-    <httpcore-version>4.4.12</httpcore-version>
+    <jetty-version>9.4.36.v20210114</jetty-version>
+    <japicmp-version>0.14.4</japicmp-version>
+    <httpclient-version>4.5.13</httpclient-version>
+    <httpcore-version>4.4.14</httpcore-version>
     <slf4j-version>1.7.30</slf4j-version>
     <log4j-version>1.2.15</log4j-version>
     <maven-javadoc-plugin-version>3.2.0</maven-javadoc-plugin-version>
     <tycho-extras-version>1.7.0</tycho-extras-version>
-    <gson-version>2.8.2</gson-version>
+    <gson-version>2.8.6</gson-version>
     <bouncycastle-version>1.65</bouncycastle-version>
-    <spotbugs-maven-plugin-version>4.1.3</spotbugs-maven-plugin-version>
+    <spotbugs-maven-plugin-version>4.2.0</spotbugs-maven-plugin-version>
     <maven-project-info-reports-plugin-version>3.1.1</maven-project-info-reports-plugin-version>
     <maven-jxr-plugin-version>3.0.0</maven-jxr-plugin-version>
-    <maven-surefire-plugin-version>3.0.0-M4</maven-surefire-plugin-version>
+    <maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
     <maven-surefire-report-plugin-version>${maven-surefire-plugin-version}</maven-surefire-report-plugin-version>
     <maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
     <plexus-compiler-version>2.8.8</plexus-compiler-version>
@@ -202,6 +202,14 @@
       <id>repo.eclipse.org.cbi-snapshots</id>
       <url>https://repo.eclipse.org/content/repositories/cbi-snapshots/</url>
     </pluginRepository>
+    <pluginRepository>
+      <id>repo.eclipse.org.dash-releases</id>
+      <url>https://repo.eclipse.org/content/repositories/dash-licenses-releases/</url>
+    </pluginRepository>
+    <pluginRepository>
+      <id>repo.eclipse.org.dash-snapshots</id>
+      <url>https://repo.eclipse.org/content/repositories/dash-licenses-snapshots/</url>
+    </pluginRepository>
   </pluginRepositories>
 
   <build>
@@ -347,7 +355,7 @@
             <dependency><!-- add support for ssh/scp -->
               <groupId>org.apache.maven.wagon</groupId>
               <artifactId>wagon-ssh</artifactId>
-              <version>3.4.0</version>
+              <version>3.4.2</version>
             </dependency>
           </dependencies>
         </plugin>
@@ -389,7 +397,12 @@
         <plugin>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-maven-plugin</artifactId>
-          <version>2.1.5.RELEASE</version>
+          <version>2.4.1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.eclipse.dash</groupId>
+          <artifactId>license-tool-plugin</artifactId>
+          <version>0.0.1-SNAPSHOT</version>
         </plugin>
       </plugins>
     </pluginManagement>
@@ -408,7 +421,7 @@
             <configuration>
               <rules>
                 <requireMavenVersion>
-                  <version>3.6.2</version>
+                  <version>3.6.3</version>
                 </requireMavenVersion>
               </rules>
             </configuration>
@@ -549,6 +562,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-report-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.eclipse.dash</groupId>
+        <artifactId>license-tool-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>
 
@@ -814,54 +831,41 @@
               <encoding>UTF-8</encoding>
               <source>1.8</source>
               <target>1.8</target>
+              <compilerArgs>
+                <arg>-XDcompilePolicy=simple</arg>
+                <arg>-Xplugin:ErrorProne</arg>
+              </compilerArgs>
+              <annotationProcessorPaths>
+                <path>
+                  <groupId>com.google.errorprone</groupId>
+                  <artifactId>error_prone_core</artifactId>
+                  <version>2.4.0</version>
+                </path>
+              </annotationProcessorPaths>
             </configuration>
-            <executions>
-              <execution>
-                <id>default-compile</id>
-                <phase>compile</phase>
-                <goals>
-                  <goal>compile</goal>
-                </goals>
-                <configuration>
-                  <includes>
-                    <include>org/eclipse/jgit/transport/InsecureCipherFactory.java</include>
-                  </includes>
-                </configuration>
-              </execution>
-              <execution>
-                <id>compile-with-errorprone</id>
-                <phase>compile</phase>
-                <goals>
-                  <goal>compile</goal>
-                </goals>
-                <configuration>
-                  <compilerId>javac-with-errorprone</compilerId>
-                  <forceJavacCompilerUse>true</forceJavacCompilerUse>
-                  <excludes>
-                    <exclude>org/eclipse/jgit/transport/InsecureCipherFactory.java</exclude>
-                  </excludes>
-                </configuration>
-              </execution>
-            </executions>
-            <dependencies>
-              <dependency>
-                <groupId>org.codehaus.plexus</groupId>
-                <artifactId>plexus-compiler-javac</artifactId>
-                <version>${plexus-compiler-version}</version>
-              </dependency>
-              <dependency>
-                <groupId>org.codehaus.plexus</groupId>
-                <artifactId>plexus-compiler-javac-errorprone</artifactId>
-                <version>${plexus-compiler-version}</version>
-              </dependency>
-              <!-- override plexus-compiler-javac-errorprone's dependency on
-                  Error Prone with the latest version -->
-              <dependency>
-                <groupId>com.google.errorprone</groupId>
-                <artifactId>error_prone_core</artifactId>
-                <version>2.3.4</version>
-              </dependency>
-            </dependencies>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>jdk8</id>
+      <activation>
+        <jdk>1.8</jdk>
+      </activation>
+      <properties>
+        <javac.version>9+181-r4173-1</javac.version>
+      </properties>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+              <fork>true</fork>
+              <compilerArgs combine.children="append">
+                <arg>-J-Xbootclasspath/p:${settings.localRepository}/com/google/errorprone/javac/${javac.version}/javac-${javac.version}.jar</arg>
+              </compilerArgs>
+            </configuration>
           </plugin>
         </plugins>
       </build>
@@ -896,7 +900,7 @@
               <dependency>
                 <groupId>org.eclipse.jdt</groupId>
                 <artifactId>ecj</artifactId>
-                <version>3.23.0</version>
+                <version>3.24.0</version>
               </dependency>
             </dependencies>
           </plugin>