Merge branch 'master' into next

* master: (497 commits)
  Prepare 7.3.0-SNAPSHOT builds
  Prepare 7.2.1-SNAPSHOT builds
  JGit v7.2.0.202503040940-r
  JGit v7.2.0.202503040805-r
  CacheRegion: fix non translatable text warnings
  Ensure access to autoRefresh is thread-safe
  FileReftableStack: use FileSnapshot to detect modification
  FileReftableDatabase: consider ref updates by another process
  BlameRegionMerger: report invalid regions with checked exception.
  Prepare 7.2.0-SNAPSHOT builds
  [ssh known_hosts] Handle unknown keys better
  [releng] Remove unused target platform definitions
  JGit v7.2.0.202502261823-rc1
  [ssh known_hosts] Handle host certificates
  [ssh known_hosts] Improve updating modified keys
  [ssh known_hosts] Add tests and fix problems
  [ssh, releng] Remove net.i2p.crypto.eddsa
  AddCommand: Use parenthesis to make the operator precedence explicit
  AddCommand: implement --all/--no-all
  Do not load bitmap indexes during directory scans
  ...

Change-Id: I619c89071f5f7a05bcd0218840f7f47bd19b779d
diff --git a/.bazelrc b/.bazelrc
index efa007a..70322dd 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -2,7 +2,7 @@
 # https://issues.gerritcodereview.com/issues/303819949
 common --noenable_bzlmod
 
-build --workspace_status_command="python ./tools/workspace_status.py"
+build --workspace_status_command="python3 ./tools/workspace_status.py"
 build --repository_cache=~/.gerritcodereview/bazel-cache/repository
 build --incompatible_strict_action_env
 build --action_env=PATH
@@ -37,5 +37,6 @@
 test --build_tests_only
 test --test_output=errors
 test --flaky_test_attempts=3
+test --test_tag_filters=-ext
 
 import %workspace%/tools/remote-bazelrc
diff --git a/.mailmap b/.mailmap
index 7116ebb..1c77395 100644
--- a/.mailmap
+++ b/.mailmap
@@ -1,20 +1,33 @@
+Adithya Chakilam <quic_achakila@quicinc.com>                Adithya Chakilam <achakila@codeaurora.org>
 Chris Aniszczyk <caniszczyk@gmail.com>                      Chris Aniszczyk <zx@eclipsesource.com>
 Christian Halstrick <christian.halstrick@sap.com>           Christian Halstrick <christian.halstrick@gmail.com>
 Dani Megert <Daniel_Megert@ch.ibm.com>                      Daniel Megert <daniel_megert@ch.ibm.com>
+Dariusz Luksza <dariusz.luksza@gmail.com>                   Dariusz Luksza <dariusz@luksza.org>
+David Ostrovsky <david.ostrovsky@gmail.com>                 David Ostrovsky <david@ostrovsky.org>
 David Pursehouse <david.pursehouse@gmail.com>               David Pursehouse <david.pursehouse@sonymobile.com>
 Han-Wen Nienhuys <hanwen@google.com>                        Han-Wen NIenhuys <hanwen@google.com>
 Hector Oswaldo Caballero <hector.caballero@ericsson.com>    Hector Caballero <hector.caballero@ericsson.com>
+Jackson Toeniskoetter <jackdt@google.com>                   <jackdt@google.com>
 Lars Vogel <Lars.Vogel@vogella.com>                         Lars Vogel <Lars.Vogel@gmail.com>
 Mark Ingram <markdingram@gmail.com>                         markdingram <markdingram@gmail.com>
 Markus Duft <markus.duft@ssi-schaefer.com>                  Markus Duft <markus.duft@salomon.at>
+Martin Fick <mfick@nvidia.com>                              Martin Fick <mfick@codeaurora.org>
+Martin Fick <mfick@nvidia.com>                              Martin Fick <quic_mfick@quicinc.com>
 Michael Keppler <michael.keppler@gmx.de>                    Michael Keppler <Michael.Keppler@gmx.de>
+Nasser Grainawi <quic_nasserg@quicinc.com>                  Nasser Grainawi <nasser@codeaurora.org>
 Roberto Tyley <roberto.tyley@guardian.co.uk>                roberto <roberto.tyley@guardian.co.uk>
 Saša Živkov <sasa.zivkov@sap.com>                           Sasa Zivkov <sasa.zivkov@sap.com>
 Saša Živkov <sasa.zivkov@sap.com>                           Saša Živkov <zivkov@gmail.com>
 Saša Živkov <sasa.zivkov@sap.com>                           Sasa Zivkov <zivkov@gmail.com>
 Sebastian Schuberth <sschuberth@gmail.com>                  Sebastian Schuberth <sebastian.schuberth@bosch.io>
+Sebastian Schuberth <sschuberth@gmail.com>                  <opensource@schuberth.dev>
 Shawn Pearce <spearce@spearce.org>                          Shawn O. Pearce <sop@google.com>
 Shawn Pearce <spearce@spearce.org>                          Shawn Pearce <sop@google.com>
 Shawn Pearce <spearce@spearce.org>                          Shawn O. Pearce <spearce@spearce.org>
+Sven Selberg <sven.selberg@axis.com>                        Sven Selberg <sven.selberg@sonymobile.com>
 Terry Parker <tparker@google.com>                           tparker <tparker@google.com>
 Thomas Wolf <twolf@apache.org>                              Thomas Wolf <thomas.wolf@paranor.ch>
+Tomasz Zarna <tzarna@gmail.com>                             <Tomasz.Zarna@pl.ibm.com>
+Tomasz Zarna <tzarna@gmail.com>                             <tomasz.zarna@tasktop.com>
+Tobias Pfeifer <to.pfeifer@web.de>                          <to.pfeifer@sap.com>
+Yunjie Li <yunjieli@google.com>                             <yunjieli@google.com>
diff --git a/Documentation/config-options.md b/Documentation/config-options.md
index 78930e6..4dde8f8 100644
--- a/Documentation/config-options.md
+++ b/Documentation/config-options.md
@@ -31,6 +31,7 @@
 | `core.dfs.blockSize` | `64 kiB` | &#x20DE; | Size in bytes of a single window read in from the pack file into the DFS block cache. |
 | `core.dfs.concurrencyLevel` | `32` | &#x20DE; | The estimated number of threads concurrently accessing the DFS block cache. |
 | `core.dfs.deltaBaseCacheLimit` | `10 MiB` | &#x20DE; | Maximum number of bytes to hold in per-reader DFS delta base cache. |
+| `core.dfs.loadRevIndexInParallel` | false; | &#x20DE; | Try to load the reverse index in parallel with the bitmap index. |
 | `core.dfs.streamFileThreshold` | `50 MiB` | &#x20DE; | The size threshold beyond which objects must be streamed. |
 | `core.dfs.streamBuffer` | Block size of the pack | &#x20DE; | Number of bytes to use for buffering when streaming a pack file during copying. If 0 the block size of the pack is used|
 | `core.dfs.streamRatio` | `0.30` | &#x20DE; | Ratio of DFS block cache to occupy with a copied pack. Values between `0` and `1.0`. |
@@ -54,9 +55,13 @@
 | `core.streamFileThreshold` | `50 MiB` | &#x20DE; | The size threshold beyond which objects must be streamed. |
 | `core.supportsAtomicFileCreation` | `true` | &#x20DE; | Whether the filesystem supports atomic file creation. |
 | `core.symlinks` | Auto detect if filesystem supports symlinks| &#x2705; | If false, symbolic links are checked out as small plain files that contain the link text. |
-| `core.trustFolderStat` | `true` | &#x20DE; | Whether to trust the pack folder's, packed-refs file's and loose-objects folder's file attributes (Java equivalent of stat command on *nix). When looking for pack files, if `false` JGit will always scan the `.git/objects/pack` folder and if set to `true` it assumes that pack files are unchanged if the file attributes of the pack folder are unchanged. When getting the list of packed refs, if `false` JGit will always read the packed-refs file and if set to `true` it uses the file attributes of the packed-refs file and will only read it if a file attribute has changed. When looking for loose objects, if `false` and if a loose object is not found, JGit will open and close a stream to `.git/objects` folder (which can refresh its directory listing, at least on some NFS clients) and retry looking for that loose object. Setting this option to `false` can help to workaround caching issues on NFS, but reduces performance. |
-| `core.trustPackedRefsStat` | `unset` | &#x20DE; | Whether to trust the file attributes (Java equivalent of stat command on *nix) of the packed-refs file. If `never` JGit will ignore the file attributes of the packed-refs file and always read it. If `always` JGit will trust the file attributes of the packed-refs file and will only read it if a file attribute has changed. `after_open` behaves the same as `always`, except that the packed-refs file is opened and closed before its file attributes are considered. An open/close of the packed-refs file is known to refresh its file attributes, at least on some NFS clients. If `unset`, JGit will use the behavior described in `trustFolderStat`. |
-| `core.trustLooseRefStat` | `always` | &#x20DE; | Whether to trust the file attributes (Java equivalent of stat command on *nix) of the loose ref. If `always` JGit will trust the file attributes of the loose ref and its parent directories. `after_open` behaves similar to `always`, except that all parent directories of the loose ref up to the repository root are opened and closed before its file attributes are considered. An open/close of these parent directories is known to refresh the file attributes, at least on some NFS clients. |
+| ~~`core.trustFolderStat`~~ | `true` | &#x20DE; | __Deprecated__, use `core.trustStat` instead. If set to `true` translated to `core.trustStat=always`, if `false` translated to `core.trustStat=never`, see below. If both `core.trustFolderStat` and `core.trustStat` are configured then `trustStat` takes precedence and `trustFolderStat` is ignored. |
+| `core.trustLooseRefStat` | `inherit` | &#x20DE; | Whether to trust the file attributes of loose refs and its fan-out parent directory. See `core.trustStat` for possible values. If `inherit`, JGit will use the behavior configured in `trustStat`. |
+| `core.trustPackedRefsStat` | `inherit` | &#x20DE; | Whether to trust the file attributes of the packed-refs file. See `core.trustStat` for possible values. If `inherit`, JGit will use the behavior configured in `core.trustStat`. |
+| `core.trustTablesListStat` | `inherit` | &#x20DE; | Whether to trust the file attributes of the `tables.list` file used by the reftable ref storage backend to store the list of reftable filenames. See `core.trustStat` for possible values. If `inherit`, JGit will use the behavior configured in `core.trustStat`.  The reftable backend is used if `extensions.refStorage = reftable`. |
+| `core.trustLooseObjectStat` | `inherit` | &#x20DE; | Whether to trust the file attributes of the loose object file and its fan-out parent directory. See `core.trustStat` for possible values. If `inherit`, JGit will use the behavior configured in `core.trustStat`. |
+| `core.trustPackStat` | `inherit` | &#x20DE; | Whether to trust the file attributes of the `objects/pack` directory. See `core.trustStat` for possible values. If `inherit`, JGit will use the behavior configured in `core.trustStat`. |
+| `core.trustStat` | `always` | &#x20DE; | Global option to configure whether to trust file attributes (Java equivalent of stat command on Unix) of files storing git objects. Can be overridden for specific files by configuring `core.trustLooseRefStat, core.trustPackedRefsStat, core.trustLooseObjectStat, core.trustPackStat,core.trustTablesListStat`. If `never` JGit will ignore the file attributes of the file and always read it. If `always` JGit will trust the file attributes and will only read it if a file attribute has changed. `after_open` behaves the same as `always`, but file attributes are only considered *after* the file itself and any transient parent directories have been opened and closed. An open/close of the file/directory is known to refresh its file attributes, at least on some NFS clients. |
 | `core.worktree` | Root directory of the working tree if it is not the parent directory of the `.git` directory | &#x2705; | The path to the root of the working tree. |
 
 ## __fetch__ options
@@ -130,6 +135,13 @@
 | `pack.window` | `10` | &#x2705; | Number of objects to try when looking for a delta base per thread searching for deltas. |
 | `pack.windowMemory` | `0` (unlimited) | &#x2705; | Maximum number of bytes to put into the delta search window. |
 
+## reftable options
+
+|  option | default | git option | description |
+|---------|---------|------------|-------------|
+| `reftable.autoRefresh` | `false` | &#x20DE; | Whether to auto-refresh the reftable stack if it is out of date. |
+
+
 ## __repack__ options
 
 |  option | default | git option | description |
diff --git a/WORKSPACE b/WORKSPACE
index ab80a64..505141c 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -73,12 +73,6 @@
 )
 
 maven_jar(
-    name = "eddsa",
-    artifact = "net.i2p.crypto:eddsa:0.3.0",
-    sha1 = "1901c8d4d8bffb7d79027686cfb91e704217c3e1",
-)
-
-maven_jar(
     name = "jsch",
     artifact = "com.jcraft:jsch:0.1.55",
     sha1 = "bbd40e5aa7aa3cfad5db34965456cee738a42a50",
@@ -108,44 +102,44 @@
     sha1 = "51cf043c87253c9f58b539c9f7e44c8894223850",
 )
 
-SSHD_VERS = "2.12.0"
+SSHD_VERS = "2.15.0"
 
 maven_jar(
     name = "sshd-osgi",
     artifact = "org.apache.sshd:sshd-osgi:" + SSHD_VERS,
-    sha1 = "32b8de1cbb722ba75bdf9898e0c41d42af00ce57",
+    sha1 = "aa76898fe47eab7da0878dd60e6f3be5631e076c",
 )
 
 maven_jar(
     name = "sshd-sftp",
     artifact = "org.apache.sshd:sshd-sftp:" + SSHD_VERS,
-    sha1 = "0f96f00a07b186ea62838a6a4122e8f4cad44df6",
+    sha1 = "2e226055ed060c64ed76256a9c45de6d0109eef8",
 )
 
-JNA_VERS = "5.14.0"
+JNA_VERS = "5.16.0"
 
 maven_jar(
     name = "jna",
     artifact = "net.java.dev.jna:jna:" + JNA_VERS,
-    sha1 = "67bf3eaea4f0718cb376a181a629e5f88fa1c9dd",
+    sha1 = "ebea09f91dc9f7048099f963fb8d6f919f0a4d9c",
 )
 
 maven_jar(
     name = "jna-platform",
     artifact = "net.java.dev.jna:jna-platform:" + JNA_VERS,
-    sha1 = "28934d48aed814f11e4c584da55c49fa7032b31b",
+    sha1 = "b2a9065f97c166893d504b164706512338e3bbc2",
 )
 
 maven_jar(
     name = "commons-codec",
-    artifact = "commons-codec:commons-codec:1.16.0",
-    sha1 = "4e3eb3d79888d76b54e28b350915b5dc3919c9de",
+    artifact = "commons-codec:commons-codec:1.18.0",
+    sha1 = "ee45d1cf6ec2cc2b809ff04b4dc7aec858e0df8f",
 )
 
 maven_jar(
     name = "commons-logging",
-    artifact = "commons-logging:commons-logging:1.2",
-    sha1 = "4bfc12adfe4842bf07b657f0369c4cb522955686",
+    artifact = "commons-logging:commons-logging:1.3.5",
+    sha1 = "a3fcc5d3c29b2b03433aa2d2f2d2c1b1638924a1",
 )
 
 maven_jar(
@@ -162,32 +156,38 @@
 
 maven_jar(
     name = "servlet-api",
-    artifact = "jakarta.servlet:jakarta.servlet-api:6.0.0",
-    sha1 = "abecc699286e65035ebba9844c03931357a6a963",
+    artifact = "jakarta.servlet:jakarta.servlet-api:6.1.0",
+    sha1 = "1169a246913fe3823782af7943e7a103634867c5",
 )
 
 maven_jar(
     name = "commons-compress",
-    artifact = "org.apache.commons:commons-compress:1.26.0",
-    sha1 = "659feffdd12280201c8aacb8f7be94f9a883c824",
+    artifact = "org.apache.commons:commons-compress:1.27.1",
+    sha1 = "a19151084758e2fbb6b41eddaa88e7b8ff4e6599",
+)
+
+maven_jar(
+    name = "commons-lang3",
+    artifact = "org.apache.commons:commons-lang3:3.17.0",
+    sha1 = "b17d2136f0460dcc0d2016ceefca8723bdf4ee70",
 )
 
 maven_jar(
     name = "commons-io",
-    artifact = "commons-io:commons-io:2.15.1",
-    sha1 = "f11560da189ab563a5c8e351941415430e9304ea",
+    artifact = "commons-io:commons-io:2.18.0",
+    sha1 = "44084ef756763795b31c578403dd028ff4a22950",
 )
 
 maven_jar(
     name = "tukaani-xz",
-    artifact = "org.tukaani:xz:1.9",
-    sha1 = "1ea4bec1a921180164852c65006d928617bd2caf",
+    artifact = "org.tukaani:xz:1.10",
+    sha1 = "1be8166f89e035a56c6bfc67dbc423996fe577e2",
 )
 
 maven_jar(
     name = "args4j",
-    artifact = "args4j:args4j:2.33",
-    sha1 = "bd87a75374a6d6523de82fef51fc3cfe9baf9fc9",
+    artifact = "args4j:args4j:2.37",
+    sha1 = "244f60c057d72a785227c0562d3560f42a7ea54b",
 )
 
 maven_jar(
@@ -204,126 +204,114 @@
 
 maven_jar(
     name = "mockito",
-    artifact = "org.mockito:mockito-core:5.10.0",
-    sha1 = "b3812fa2ee069f1d0b41c1c0155da79d0e1dcde0",
+    artifact = "org.mockito:mockito-core:5.15.2",
+    sha1 = "87be4b1e0cc5febc07ab3197a8ff3ede56b37a79",
 )
 
 maven_jar(
     name = "assertj-core",
-    artifact = "org.assertj:assertj-core:3.25.3",
-    sha1 = "792b270e73aa1cfc28fa135be0b95e69ea451432",
+    artifact = "org.assertj:assertj-core:3.27.3",
+    sha1 = "31f5d58a202bd5df4993fb10fa2cffd610c20d6f",
 )
 
-BYTE_BUDDY_VERSION = "1.14.12"
+BYTE_BUDDY_VERSION = "1.17.1"
 
 maven_jar(
     name = "bytebuddy",
     artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
-    sha1 = "6e37f743dc15a8d7a4feb3eb0025cbc612d5b9e1",
+    sha1 = "8b5205fad48196a88d3d66dddff5a7417bce3596",
 )
 
 maven_jar(
     name = "bytebuddy-agent",
     artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
-    sha1 = "be4984cb6fd1ef1d11f218a648889dfda44b8a15",
+    sha1 = "0669a13b59d5ffd8198a79e4dc99018a9278e457",
 )
 
 maven_jar(
     name = "objenesis",
-    artifact = "org.objenesis:objenesis:3.3",
-    sha1 = "1049c09f1de4331e8193e579448d0916d75b7631",
+    artifact = "org.objenesis:objenesis:3.4",
+    sha1 = "675cbe121a68019235d27f6c34b4f0ac30e07418",
 )
 
 maven_jar(
     name = "gson",
-    artifact = "com.google.code.gson:gson:2.10.1",
-    sha1 = "b3add478d4382b78ea20b1671390a858002feb6c",
+    artifact = "com.google.code.gson:gson:2.12.1",
+    sha1 = "4e773a317740b83b43cfc3d652962856041697cb",
 )
 
-JETTY_VER = "12.0.9"
+JETTY_VER = "12.0.16"
 
 maven_jar(
     name = "jetty-servlet",
     artifact = "org.eclipse.jetty.ee10:jetty-ee10-servlet:" + JETTY_VER,
-    sha1 = "19e056d75741e7348411d677a9b26a54ea4b7d16",
-    src_sha1 = "d7fcb4e9d259c1dd8595c6163054be449072fe14",
+    sha1 = "022a746c00b1ac5c790fee65a398c707160a46d8",
 )
 
 maven_jar(
     name = "jetty-security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VER,
-    sha1 = "0c03a77f9d1a8b595cb4a83011cef735bd34bc95",
-    src_sha1 = "9fba93bbce1466ef9c77d7a75338abd479641721",
+    sha1 = "23b1a3abecf9d6f5498064a32d9145ae1d8330f9",
 )
 
 maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VER,
-    sha1 = "87adc518dd68b674e08d7e4968d07b6dc73214f1",
-    src_sha1 = "5404f097aae0126b820b040b4e924f31fe4ff25b",
+    sha1 = "3e3638b4bfbee04c27b3ae68e4949fc43b40a042",
 )
 
 maven_jar(
     name = "jetty-session",
     artifact = "org.eclipse.jetty:jetty-session:" + JETTY_VER,
-    sha1 = "628444f02dfbc4efbd1920a12e055580b227e86b",
-    src_sha1 = "2ac0ca2c84fa8e40655af1482a9c67d60d659ad2",
+    sha1 = "79cdedc7afebbdba4453f603dfe2f970baa35cc3",
 )
 
 maven_jar(
     name = "jetty-http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VER,
-    sha1 = "cb54f006b1484306bc4b24dc3802cff472a6ba82",
-    src_sha1 = "a1a6bc169e06007cabf6534fd7c7d1f2e91ab775",
+    sha1 = "68019fa90e8420ae15c109bd8c8611cacbaf43e5",
 )
 
 maven_jar(
     name = "jetty-io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VER,
-    sha1 = "03e8f5b5c6d583ea591064671c23b8639c132052",
-    src_sha1 = "41b5752f3aa4c77f872649c215142aee1d6a3395",
+    sha1 = "7a162c537a99bbaf35a074fec9a50815e6c81d9d",
 )
 
 maven_jar(
     name = "jetty-util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VER,
-    sha1 = "996ebc69825af41d49e2edc6796eb714acdc3369",
-    src_sha1 = "fe3b4ecf5a176bfd3c0055e5e1490503d90965e8",
+    sha1 = "e262e505363e5925df15618622d9888aefc1b0d0",
 )
 
 maven_jar(
     name = "jetty-util-ajax",
     artifact = "org.eclipse.jetty:jetty-util-ajax:" + JETTY_VER,
-    sha1 = "634f67e811e0ad2acb32feaaf409927d80cd69ae",
-    src_sha1 = "192f1e1254f659af64c6cd1124807fa12bdfa721",
+    sha1 = "60225034131e3f771b40bc75c15bd9cc4952302b",
 )
 
-BOUNCYCASTLE_VER = "1.77"
+BOUNCYCASTLE_VER = "1.80"
 
 maven_jar(
     name = "bcpg",
     artifact = "org.bouncycastle:bcpg-jdk18on:" + BOUNCYCASTLE_VER,
-    sha1 = "bb0be51e8b378baae6e0d86f5282cd3887066539",
-    src_sha1 = "33ff3269cede7165dac44033a3b150cc9f9f11cf",
+    sha1 = "163889a825393854dbe7dc52f1a8667e715e9859",
 )
 
 maven_jar(
     name = "bcprov",
     artifact = "org.bouncycastle:bcprov-jdk18on:" + BOUNCYCASTLE_VER,
-    sha1 = "2cc971b6c20949c1ff98d1a4bc741ee848a09523",
-    src_sha1 = "14ea9a3d759261358c6a1f59490ded125b5273a6",
+    sha1 = "e22100b41042decf09cab914a5af8d2c57b5ac4a",
 )
 
 maven_jar(
     name = "bcutil",
     artifact = "org.bouncycastle:bcutil-jdk18on:" + BOUNCYCASTLE_VER,
-    sha1 = "de3eaef351545fe8562cf29ddff4a403a45b49b7",
-    src_sha1 = "6f8f56ab009e7a3204817a0d45ed9638f5e30116",
+    sha1 = "b95726d1d49a0c65010c59a3e6640311d951bfd1",
 )
 
 maven_jar(
     name = "bcpkix",
     artifact = "org.bouncycastle:bcpkix-jdk18on:" + BOUNCYCASTLE_VER,
-    sha1 = "ed953791ba0229747dd0fd9911e3d76a462acfd3",
-    src_sha1 = "fdff397d5de0306db014f0a17e91717150db2768",
+    sha1 = "5277dfaaef2e92ce1d802499599a0ca7488f86e6",
 )
diff --git a/lib/BUILD b/lib/BUILD
index c2a8271..d236b3a 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -20,6 +20,16 @@
 )
 
 java_library(
+    name = "commons-lang3",
+    visibility = [
+        "//org.eclipse.jgit.archive:__pkg__",
+        "//org.eclipse.jgit.pgm.test:__pkg__",
+        "//org.eclipse.jgit.test:__pkg__",
+    ],
+    exports = ["@commons-lang3//jar"],
+)
+
+java_library(
     name = "commons-io",
     visibility = [
         "//org.eclipse.jgit.archive:__pkg__",
@@ -45,16 +55,6 @@
 )
 
 java_library(
-    name = "eddsa",
-    visibility = [
-        "//org.eclipse.jgit.ssh.apache:__pkg__",
-        "//org.eclipse.jgit.ssh.apache.test:__pkg__",
-        "//org.eclipse.jgit.ssh.jsch.test:__pkg__",
-    ],
-    exports = ["@eddsa//jar"],
-)
-
-java_library(
     name = "gson",
     visibility = [
         "//org.eclipse.jgit.lfs:__pkg__",
@@ -208,6 +208,8 @@
     visibility = [
         "//org.eclipse.jgit.gpg.bc:__pkg__",
         "//org.eclipse.jgit.gpg.bc.test:__pkg__",
+        "//org.eclipse.jgit.ssh.apache.test:__pkg__",
+        "//org.eclipse.jgit.ssh.jsch.test:__pkg__",
         "//org.eclipse.jgit.test:__pkg__",
     ],
     exports = ["@bcprov//jar"],
@@ -218,6 +220,8 @@
     visibility = [
         "//org.eclipse.jgit.gpg.bc:__pkg__",
         "//org.eclipse.jgit.gpg.bc.test:__pkg__",
+        "//org.eclipse.jgit.ssh.apache.test:__pkg__",
+        "//org.eclipse.jgit.ssh.jsch.test:__pkg__",
         "//org.eclipse.jgit.test:__pkg__",
     ],
     exports = ["@bcutil//jar"],
@@ -227,6 +231,8 @@
     name = "bcpkix",
     visibility = [
         "//org.eclipse.jgit.gpg.bc:__pkg__",
+        "//org.eclipse.jgit.ssh.apache.test:__pkg__",
+        "//org.eclipse.jgit.ssh.jsch.test:__pkg__",
         "//org.eclipse.jgit.test:__pkg__",
     ],
     exports = ["@bcpkix//jar"],
diff --git a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
index 7c079a5..fa9a416 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: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: org.apache.tools.ant,
- org.eclipse.jgit.ant.tasks;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.ant.tasks;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)"
diff --git a/org.eclipse.jgit.ant.test/pom.xml b/org.eclipse.jgit.ant.test/pom.xml
index be954bd..4a54d87 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 b6b52e5..480d88e 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: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: org.apache.tools.ant,
-  org.eclipse.jgit.storage.file;version="[7.0.0,7.1.0)"
+  org.eclipse.jgit.storage.file;version="[7.3.0,7.4.0)"
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.ant;version="7.0.0",
- org.eclipse.jgit.ant.tasks;version="7.0.0";
+Export-Package: org.eclipse.jgit.ant;version="7.3.0",
+ org.eclipse.jgit.ant.tasks;version="7.3.0";
   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 22f8aff..136b7cf 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ant;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ant;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.ant/pom.xml b/org.eclipse.jgit.ant/pom.xml
index 7c3edf4..4e85ee4 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ant</artifactId>
diff --git a/org.eclipse.jgit.archive/BUILD b/org.eclipse.jgit.archive/BUILD
index 3d7dbd2..d4f5340 100644
--- a/org.eclipse.jgit.archive/BUILD
+++ b/org.eclipse.jgit.archive/BUILD
@@ -12,6 +12,7 @@
     resources = glob(["resources/**"]),
     deps = [
         "//lib:commons-compress",
+        "//lib:commons-lang3",
         # We want these deps to be provided_deps
         "//org.eclipse.jgit:jgit",
     ],
diff --git a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
index e1a3671..fdfc635 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: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -13,17 +13,18 @@
  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="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
- org.osgi.framework;version="[1.3.0,2.0.0)"
+ org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
+ org.osgi.framework;version="[1.3.0,2.0.0)",
+ org.tukaani.xz
 Bundle-ActivationPolicy: lazy
 Bundle-Activator: org.eclipse.jgit.archive.FormatActivator
-Export-Package: org.eclipse.jgit.archive;version="7.0.0";
-  uses:="org.eclipse.jgit.lib,
+Export-Package: org.eclipse.jgit.archive;version="7.3.0";
+  uses:="org.apache.commons.compress.archivers,
+   org.osgi.framework,
    org.eclipse.jgit.api,
-   org.apache.commons.compress.archivers,
-   org.osgi.framework",
- org.eclipse.jgit.archive.internal;version="7.0.0";x-internal:=true
+   org.eclipse.jgit.lib",
+ org.eclipse.jgit.archive.internal;version="7.3.0";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 322f52f..716e8d0 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.archive;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.archive;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.archive/pom.xml b/org.eclipse.jgit.archive/pom.xml
index 054aa59..3935dfa 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.archive</artifactId>
@@ -41,6 +41,11 @@
     </dependency>
 
     <dependency>
+      <groupId>org.tukaani</groupId>
+      <artifactId>xz</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
       <version>${project.version}</version>
diff --git a/org.eclipse.jgit.benchmarks/pom.xml b/org.eclipse.jgit.benchmarks/pom.xml
index d8a616f..87d2bb3 100644
--- a/org.eclipse.jgit.benchmarks/pom.xml
+++ b/org.eclipse.jgit.benchmarks/pom.xml
@@ -16,7 +16,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.benchmarks</artifactId>
@@ -52,6 +52,10 @@
       <artifactId>org.eclipse.jgit.junit</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+    </dependency>
   </dependencies>
 
   <build>
@@ -79,7 +83,6 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
-        <version>${maven-compiler-plugin-version}</version>
         <configuration>
           <encoding>UTF-8</encoding>
           <release>${java.version}</release>
diff --git a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java
index 52a881b..44e862e 100644
--- a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java
+++ b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java
@@ -24,10 +24,12 @@
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.internal.storage.file.FileReftableDatabase;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.TrustStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
@@ -38,8 +40,10 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
+import org.junit.Assume;
 import org.openjdk.jmh.annotations.Benchmark;
 import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
 import org.openjdk.jmh.annotations.Measurement;
 import org.openjdk.jmh.annotations.Mode;
 import org.openjdk.jmh.annotations.OutputTimeUnit;
@@ -66,11 +70,14 @@ public static class BenchmarkState {
 		@Param({ "true", "false" })
 		boolean useRefTable;
 
-		@Param({ "100", "2500", "10000", "50000" })
+		@Param({ "true", "false" })
+		boolean autoRefresh;
+
+		@Param({ "100", "1000", "10000", "100000" })
 		int numBranches;
 
-		@Param({ "true", "false" })
-		boolean trustFolderStat;
+		@Param({ "ALWAYS", "AFTER_OPEN", "NEVER" })
+		TrustStat trustStat;
 
 		List<String> branches = new ArrayList<>(numBranches);
 
@@ -81,10 +88,13 @@ public static class BenchmarkState {
 		@Setup
 		@SuppressWarnings("boxing")
 		public void setupBenchmark() throws IOException, GitAPIException {
+			// if we use RefDirectory skip autoRefresh = false
+			Assume.assumeTrue(useRefTable || autoRefresh);
+
 			String firstBranch = "firstbranch";
 			testDir = Files.createDirectory(Paths.get("testrepos"));
-			String repoName = "branches-" + numBranches + "-trustFolderStat-"
-					+ trustFolderStat + "-" + refDatabaseType();
+			String repoName = "branches-" + numBranches + "-trustStat-"
+					+ trustStat + "-" + refDatabaseType();
 			Path workDir = testDir.resolve(repoName);
 			Path repoPath = workDir.resolve(".git");
 			Git git = Git.init().setDirectory(workDir.toFile()).call();
@@ -97,10 +107,13 @@ public void setupBenchmark() throws IOException, GitAPIException {
 				((FileRepository) git.getRepository()).convertRefStorage(
 						ConfigConstants.CONFIG_REF_STORAGE_REFTABLE, false,
 						false);
+				FileReftableDatabase refdb = (FileReftableDatabase) git
+						.getRepository().getRefDatabase();
+				refdb.setAutoRefresh(autoRefresh);
 			} else {
-				cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT,
-						trustFolderStat);
+				cfg.setEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
+						ConfigConstants.CONFIG_KEY_TRUST_STAT,
+						trustStat);
 			}
 			cfg.setInt(ConfigConstants.CONFIG_RECEIVE_SECTION, null,
 					"maxCommandBytes", Integer.MAX_VALUE);
@@ -112,7 +125,8 @@ public void setupBenchmark() throws IOException, GitAPIException {
 			System.out.println("Preparing test");
 			System.out.println("- repository: \t\t" + repoPath);
 			System.out.println("- refDatabase: \t\t" + refDatabaseType());
-			System.out.println("- trustFolderStat: \t" + trustFolderStat);
+			System.out.println("- autoRefresh: \t\t" + autoRefresh);
+			System.out.println("- trustStat: \t" + trustStat);
 			System.out.println("- branches: \t\t" + numBranches);
 
 			BatchRefUpdate u = repo.getRefDatabase().newBatchUpdate();
@@ -152,7 +166,8 @@ public void teardown() throws IOException {
 	@BenchmarkMode({ Mode.AverageTime })
 	@OutputTimeUnit(TimeUnit.MICROSECONDS)
 	@Warmup(iterations = 2, time = 100, timeUnit = TimeUnit.MILLISECONDS)
-	@Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(2)
 	public void testGetExactRef(Blackhole blackhole, BenchmarkState state)
 			throws IOException {
 		String branchName = state.branches
@@ -164,7 +179,8 @@ public void testGetExactRef(Blackhole blackhole, BenchmarkState state)
 	@BenchmarkMode({ Mode.AverageTime })
 	@OutputTimeUnit(TimeUnit.MICROSECONDS)
 	@Warmup(iterations = 2, time = 100, timeUnit = TimeUnit.MILLISECONDS)
-	@Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(2)
 	public void testGetRefsByPrefix(Blackhole blackhole, BenchmarkState state)
 			throws IOException {
 		String branchPrefix = "refs/heads/branch/" + branchIndex.nextInt(100)
diff --git a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/RawTextBenchmark.java b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/RawTextBenchmark.java
new file mode 100644
index 0000000..19297eb
--- /dev/null
+++ b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/RawTextBenchmark.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2022, Matthias Sohn <matthias.sohn@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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.benchmarks;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.eclipse.jgit.diff.RawText.getBufferSize;
+import static org.eclipse.jgit.diff.RawText.isBinary;
+import static org.eclipse.jgit.diff.RawText.isCrLfText;
+
+@State(Scope.Thread)
+public class RawTextBenchmark {
+
+	@State(Scope.Benchmark)
+	public static class BenchmarkState {
+
+		@Param({"1", "2", "3", "4", "5", "6"})
+		int testIndex;
+
+		@Param({"false", "true"})
+		boolean complete;
+
+		byte[] bytes;
+
+		@Setup
+		public void setupBenchmark() {
+			switch (testIndex) {
+				case 1: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					bytes = tmpBytes;
+					break;
+				}
+				case 2: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					byte[] tmpBytes2 = new byte[tmpBytes.length + 1];
+					System.arraycopy(tmpBytes, 0, tmpBytes2, 0, tmpBytes.length);
+					tmpBytes2[500] = '\0';
+					tmpBytes2[tmpBytes.length] = '\0';
+					bytes = tmpBytes2;
+					break;
+				}
+				case 3: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					byte[] tmpBytes2 = new byte[tmpBytes.length + 1];
+					System.arraycopy(tmpBytes, 0, tmpBytes2, 0, tmpBytes.length);
+					tmpBytes2[500] = '\r';
+					tmpBytes2[tmpBytes.length] = '\r';
+					bytes = tmpBytes2;
+					break;
+				}
+				case 4: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					byte[] tmpBytes2 = new byte[tmpBytes.length + 1];
+					System.arraycopy(tmpBytes, 0, tmpBytes2, 0, tmpBytes.length);
+					tmpBytes2[499] = '\r';
+					tmpBytes2[500] = '\n';
+					tmpBytes2[tmpBytes.length - 1] = '\r';
+					tmpBytes2[tmpBytes.length] = '\n';
+					bytes = tmpBytes2;
+					break;
+				}
+				case 5: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					tmpBytes[0] = '\0';
+					bytes = tmpBytes;
+					break;
+				}
+				case 6: {
+					byte[] tmpBytes = "a".repeat(102400).getBytes();
+					tmpBytes[0] = '\r';
+					bytes = tmpBytes;
+					break;
+				}
+				default:
+			}
+		}
+
+		@TearDown
+		public void teardown() {
+		}
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextOld(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfTextOld(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextNewCandidate1(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfTextNewCandidate1(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextNewCandidate2(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfTextNewCandidate2(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextNewCandidate3(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfTextNewCandidate3(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsCrLfTextNew(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isCrLfText(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsBinaryOld(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isBinaryOld(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+
+	@Benchmark
+	@BenchmarkMode({Mode.AverageTime})
+	@OutputTimeUnit(TimeUnit.NANOSECONDS)
+	@Warmup(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
+	@Fork(1)
+	public void testIsBinaryNew(Blackhole blackhole, BenchmarkState state) {
+		blackhole.consume(
+				isBinary(
+						state.bytes,
+						state.bytes.length,
+						state.complete
+				)
+		);
+	}
+
+
+	/**
+	 * Determine heuristically whether a byte array represents binary (as
+	 * opposed to text) content.
+	 *
+	 * @param raw
+	 *			the raw file content.
+	 * @param length
+	 *			number of bytes in {@code raw} to evaluate. This should be
+	 *			{@code raw.length} unless {@code raw} was over-allocated by
+	 *			the caller.
+	 * @param complete
+	 *			whether {@code raw} contains the whole data
+	 * @return true if raw is likely to be a binary file, false otherwise
+	 * @since 6.0
+	 */
+	public static boolean isBinaryOld(byte[] raw, int length, boolean complete) {
+		// Similar heuristic as C Git. Differences:
+		// - limited buffer size; may be only the beginning of a large blob
+		// - no counting of printable vs. non-printable bytes < 0x20 and 0x7F
+		int maxLength = getBufferSize();
+		boolean isComplete = complete;
+		if (length > maxLength) {
+			// We restrict the length in all cases to getBufferSize() to get
+			// predictable behavior. Sometimes we load streams, and sometimes we
+			// have the full data in memory. With streams, we never look at more
+			// than the first getBufferSize() bytes. If we looked at more when
+			// we have the full data, different code paths in JGit might come to
+			// different conclusions.
+			length = maxLength;
+			isComplete = false;
+		}
+		byte last = 'x'; // Just something inconspicuous.
+		for (int ptr = 0; ptr < length; ptr++) {
+			byte curr = raw[ptr];
+			if (isBinary(curr, last)) {
+				return true;
+			}
+			last = curr;
+		}
+		if (isComplete) {
+			// Buffer contains everything...
+			return last == '\r'; // ... so this must be a lone CR
+		}
+		return false;
+	}
+
+	/**
+	 * Determine heuristically whether a byte array represents text content
+	 * using CR-LF as line separator.
+	 *
+	 * @param raw	  the raw file content.
+	 * @param length   number of bytes in {@code raw} to evaluate.
+	 * @param complete whether {@code raw} contains the whole data
+	 * @return {@code true} if raw is likely to be CR-LF delimited text,
+	 * {@code false} otherwise
+	 * @since 6.0
+	 */
+	public static boolean isCrLfTextOld(byte[] raw, int length, boolean complete) {
+		boolean has_crlf = false;
+		byte last = 'x'; // Just something inconspicuous
+		for (int ptr = 0; ptr < length; ptr++) {
+			byte curr = raw[ptr];
+			if (isBinary(curr, last)) {
+				return false;
+			}
+			if (curr == '\n' && last == '\r') {
+				has_crlf = true;
+			}
+			last = curr;
+		}
+		if (last == '\r') {
+			if (complete) {
+				// Lone CR: it's binary after all.
+				return false;
+			}
+			// Tough call. If the next byte, which we don't have, would be a
+			// '\n', it'd be a CR-LF text, otherwise it'd be binary. Just decide
+			// based on what we already scanned; it wasn't binary until now.
+		}
+		return has_crlf;
+	}
+
+	/**
+	 * Determine heuristically whether a byte array represents text content
+	 * using CR-LF as line separator.
+	 *
+	 * @param raw
+	 *			the raw file content.
+	 * @param length
+	 *			number of bytes in {@code raw} to evaluate.
+	 * @return {@code true} if raw is likely to be CR-LF delimited text,
+	 *		 {@code false} otherwise
+	 * @param complete
+	 *			whether {@code raw} contains the whole data
+	 * @since 6.0
+	 */
+	public static boolean isCrLfTextNewCandidate1(byte[] raw, int length, boolean complete) {
+		boolean has_crlf = false;
+
+		// first detect empty
+		if (length <= 0) {
+			return false;
+		}
+
+		// next detect '\0'
+		for (int reversePtr = length - 1; reversePtr >= 0; --reversePtr) {
+			if (raw[reversePtr] == '\0') {
+				return false;
+			}
+		}
+
+		// if '\r' be last, then if complete then return non-crlf
+		if (raw[length - 1] == '\r' && complete) {
+			return false;
+		}
+
+		for (int ptr = 0; ptr < length - 1; ptr++) {
+			byte curr = raw[ptr];
+			if (curr == '\r') {
+				byte next = raw[ptr + 1];
+				if (next != '\n') {
+					return false;
+				}
+				// else
+				// we have crlf here
+				has_crlf = true;
+				// as next is '\n', it can never be '\r', just skip it from next check
+				++ptr;
+			}
+		}
+
+		return has_crlf;
+	}
+
+	/**
+	 * Determine heuristically whether a byte array represents text content
+	 * using CR-LF as line separator.
+	 *
+	 * @param raw
+	 *			the raw file content.
+	 * @param length
+	 *			number of bytes in {@code raw} to evaluate.
+	 * @return {@code true} if raw is likely to be CR-LF delimited text,
+	 *		 {@code false} otherwise
+	 * @param complete
+	 *			whether {@code raw} contains the whole data
+	 * @since 6.0
+	 */
+	public static boolean isCrLfTextNewCandidate2(byte[] raw, int length, boolean complete) {
+		boolean has_crlf = false;
+
+		// first detect empty
+		if (length <= 0) {
+			return false;
+		}
+
+		// if '\r' be last, then if complete then return non-crlf
+		byte last = raw[length - 1];
+		if (last == '\0' || last == '\r' && complete) {
+			return false;
+		}
+
+		for (int ptr = 0; ptr < length - 1; ptr++) {
+			byte b = raw[ptr];
+			switch (b) {
+				case  '\0':
+					return false;
+				case '\r': {
+					++ptr;
+					b = raw[ptr];
+					if (b != '\n') {
+						return false;
+					}
+					// else
+					// we have crlf here
+					has_crlf = true;
+					// as next is '\n', it can never be '\r', just skip it from next check
+					break;
+				}
+				default:
+					// do nothing;
+					break;
+			}
+		}
+
+		return has_crlf;
+	}
+
+	/**
+	 * Determine heuristically whether a byte array represents text content
+	 * using CR-LF as line separator.
+	 *
+	 * @param raw
+	 *			the raw file content.
+	 * @param length
+	 *			number of bytes in {@code raw} to evaluate.
+	 * @return {@code true} if raw is likely to be CR-LF delimited text,
+	 *		 {@code false} otherwise
+	 * @param complete
+	 *			whether {@code raw} contains the whole data
+	 * @since 6.0
+	 */
+	public static boolean isCrLfTextNewCandidate3(byte[] raw, int length, boolean complete) {
+		boolean has_crlf = false;
+
+		int ptr = -1;
+		byte current;
+		while (ptr < length - 2) {
+			current = raw[++ptr];
+			if ('\0' == current || '\r' == current && (raw[++ptr] != '\n' || !(has_crlf = true))) {
+				return false;
+			}
+		}
+
+		if (ptr == length - 2) {
+			// if '\r' be last, then if isComplete then return binary
+			current = raw[++ptr];
+			if('\0' == current || '\r' == current && complete){
+				return false;
+			}
+		}
+
+		return has_crlf;
+	}
+
+
+	public static void main(String[] args) throws RunnerException {
+		Options opt = new OptionsBuilder()
+				.include(RawTextBenchmark.class.getSimpleName())
+				.forks(1).jvmArgs("-ea").build();
+		new Runner(opt).run();
+	}
+}
diff --git a/org.eclipse.jgit.coverage/pom.xml b/org.eclipse.jgit.coverage/pom.xml
index 1fe5318..8cab250 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
@@ -27,88 +27,88 @@
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ant</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.archive</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.apache</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.server</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.server</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.pgm</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ui</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
 
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.test</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ant.test</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.http.test</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.pgm.test</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.test</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.lfs.server.test</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit.ssh.apache.test</artifactId>
-      <version>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-SNAPSHOT</version>
     </dependency>
   </dependencies>
 
diff --git a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
index 27510e2..287d75f 100644
--- a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF
@@ -3,19 +3,20 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.gpg.bc.test
 Bundle-SymbolicName: org.eclipse.jgit.gpg.bc.test
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Require-Bundle: org.hamcrest.core;bundle-version="[1.3.0,2.0.0)"
-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="[7.0.0,7.1.0)",
- org.eclipse.jgit.gpg.bc.internal.keys;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util.sha1;version="[7.0.0,7.1.0)",
+Import-Package: org.bouncycastle.asn1.cryptlib;version="[1.79.0,2.0.0)",
+ org.bouncycastle.jce.provider;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.util.encoders;version="[1.79.0,2.0.0)",
+ org.eclipse.jgit.gpg.bc.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.gpg.bc.internal.keys;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util.sha1;version="[7.3.0,7.4.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)"
diff --git a/org.eclipse.jgit.gpg.bc.test/pom.xml b/org.eclipse.jgit.gpg.bc.test/pom.xml
index d865b13..10aa742 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.gpg.bc.test</artifactId>
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
index fed0610..d486c97 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.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
@@ -9,10 +9,7 @@
  */
 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;
@@ -20,8 +17,6 @@
 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;
@@ -49,39 +44,15 @@ public static void ensureBC() {
 		}
 	}
 
-	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) {
+		TestData(String name, boolean encrypted) {
 			this.name = name;
 			this.encrypted = encrypted;
-			this.keyValue = keyValue;
 		}
 
 		@Override
@@ -93,19 +64,12 @@ public String toString() {
 	@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("62D43D7F117F7A5E4998ECB6617EE9942D069C14", 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);
-		}
+				new TestData("AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11", false),
+				new TestData("2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A", false),
+				new TestData("66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9", true),
+				new TestData("F727FAB884DA3BD402B6E0F5472E108D21033124", true),
+				new TestData("62D43D7F117F7A5E4998ECB6617EE9942D069C14", true),
+				new TestData("faked", false) };
 	}
 
 	private static PGPPublicKey readAsc(InputStream in)
@@ -131,11 +95,6 @@ private static PGPPublicKey readAsc(InputStream in)
 
 	@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) {
@@ -151,11 +110,6 @@ public void testKeyRead() throws Exception {
 									: null,
 							publicKey);
 					assertNotNull(secretKey);
-				} catch (PGPException e) {
-					// Currently we may not be able to load OCB-encrypted keys.
-					assertTrue(e.toString(), 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 9749ac1..f35e5e5 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
@@ -3,34 +3,28 @@
 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="[7.0.0,7.1.0)"
+Fragment-Host: org.eclipse.jgit;bundle-version="[7.3.0,7.4.0)"
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/gpg_bc
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.bouncycastle.asn1;version="[1.69.0,2.0.0)",
- org.bouncycastle.asn1.x9;version="[1.69.0,2.0.0)",
- org.bouncycastle.bcpg;version="[1.69.0,2.0.0)",
- org.bouncycastle.bcpg.sig;version="[1.69.0,2.0.0)",
- org.bouncycastle.crypto.ec;version="[1.69.0,2.0.0)",
- org.bouncycastle.gpg;version="[1.69.0,2.0.0)",
- org.bouncycastle.gpg.keybox;version="[1.69.0,2.0.0)",
- org.bouncycastle.gpg.keybox.jcajce;version="[1.69.0,2.0.0)",
- org.bouncycastle.jcajce.interfaces;version="[1.69.0,2.0.0)",
- org.bouncycastle.jcajce.util;version="[1.69.0,2.0.0)",
- org.bouncycastle.jce.provider;version="[1.69.0,2.0.0)",
- org.bouncycastle.math.ec;version="[1.69.0,2.0.0)",
- org.bouncycastle.math.field;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp.jcajce;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp.operator;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp.operator.jcajce;version="[1.69.0,2.0.0)",
- org.bouncycastle.util;version="[1.69.0,2.0.0)",
- org.bouncycastle.util.encoders;version="[1.69.0,2.0.0)",
- org.bouncycastle.util.io;version="[1.69.0,2.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
+Import-Package: org.bouncycastle.asn1;version="[1.79.0,2.0.0)",
+ org.bouncycastle.asn1.x9;version="[1.79.0,2.0.0)",
+ org.bouncycastle.bcpg;version="[1.79.0,2.0.0)",
+ org.bouncycastle.bcpg.sig;version="[1.79.0,2.0.0)",
+ org.bouncycastle.crypto.ec;version="[1.79.0,2.0.0)",
+ org.bouncycastle.gpg;version="[1.79.0,2.0.0)",
+ org.bouncycastle.gpg.keybox;version="[1.79.0,2.0.0)",
+ org.bouncycastle.gpg.keybox.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.jcajce.interfaces;version="[1.79.0,2.0.0)",
+ org.bouncycastle.jcajce.util;version="[1.79.0,2.0.0)",
+ org.bouncycastle.math.ec;version="[1.79.0,2.0.0)",
+ org.bouncycastle.math.field;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.util.encoders;version="[1.79.0,2.0.0)",
  org.slf4j;version="[1.7.0,3.0.0)"
-Export-Package: org.eclipse.jgit.gpg.bc;version="7.0.0",
- org.eclipse.jgit.gpg.bc.internal;version="7.0.0";x-friends:="org.eclipse.jgit.gpg.bc.test",
- org.eclipse.jgit.gpg.bc.internal.keys;version="7.0.0";x-friends:="org.eclipse.jgit.gpg.bc.test"
+Export-Package: org.eclipse.jgit.gpg.bc.internal;version="7.3.0";x-friends:="org.eclipse.jgit.gpg.bc.test",
+ org.eclipse.jgit.gpg.bc.internal.keys;version="7.3.0";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 0733a68..5134716 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.gpg.bc;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.gpg.bc;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.gpg.bc/about.html b/org.eclipse.jgit.gpg.bc/about.html
index fc527d5..92b9409 100644
--- a/org.eclipse.jgit.gpg.bc/about.html
+++ b/org.eclipse.jgit.gpg.bc/about.html
@@ -58,32 +58,6 @@
 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.</p>
 
-<hr>
-<p><b>org.eclipse.jgit.gpg.bc.internal.keys.SExprParser - MIT</b></p>
-
-<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>
-<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>
 
 </html>
diff --git a/org.eclipse.jgit.gpg.bc/pom.xml b/org.eclipse.jgit.gpg.bc/pom.xml
index f4dce68..6159129 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.gpg.bc</artifactId>
@@ -160,7 +160,7 @@
                   <breakBuildOnBinaryIncompatibleModifications>false</breakBuildOnBinaryIncompatibleModifications>
                   <onlyBinaryIncompatible>false</onlyBinaryIncompatible>
                   <includeSynthetic>false</includeSynthetic>
-                  <ignoreMissingClasses>false</ignoreMissingClasses>
+                  <ignoreMissingClasses>true</ignoreMissingClasses>
                   <skipPomModules>true</skipPomModules>
               </parameter>
               <skip>false</skip>
diff --git a/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSigner b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSigner
deleted file mode 100644
index 6752b64..0000000
--- a/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSigner
+++ /dev/null
@@ -1 +0,0 @@
-org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSigner
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.SignatureVerifierFactory
similarity index 100%
rename from org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory
rename to org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory
diff --git a/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory
new file mode 100644
index 0000000..c0b214d
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory
@@ -0,0 +1 @@
+org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSignerFactory
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 77ca2cd..9e7f98c 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,7 +1,5 @@
 corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0}
 credentialPassphrase=Passphrase
-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
@@ -9,22 +7,14 @@
 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
+keyAlgorithmMismatch=Secret key has a different algorithm than the public key
+keyMismatch=Secret key does not match public key; public key is {0} {1} while secret key is for {2} {3}
 logWarnGnuPGHome=Cannot access GPG home directory given by environment variable GNUPGHOME={}
 logWarnGpgHomeProperty=Cannot access GPG home directory given by Java system property jgit.gpg.home={}
 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
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
deleted file mode 100644
index fdd1a2b..0000000
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 705e195..fcae7c2 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,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2021 Salesforce and others
+ * Copyright (C) 2018, 2024 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
@@ -30,8 +30,6 @@ public static BCText get() {
 	// @formatter:off
 	/***/ public String corrupt25519Key;
 	/***/ public String credentialPassphrase;
-	/***/ public String cryptCipherError;
-	/***/ public String cryptWrongDecryptedLength;
 	/***/ public String gpgFailedToParseSecretKey;
 	/***/ public String gpgNoCredentialsProvider;
 	/***/ public String gpgNoKeygrip;
@@ -39,22 +37,14 @@ public static BCText get() {
 	/***/ public String gpgNoKeyInLegacySecring;
 	/***/ public String gpgNoPublicKeyFound;
 	/***/ public String gpgNoSecretKeyForPublicKey;
-	/***/ public String gpgNoSuchAlgorithm;
 	/***/ public String gpgNotASigningKey;
 	/***/ public String gpgKeyInfo;
 	/***/ public String gpgSigningCancelled;
+	/***/ public String keyAlgorithmMismatch;
+	/***/ public String keyMismatch;
 	/***/ public String logWarnGnuPGHome;
 	/***/ public String logWarnGpgHomeProperty;
 	/***/ 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;
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgPublicKey.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgPublicKey.java
index d736536..9ec5b45 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgPublicKey.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgPublicKey.java
@@ -1,3 +1,12 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.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.gpg.bc.internal;
 
 import java.util.List;
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
index 3378bb3..5a3d43b 100644
--- 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
@@ -12,7 +12,6 @@
 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.Date;
@@ -20,7 +19,6 @@
 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;
@@ -31,33 +29,20 @@
 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.api.errors.JGitInternalException;
-import org.eclipse.jgit.lib.AbstractGpgSignatureVerifier;
 import org.eclipse.jgit.lib.GpgConfig;
-import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SignatureVerifier;
 import org.eclipse.jgit.util.LRUMap;
 import org.eclipse.jgit.util.StringUtils;
 
 /**
- * A {@link GpgSignatureVerifier} to verify GPG signatures using BouncyCastle.
+ * A {@link SignatureVerifier} to verify GPG signatures using BouncyCastle.
  */
 public class BouncyCastleGpgSignatureVerifier
-		extends AbstractGpgSignatureVerifier {
+		implements SignatureVerifier {
 
-	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();
-	}
+	private static final String NAME = "bc"; //$NON-NLS-1$
 
 	// To support more efficient signature verification of multiple objects we
 	// cache public keys once found in a LRU cache.
@@ -70,7 +55,7 @@ public BouncyCastleGpgSignatureVerifier() {
 
 	@Override
 	public String getName() {
-		return "bc"; //$NON-NLS-1$
+		return NAME;
 	}
 
 	static PGPSignature parseSignature(InputStream in)
@@ -90,9 +75,8 @@ static PGPSignature parseSignature(InputStream in)
 	}
 
 	@Override
-	public SignatureVerification verify(@NonNull GpgConfig config, byte[] data,
-			byte[] signatureData)
-			throws IOException {
+	public SignatureVerification verify(Repository repository, GpgConfig config,
+			byte[] data, byte[] signatureData) throws IOException {
 		PGPSignature signature = null;
 		String fingerprint = null;
 		String signer = null;
@@ -127,14 +111,15 @@ public SignatureVerification verify(@NonNull GpgConfig config, byte[] data,
 		}
 		Date signatureCreatedAt = signature.getCreationTime();
 		if (fingerprint == null && signer == null && keyId == null) {
-			return new VerificationResult(signatureCreatedAt, null, null, null,
-					false, false, TrustLevel.UNKNOWN,
+			return new SignatureVerification(NAME, 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,
-					signer, false, false, TrustLevel.UNKNOWN,
+			return new SignatureVerification(NAME, signatureCreatedAt,
+					signer, fingerprint, signer, false, false,
+					TrustLevel.UNKNOWN,
 					MessageFormat.format(BCText.get().signatureInconsistent,
 							keyId, fingerprint));
 		}
@@ -175,15 +160,16 @@ public SignatureVerification verify(@NonNull GpgConfig config, byte[] data,
 					bySigner.put(signer, NO_KEY);
 				}
 			}
-			return new VerificationResult(signatureCreatedAt, signer,
-					fingerprint, signer, false, false, TrustLevel.UNKNOWN,
-					BCText.get().signatureNoPublicKey);
+			return new SignatureVerification(NAME, signatureCreatedAt,
+					signer, fingerprint, signer, false, false,
+					TrustLevel.UNKNOWN, BCText.get().signatureNoPublicKey);
 		}
 		if (fingerprint != null && !publicKey.isExactMatch()) {
 			// We did find _some_ signing key for the signer, but it doesn't
 			// match the given fingerprint.
-			return new VerificationResult(signatureCreatedAt, signer,
-					fingerprint, signer, false, false, TrustLevel.UNKNOWN,
+			return new SignatureVerification(NAME, signatureCreatedAt,
+					signer, fingerprint, signer, false, false,
+					TrustLevel.UNKNOWN,
 					MessageFormat.format(BCText.get().signatureNoSigningKey,
 							fingerprint));
 		}
@@ -229,8 +215,7 @@ public SignatureVerification verify(@NonNull GpgConfig config, byte[] data,
 		boolean verified = false;
 		try {
 			signature.init(
-					new JcaPGPContentVerifierBuilderProvider()
-							.setProvider(BouncyCastleProvider.PROVIDER_NAME),
+					new JcaPGPContentVerifierBuilderProvider(),
 					pubKey);
 			signature.update(data);
 			verified = signature.verify();
@@ -238,15 +223,8 @@ public SignatureVerification verify(@NonNull GpgConfig config, byte[] data,
 			throw new JGitInternalException(
 					BCText.get().signatureVerificationError, e);
 		}
-		return new VerificationResult(signatureCreatedAt, signer, fingerprint, user,
-				verified, expired, trust, null);
-	}
-
-	@Override
-	public SignatureVerification verify(byte[] data, byte[] signatureData)
-			throws IOException {
-		throw new UnsupportedOperationException(
-				"Call verify(GpgConfig, byte[], byte[]) instead."); //$NON-NLS-1$
+		return new SignatureVerification(NAME, signatureCreatedAt, signer,
+				fingerprint, user, verified, expired, trust, null);
 	}
 
 	private TrustLevel parseGpgTrustPacket(byte[] packet) {
@@ -282,76 +260,4 @@ 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
index ae82b75..566ad1b 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.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
@@ -9,20 +9,27 @@
  */
 package org.eclipse.jgit.gpg.bc.internal;
 
-import org.eclipse.jgit.lib.GpgSignatureVerifier;
-import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.lib.SignatureVerifierFactory;
 
 /**
- * A {@link GpgSignatureVerifierFactory} that creates
- * {@link GpgSignatureVerifier} instances that verify GPG signatures using
- * BouncyCastle and that do cache public keys.
+ * A {@link SignatureVerifierFactory} that creates {@link SignatureVerifier}
+ * instances that verify GPG signatures using BouncyCastle and that do cache
+ * public keys.
  */
 public final class BouncyCastleGpgSignatureVerifierFactory
-		extends GpgSignatureVerifierFactory {
+		implements SignatureVerifierFactory {
 
 	@Override
-	public GpgSignatureVerifier getVerifier() {
+	public GpgFormat getType() {
+		return GpgFormat.OPENPGP;
+	}
+
+	@Override
+	public SignatureVerifier create() {
 		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 763b7f7..adac9b1 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, 2021, Salesforce and others
+ * Copyright (C) 2018, 2024, 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,13 +14,11 @@
 import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
-import java.security.Security;
 import java.util.Iterator;
 
 import org.bouncycastle.bcpg.ArmoredOutputStream;
 import org.bouncycastle.bcpg.BCPGOutputStream;
 import org.bouncycastle.bcpg.HashAlgorithmTags;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.bouncycastle.openpgp.PGPException;
 import org.bouncycastle.openpgp.PGPPrivateKey;
 import org.bouncycastle.openpgp.PGPPublicKey;
@@ -30,79 +28,23 @@
 import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
 import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
 import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
-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.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.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.util.StringUtils;
 
 /**
  * GPG Signer using the BouncyCastle library.
  */
-public class BouncyCastleGpgSigner extends GpgSigner
-		implements GpgObjectSigner {
-
-	private static void registerBouncyCastleProviderIfNecessary() {
-		if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
-			Security.addProvider(new BouncyCastleProvider());
-		}
-	}
-
-	/**
-	 * Create a new instance.
-	 * <p>
-	 * The BounceCastleProvider will be registered if necessary.
-	 * </p>
-	 */
-	public BouncyCastleGpgSigner() {
-		registerBouncyCastleProviderIfNecessary();
-	}
-
-	@Override
-	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 (CanceledException e) {
-			throw e;
-		} catch (Exception e) {
-			return false;
-		}
-	}
+public class BouncyCastleGpgSigner implements Signer {
 
 	private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey,
 			PersonIdent committer,
@@ -121,38 +63,24 @@ private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey,
 	}
 
 	@Override
-	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);
+	public GpgSignature sign(Repository repository, GpgConfig config,
+			byte[] data, PersonIdent committer, String signingKey,
+			CredentialsProvider credentialsProvider) throws CanceledException,
+			IOException, UnsupportedSigningFormatException {
+		String gpgSigningKey = signingKey;
+		if (gpgSigningKey == null) {
+			gpgSigningKey = config.getSigningKey();
 		}
 		try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
 				credentialsProvider)) {
 			BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
-					committer,
-						passphrasePrompt);
+					committer, passphrasePrompt);
 			PGPSecretKey secretKey = gpgKey.getSecretKey();
 			if (secretKey == null) {
 				throw new JGitInternalException(
 						BCText.get().unableToSignCommitNoSecretKey);
 			}
-			JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder()
-					.setProvider(BouncyCastleProvider.PROVIDER_NAME);
+			JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder();
 			PGPPrivateKey privateKey = null;
 			if (!passphrasePrompt.hasPassphrase()) {
 				// Either the key is not encrypted, or it was read from the
@@ -177,8 +105,8 @@ public void signObject(@NonNull ObjectBuilder object,
 			PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
 					new JcaPGPContentSignerBuilder(
 							publicKey.getAlgorithm(),
-							HashAlgorithmTags.SHA256).setProvider(
-									BouncyCastleProvider.PROVIDER_NAME));
+							HashAlgorithmTags.SHA256),
+					publicKey);
 			signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
 			PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator();
 			subpackets.setIssuerFingerprint(false, publicKey);
@@ -202,16 +130,36 @@ public void signObject(@NonNull ObjectBuilder object,
 			ByteArrayOutputStream buffer = new ByteArrayOutputStream();
 			try (BCPGOutputStream out = new BCPGOutputStream(
 					new ArmoredOutputStream(buffer))) {
-				signatureGenerator.update(object.build());
+				signatureGenerator.update(data);
 				signatureGenerator.generate().encode(out);
 			}
-			object.setGpgSignature(new GpgSignature(buffer.toByteArray()));
-		} catch (PGPException | IOException | NoSuchAlgorithmException
+			return new GpgSignature(buffer.toByteArray());
+		} catch (PGPException | NoSuchAlgorithmException
 				| NoSuchProviderException | URISyntaxException e) {
 			throw new JGitInternalException(e.getMessage(), e);
 		}
 	}
 
+	@Override
+	public boolean canLocateSigningKey(Repository repository, GpgConfig config,
+			PersonIdent committer, String signingKey,
+			CredentialsProvider credentialsProvider) throws CanceledException {
+		String gpgSigningKey = signingKey;
+		if (gpgSigningKey == null) {
+			gpgSigningKey = config.getSigningKey();
+		}
+		try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
+				credentialsProvider)) {
+			BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
+					committer, passphrasePrompt);
+			return gpgKey != null;
+		} catch (CanceledException e) {
+			throw e;
+		} catch (Exception e) {
+			return false;
+		}
+	}
+
 	static String extractSignerId(String pgpUserId) {
 		int from = pgpUserId.indexOf('<');
 		if (from >= 0) {
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignerFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignerFactory.java
new file mode 100644
index 0000000..92ab65d
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignerFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.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.gpg.bc.internal;
+
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.lib.SignerFactory;
+
+/**
+ * Factory for creating a {@link Signer} for OPENPGP signatures based on Bouncy
+ * Castle.
+ */
+public final class BouncyCastleGpgSignerFactory implements SignerFactory {
+
+	@Override
+	public GpgFormat getType() {
+		return GpgFormat.OPENPGP;
+	}
+
+	@Override
+	public Signer create() {
+		return new BouncyCastleGpgSigner();
+	}
+}
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
deleted file mode 100644
index 3924d68..0000000
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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 {
-					// errorprone: "Dynamically constructed transformation
-					// strings are also flagged, as they may conceal an instance
-					// of ECB mode."
-					@SuppressWarnings("InsecureCryptoUsage")
-					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
deleted file mode 100644
index fd030ee..0000000
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
+++ /dev/null
@@ -1,859 +0,0 @@
-/*
- * 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.ASN1ObjectIdentifier;
-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)</li>
- * <li>handle secret keys using AES/OCB as encryption (those don't have a
- * hash)</li>
- * <li>fix EC parsing to account for "flags" sub-list present for ed25519 and
- * curve25519</li>
- * <li>add support for ed25519 OIDs unknown to BouncyCastle</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
-	 *             if an IO error occurred
-	 * @throws PGPException
-	 *             if some PGP error occurred
-	 */
-	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());
-				// JGit: c.f. https://github.com/bcgit/bc-java/issues/1590.
-				// There may be a flags sub-list here for ed25519 or curve25519.
-				if (type.equals("flags")) {
-					SXprUtils.readString(inputStream, inputStream.read());
-					SXprUtils.skipCloseParenthesis(inputStream);
-					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());
-				}
-
-				// JGit: BC doesn't know Ed25519 curve name.
-				ASN1ObjectIdentifier curveOid = ECNamedCurveTable
-						.getOID(curveName);
-				if (curveOid == null) {
-					curveOid = ObjectIds.getByName(curveName);
-				}
-				ECPublicBCPGKey basePubKey = new ECDSAPublicBCPGKey(
-						curveOid,
-						new BigInteger(1, qVal));
-				ECPublicBCPGKey assocPubKey = (ECPublicBCPGKey) pubKey
-						.getPublicKeyPacket().getKey();
-				if (!ObjectIds.match(basePubKey.getCurveOID(),
-						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
-	 *             if an IO error occurred
-	 * @throws PGPException
-	 *             if a PGP error occurred
-	 */
-	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());
-				// JGit: c.f. https://github.com/bcgit/bc-java/issues/1590.
-				// There may be a flags sub-list here for ed25519 or curve25519.
-				if (type.equals("flags")) {
-					SXprUtils.readString(inputStream, inputStream.read());
-					SXprUtils.skipCloseParenthesis(inputStream);
-					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
deleted file mode 100644
index 220aa28..0000000
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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
index a659d38..a56e418 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.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
@@ -9,34 +9,36 @@
  */
 package org.eclipse.jgit.gpg.bc.internal.keys;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
+import java.io.BufferedInputStream;
 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.bcpg.ECPublicBCPGKey;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
+import org.bouncycastle.gpg.PGPSecretKeyParser;
+import org.bouncycastle.gpg.SExprParser;
+import org.bouncycastle.openpgp.OpenedPGPKeyData;
 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.JcaKeyFingerprintCalculator;
 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 {
 
+	// Maximum nesting depth of sub-lists in an S-Expression for a secret key.
+	private static final int MAX_SEXPR_NESTING = 20;
+
 	private SecretKeys() {
 		// No instantiation.
 	}
@@ -64,12 +66,6 @@ public interface PassphraseSupplier {
 				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.
 	 *
@@ -99,500 +95,59 @@ public static PGPSecretKey readSecretKey(InputStream in,
 			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)));
+		OpenedPGPKeyData data;
+		try (InputStream keyIn = new BufferedInputStream(in)) {
+			data = PGPSecretKeyParser.parse(keyIn, MAX_SEXPR_NESTING);
 		}
-		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);
+		PBEProtectionRemoverFactory decryptor = null;
+		if (isProtected(data)) {
+			decryptor = new JcePBEProtectionRemoverFactory(
+					passphraseSupplier.getPassphrase(), calculatorProvider);
 		}
-	}
-
-	/**
-	 * 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;
+		switch (publicKey.getAlgorithm()) {
+		case PublicKeyAlgorithmTags.EDDSA_LEGACY:
+		case PublicKeyAlgorithmTags.Ed25519:
+			// If we let Bouncy Castle check whether the secret key matches the
+			// given public key it may get into trouble in some cases with
+			// ed25519 keys. It appears that we may end up with secret keys
+			// using the official RFC 8410 OID for ed25519, "1.3.101.112", while
+			// the public key passed in may have a non-standard OpenPGP-specific
+			// OID "1.3.6.1.4.1.11591.15.1", or vice versa. Bouncy Castle then
+			// throws an exception because of the different OIDs.
+			//
+			// The work-around is to just read the secret key, and double-check
+			// later that the OIDs are compatible and the curve points match.
+			PGPSecretKey secret = data.getKeyData(null, calculatorProvider,
+					decryptor, new JcaKeyFingerprintCalculator(),
+					MAX_SEXPR_NESTING);
+			PGPPublicKey pubKeyRead = secret.getPublicKey();
+			int algoRead = pubKeyRead.getAlgorithm();
+			if (algoRead != PublicKeyAlgorithmTags.EDDSA_LEGACY
+					&& algoRead != PublicKeyAlgorithmTags.Ed25519) {
+				throw new PGPException(BCText.get().keyAlgorithmMismatch);
 			}
-			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)));
-				}
+			ECPublicBCPGKey ec1 = (ECPublicBCPGKey) publicKey
+					.getPublicKeyPacket().getKey();
+			ECPublicBCPGKey ec2 = (ECPublicBCPGKey) pubKeyRead
+					.getPublicKeyPacket().getKey();
+			if (!ObjectIds.match(ec1.getCurveOID(), ec2.getCurveOID())
+					|| !ec1.getEncodedPoint().equals(ec2.getEncodedPoint())) {
+				throw new PGPException(
+						MessageFormat.format(BCText.get().keyMismatch,
+								ec1.getCurveOID(), ec1.getEncodedPoint(),
+								ec2.getCurveOID(), ec2.getEncodedPoint()));
 			}
-			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
-	 *             if object stream is corrupt
-	 */
-	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;
+			return secret;
 		default:
-			if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
-					|| (ch >= '0' && ch <= '9')) {
-				return true;
-			}
-			return false;
+			// For other key types let Bouncy Castle do the check.
+			return data.getKeyData(publicKey, calculatorProvider, decryptor,
+					null, MAX_SEXPR_NESTING);
 		}
 	}
 
-	private static boolean isHex(int ch) {
-		return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')
-				|| (ch >= 'a' && ch <= 'f');
+	private static boolean isProtected(OpenedPGPKeyData data) {
+		return SExprParser.ProtectionFormatTypeTags.PROTECTED_PRIVATE_KEY == SExprParser
+				.getProtectionType(data.getKeyExpression().getString(0));
 	}
 
-	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 c1ec7ae..244b20a 100644
--- a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.http.apache
 Bundle-SymbolicName: org.eclipse.jgit.http.apache
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
@@ -26,11 +26,11 @@
  org.apache.http.impl.conn;version="[4.4.0,5.0.0)",
  org.apache.http.params;version="[4.3.0,5.0.0)",
  org.apache.http.ssl;version="[4.3.0,5.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)"
-Export-Package: org.eclipse.jgit.transport.http.apache;version="7.0.0";
+ org.eclipse.jgit.annotations;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.http;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)"
+Export-Package: org.eclipse.jgit.transport.http.apache;version="7.3.0";
   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 d29d387..86adeb6 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.http.apache;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.http.apache;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.http.apache/pom.xml b/org.eclipse.jgit.http.apache/pom.xml
index 51a79d6..0c7c5ff 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.apache</artifactId>
diff --git a/org.eclipse.jgit.http.server/BUILD b/org.eclipse.jgit.http.server/BUILD
index f8aa44d..a0dae48 100644
--- a/org.eclipse.jgit.http.server/BUILD
+++ b/org.eclipse.jgit.http.server/BUILD
@@ -2,11 +2,16 @@
 
 package(default_visibility = ["//visibility:public"])
 
+filegroup(
+    name = "jgit-servlet-resources",
+    srcs = glob(["resources/**"]),
+)
+
 java_library(
     name = "jgit-servlet",
     srcs = glob(["src/**/*.java"]),
     resource_strip_prefix = "org.eclipse.jgit.http.server/resources",
-    resources = glob(["resources/**"]),
+    resources = [":jgit-servlet-resources"],
     deps = [
         "//lib:servlet-api",
         # We want these deps to be provided_deps
diff --git a/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
index 124129d..71c471d 100644
--- a/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
@@ -3,14 +3,14 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.http.server
 Bundle-SymbolicName: org.eclipse.jgit.http.server
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.http.server;version="7.0.0",
- org.eclipse.jgit.http.server.glue;version="7.0.0";
+Export-Package: org.eclipse.jgit.http.server;version="7.3.0",
+ org.eclipse.jgit.http.server.glue;version="7.3.0";
   uses:="jakarta.servlet,
   	jakarta.servlet.http",
- org.eclipse.jgit.http.server.resolver;version="7.0.0";
+ org.eclipse.jgit.http.server.resolver;version="7.3.0";
   uses:="jakarta.servlet.http
    org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.lib,
@@ -19,14 +19,14 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: jakarta.servlet;version="[6.0.0,7.0.0)",
  jakarta.servlet.http;version="[6.0.0,7.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.transport.parser;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)"
+ org.eclipse.jgit.annotations;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.transport.parser;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.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 bbb29cd..67a14cf 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.http.server;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.http.server;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.http.server/pom.xml b/org.eclipse.jgit.http.server/pom.xml
index 7645dd1..b34ca2f 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.server</artifactId>
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java
index d2f0cd2..bf3da4b 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java
@@ -255,9 +255,11 @@ private static void writePacket(PacketLineOut pckOut, String textForGit)
 	private static void send(HttpServletRequest req, HttpServletResponse res,
 			String type, byte[] buf, int httpStatus) throws IOException {
 		ServletUtils.consumeRequestBody(req);
-		res.setStatus(httpStatus);
-		res.setContentType(type);
-		res.setContentLength(buf.length);
+		if (!res.isCommitted()) {
+			res.setStatus(httpStatus);
+			res.setContentType(type);
+			res.setContentLength(buf.length);
+		}
 		try (OutputStream os = res.getOutputStream()) {
 			os.write(buf);
 		}
diff --git a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
index edce2d8..518afb3 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: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -29,26 +29,26 @@
  org.eclipse.jetty.util.component;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.security;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.thread;version="[12.0.0,13.0.0)",
- org.eclipse.jgit.api;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.http.server;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.http.server.glue;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.http.server.resolver;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit.http;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.http.apache;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.http.server;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.http.server.glue;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.http.server.resolver;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit.http;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.http;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.http.apache;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.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)",
diff --git a/org.eclipse.jgit.http.test/pom.xml b/org.eclipse.jgit.http.test/pom.xml
index 01ee782..8ed3017 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.test</artifactId>
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 850e895..b0d17ad 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
@@ -1728,7 +1728,8 @@ public void testPush_CreateBranch() throws Exception {
 		assertEquals(Q, remoteRepository.exactRef(dstName).getObjectId());
 		fsck(remoteRepository, Q);
 
-		final ReflogReader log = remoteRepository.getReflogReader(dstName);
+		final ReflogReader log = remoteRepository.getRefDatabase()
+				.getReflogReader(dstName);
 		assertNotNull("has log for " + dstName, log);
 
 		final ReflogEntry last = log.getLastEntry();
diff --git a/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
index 3e98351..e3a6e26 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: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
@@ -22,17 +22,17 @@
  org.eclipse.jetty.util.component;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.security;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.ssl;version="[12.0.0,13.0.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.http.server;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.http.server;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.3.0,7.4.0)",
  org.junit;version="[4.13,5.0.0)",
  org.slf4j.helpers;version="[1.7.0,3.0.0)"
-Export-Package: org.eclipse.jgit.junit.http;version="7.0.0";
+Export-Package: org.eclipse.jgit.junit.http;version="7.3.0";
   uses:="org.eclipse.jgit.transport,
    jakarta.servlet,
    jakarta.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 cfeb426..855f210 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit.http;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit.http;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit.http/pom.xml b/org.eclipse.jgit.junit.http/pom.xml
index 67c8265..2947f21 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.http</artifactId>
diff --git a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
index 424bac1..30c359b 100644
--- a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF
@@ -3,46 +3,46 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.junit.ssh
 Bundle-SymbolicName: org.eclipse.jgit.junit.ssh
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.apache.sshd.common;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.config.keys;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.file.virtualfs;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.helpers;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.io;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.kex;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.keyprovider;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.session;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.signature;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.buffer;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.logging;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.security;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.threads;version="[2.12.0,2.13.0)",
- org.apache.sshd.core;version="[2.12.0,2.13.0)",
- org.apache.sshd.server;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.auth;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.auth.gss;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.auth.keyboard;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.auth.password;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.command;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.session;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.shell;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.subsystem;version="[2.12.0,2.13.0)",
- org.apache.sshd.sftp;version="[2.12.0,2.13.0)",
- org.apache.sshd.sftp.server;version="[2.12.0,2.13.0)",
- org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
+Import-Package: org.apache.sshd.common;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.config.keys;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.file.virtualfs;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.helpers;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.io;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.kex;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.keyprovider;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.session;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.signature;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.buffer;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.logging;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.security;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.threads;version="[2.15.0,2.16.0)",
+ org.apache.sshd.core;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.auth;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.auth.gss;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.auth.keyboard;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.auth.password;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.command;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.session;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.shell;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.subsystem;version="[2.15.0,2.16.0)",
+ org.apache.sshd.sftp;version="[2.15.0,2.16.0)",
+ org.apache.sshd.sftp.server;version="[2.15.0,2.16.0)",
+ org.eclipse.jgit.annotations;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.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,3.0.0)"
-Export-Package: org.eclipse.jgit.junit.ssh;version="7.0.0"
+Export-Package: org.eclipse.jgit.junit.ssh;version="7.3.0"
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 a7ca633..27d1737 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit.ssh;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit.ssh;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit.ssh/pom.xml b/org.eclipse.jgit.junit.ssh/pom.xml
index 2a885f2..cbdfa31 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.ssh</artifactId>
diff --git a/org.eclipse.jgit.junit/.settings/.api_filters b/org.eclipse.jgit.junit/.settings/.api_filters
new file mode 100644
index 0000000..2781530
--- /dev/null
+++ b/org.eclipse.jgit.junit/.settings/.api_filters
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<component id="org.eclipse.jgit.junit" version="2">
+    <resource path="src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java" type="org.eclipse.jgit.junit.LocalDiskRepositoryTestCase">
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.junit.LocalDiskRepositoryTestCase"/>
+                <message_argument value="testRoot"/>
+            </message_arguments>
+        </filter>
+    </resource>
+</component>
diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
index af1cbf2..5f0546e 100644
--- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
@@ -3,36 +3,36 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.junit
 Bundle-SymbolicName: org.eclipse.jgit.junit
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.pack;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.util;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.merge;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="7.0.0",
- org.eclipse.jgit.treewalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util.time;version="[7.0.0,7.1.0)",
+Import-Package: org.eclipse.jgit.annotations;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.dircache;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.util;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.merge;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="7.3.0",
+ org.eclipse.jgit.treewalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util.io;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util.time;version="[7.3.0,7.4.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,3.0.0)"
-Export-Package: org.eclipse.jgit.junit;version="7.0.0";
+Export-Package: org.eclipse.jgit.junit;version="7.3.0";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
@@ -45,4 +45,4 @@
    org.junit.runners.model,
    org.junit.runner,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.junit.time;version="7.0.0";uses:="org.eclipse.jgit.util.time"
+ org.eclipse.jgit.junit.time;version="7.3.0";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 149ad00..4e0108a 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.junit;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.junit;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.junit/pom.xml b/org.eclipse.jgit.junit/pom.xml
index c33cb29..dbb8b06 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit</artifactId>
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/FakeIndexFactory.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/FakeIndexFactory.java
new file mode 100644
index 0000000..eb23bec
--- /dev/null
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/FakeIndexFactory.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toUnmodifiableList;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.internal.storage.file.PackIndex.EntriesIterator;
+import org.eclipse.jgit.internal.storage.file.PackReverseIndex;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Create indexes with predefined data
+ *
+ * @since 7.2
+ */
+public class FakeIndexFactory {
+
+	/**
+	 * An object for the fake index
+	 *
+	 * @param name
+	 *            a sha1
+	 * @param offset
+	 *            the (fake) position of the object in the pack
+	 */
+	public record IndexObject(String name, long offset) {
+		/**
+		 * Name (sha1) as an objectId
+		 *
+		 * @return name (a sha1) as an objectId.
+		 */
+		public ObjectId getObjectId() {
+			return ObjectId.fromString(name);
+		}
+	}
+
+	/**
+	 * Return an index populated with these objects
+	 *
+	 * @param objs
+	 *            objects to be indexed
+	 * @return a PackIndex implementation
+	 */
+	public static PackIndex indexOf(List<IndexObject> objs) {
+		return new FakePackIndex(objs);
+	}
+
+	/**
+	 * Return a reverse pack index with these objects
+	 *
+	 * @param objs
+	 *            objects to be indexed
+	 * @return a PackReverseIndex implementation
+	 */
+	public static PackReverseIndex reverseIndexOf(List<IndexObject> objs) {
+		return new FakeReverseIndex(objs);
+	}
+
+	private FakeIndexFactory() {
+	}
+
+	private static class FakePackIndex implements PackIndex {
+		private static final Comparator<IndexObject> SHA1_COMPARATOR = (o1,
+				o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.name(),
+						o2.name());
+
+		private final Map<String, IndexObject> idx;
+
+		private final List<IndexObject> sha1Ordered;
+
+		private final long offset64count;
+
+		FakePackIndex(List<IndexObject> objs) {
+			sha1Ordered = objs.stream().sorted(SHA1_COMPARATOR)
+					.collect(toUnmodifiableList());
+			idx = objs.stream().collect(toMap(IndexObject::name, identity()));
+			offset64count = objs.stream()
+					.filter(o -> o.offset > Integer.MAX_VALUE).count();
+		}
+
+		@Override
+		public Iterator<MutableEntry> iterator() {
+			return new FakeEntriesIterator(sha1Ordered);
+		}
+
+		@Override
+		public long getObjectCount() {
+			return sha1Ordered.size();
+		}
+
+		@Override
+		public long getOffset64Count() {
+			return offset64count;
+		}
+
+		@Override
+		public ObjectId getObjectId(long nthPosition) {
+			return ObjectId
+					.fromString(sha1Ordered.get((int) nthPosition).name());
+		}
+
+		@Override
+		public long getOffset(long nthPosition) {
+			return sha1Ordered.get((int) nthPosition).offset();
+		}
+
+		@Override
+		public long findOffset(AnyObjectId objId) {
+			IndexObject o = idx.get(objId.name());
+			if (o == null) {
+				return -1;
+			}
+			return o.offset();
+		}
+
+		@Override
+		public int findPosition(AnyObjectId objId) {
+			IndexObject o = idx.get(objId.name());
+			if (o == null) {
+				return -1;
+			}
+			return sha1Ordered.indexOf(o);
+		}
+
+		@Override
+		public long findCRC32(AnyObjectId objId) throws MissingObjectException {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override
+		public boolean hasCRC32Support() {
+			return false;
+		}
+
+		@Override
+		public void resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
+				int matchLimit) throws IOException {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override
+		public byte[] getChecksum() {
+			return new byte[0];
+		}
+	}
+
+	private static class FakeReverseIndex implements PackReverseIndex {
+		private static final Comparator<IndexObject> OFFSET_COMPARATOR = Comparator
+				.comparingLong(IndexObject::offset);
+
+		private final List<IndexObject> byOffset;
+
+		private final Map<Long, IndexObject> ridx;
+
+		FakeReverseIndex(List<IndexObject> objs) {
+			byOffset = objs.stream().sorted(OFFSET_COMPARATOR)
+					.collect(toUnmodifiableList());
+			ridx = byOffset.stream()
+					.collect(toMap(IndexObject::offset, identity()));
+		}
+
+		@Override
+		public void verifyPackChecksum(String packFilePath) {
+			// Do nothing
+		}
+
+		@Override
+		public ObjectId findObject(long offset) {
+			IndexObject indexObject = ridx.get(offset);
+			if (indexObject == null) {
+				return null;
+			}
+			return ObjectId.fromString(indexObject.name());
+		}
+
+		@Override
+		public long findNextOffset(long offset, long maxOffset)
+				throws CorruptObjectException {
+			IndexObject o = ridx.get(offset);
+			if (o == null) {
+				throw new CorruptObjectException("Invalid offset"); //$NON-NLS-1$
+			}
+			int pos = byOffset.indexOf(o);
+			if (pos == byOffset.size() - 1) {
+				return maxOffset;
+			}
+			return byOffset.get(pos + 1).offset();
+		}
+
+		@Override
+		public int findPosition(long offset) {
+			IndexObject indexObject = ridx.get(offset);
+			return byOffset.indexOf(indexObject);
+		}
+
+		@Override
+		public ObjectId findObjectByPosition(int nthPosition) {
+			return byOffset.get(nthPosition).getObjectId();
+		}
+	}
+
+	private static class FakeEntriesIterator extends EntriesIterator {
+
+		private static final byte[] buffer = new byte[Constants.OBJECT_ID_LENGTH];
+
+		private final Iterator<IndexObject> it;
+
+		FakeEntriesIterator(List<IndexObject> objs) {
+			super(objs.size());
+			it = objs.iterator();
+		}
+
+		@Override
+		protected void readNext() {
+			IndexObject next = it.next();
+			next.getObjectId().copyRawTo(buffer, 0);
+			setIdBuffer(buffer, 0);
+			setOffset(next.offset());
+		}
+	}
+}
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
index 407290a..0d20f64 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
@@ -20,19 +20,19 @@
 import java.io.IOException;
 import java.io.PrintStream;
 import java.time.Instant;
+import java.time.ZoneId;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Random;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.internal.util.ShutdownHook;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -47,6 +47,7 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestName;
 
 /**
@@ -84,6 +85,16 @@ public abstract class LocalDiskRepositoryTestCase {
 	protected MockSystemReader mockSystemReader;
 
 	private final Set<Repository> toClose = new HashSet<>();
+
+	/**
+	 * Temporary test root directory for files created by tests.
+	 * @since 7.2
+	 */
+	@Rule
+	public TemporaryFolder testRoot = new TemporaryFolder();
+
+	Random rand = new Random();
+
 	private File tmp;
 
 	private File homeDir;
@@ -114,11 +125,8 @@ private String getTestName() {
 	 */
 	@Before
 	public void setUp() throws Exception {
-		tmp = File.createTempFile("jgit_" + getTestName() + '_', "_tmp");
-		Cleanup.deleteOnShutdown(tmp);
-		if (!tmp.delete() || !tmp.mkdir()) {
-			throw new IOException("Cannot create " + tmp);
-		}
+		tmp = testRoot.newFolder(getTestName() + rand.nextInt());
+
 		mockSystemReader = new MockSystemReader();
 		SystemReader.setInstance(mockSystemReader);
 
@@ -219,12 +227,6 @@ public void tearDown() throws Exception {
 			System.gc();
 		}
 		FS.DETECTED.setUserHome(homeDir);
-		if (tmp != null) {
-			recursiveDelete(tmp, false, true);
-		}
-		if (tmp != null && !tmp.exists()) {
-			Cleanup.removed(tmp);
-		}
 		SystemReader.setInstance(null);
 	}
 
@@ -233,8 +235,8 @@ public void tearDown() throws Exception {
 	 */
 	protected void tick() {
 		mockSystemReader.tick(5 * 60);
-		final long now = mockSystemReader.getCurrentTime();
-		final int tz = mockSystemReader.getTimezone(now);
+		Instant now = mockSystemReader.now();
+		ZoneId tz = mockSystemReader.getTimeZoneId();
 
 		author = new PersonIdent(author, now, tz);
 		committer = new PersonIdent(committer, now, tz);
@@ -623,41 +625,4 @@ protected String read(File f) throws IOException {
 	private static HashMap<String, String> cloneEnv() {
 		return new HashMap<>(System.getenv());
 	}
-
-	private static final class Cleanup {
-		private static final Cleanup INSTANCE = new Cleanup();
-
-		static {
-			ShutdownHook.INSTANCE.register(() -> INSTANCE.onShutdown());
-		}
-
-		private final Set<File> toDelete = ConcurrentHashMap.newKeySet();
-
-		private Cleanup() {
-			// empty
-		}
-
-		static void deleteOnShutdown(File tmp) {
-			INSTANCE.toDelete.add(tmp);
-		}
-
-		static void removed(File tmp) {
-			INSTANCE.toDelete.remove(tmp);
-		}
-
-		private void onShutdown() {
-			// On windows accidentally open files or memory
-			// mapped regions may prevent files from being deleted.
-			// Suggesting a GC increases the likelihood that our
-			// test repositories actually get removed after the
-			// tests, even in the case of failure.
-			System.gc();
-			synchronized (this) {
-				boolean silent = false;
-				boolean failOnError = false;
-				for (File tmp : toDelete)
-					recursiveDelete(tmp, silent, failOnError);
-			}
-		}
-	}
 }
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
index 419fdb1..38f0d0b 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
@@ -18,6 +18,8 @@
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.time.Duration;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -242,6 +244,11 @@ public TimeZone getTimeZone() {
 	}
 
 	@Override
+	public ZoneId getTimeZoneId() {
+		return ZoneOffset.ofHoursMinutes(-3, -30);
+	}
+
+	@Override
 	public Locale getLocale() {
 		return Locale.US;
 	}
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 c8c56b2..2a482df 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
@@ -44,7 +44,7 @@ private static Class<?> loadNewClass(Class<?> klass)
 		try {
 			String pathSeparator = System.getProperty("path.separator");
 			String[] classPathEntries = System.getProperty("java.class.path")
-					.split(pathSeparator);
+					.split(pathSeparator, -1);
 			URL[] urls = new URL[classPathEntries.length];
 			for (int i = 0; i < classPathEntries.length; i++) {
 				urls[i] = Paths.get(classPathEntries[i]).toUri().toURL();
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 a2e0a57..2d00a85 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
@@ -21,6 +21,7 @@
 import java.io.OutputStream;
 import java.security.MessageDigest;
 import java.time.Instant;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -30,6 +31,7 @@
 import java.util.Set;
 import java.util.TimeZone;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -157,8 +159,8 @@ public TestRepository(R db, RevWalk rw, MockSystemReader reader)
 		this.pool = rw;
 		this.inserter = db.newObjectInserter();
 		this.mockSystemReader = reader;
-		long now = mockSystemReader.getCurrentTime();
-		int tz = mockSystemReader.getTimezone(now);
+		Instant now = mockSystemReader.now();
+		ZoneId tz = mockSystemReader.getTimeZoneAt(now);
 		defaultAuthor = new PersonIdent(AUTHOR, AUTHOR_EMAIL, now, tz);
 		defaultCommitter = new PersonIdent(COMMITTER, COMMITTER_EMAIL, now, tz);
 	}
@@ -197,7 +199,9 @@ public Git git() {
 	 *
 	 * @return current date.
 	 * @since 4.2
+	 * @deprecated Use {@link #getInstant()} instead.
 	 */
+	@Deprecated(since = "7.2")
 	public Date getDate() {
 		return new Date(mockSystemReader.getCurrentTime());
 	}
@@ -209,18 +213,31 @@ public Date getDate() {
 	 * @since 6.8
 	 */
 	public Instant getInstant() {
-		return Instant.ofEpochMilli(mockSystemReader.getCurrentTime());
+		return mockSystemReader.now();
 	}
 
 	/**
 	 * Get timezone
 	 *
 	 * @return timezone used for default identities.
+	 * @deprecated Use {@link #getTimeZoneId()} instead.
 	 */
+	@Deprecated(since = "7.2")
 	public TimeZone getTimeZone() {
 		return mockSystemReader.getTimeZone();
 	}
 
+
+	/**
+	 * Get timezone
+	 *
+	 * @return timezone used for default identities.
+	 * @since 7.2
+	 */
+	public ZoneId getTimeZoneId() {
+		return mockSystemReader.getTimeZoneId();
+	}
+
 	/**
 	 * Adjust the current time that will used by the next commit.
 	 *
@@ -232,14 +249,14 @@ public void tick(int secDelta) {
 	}
 
 	/**
-	 * Set the author and committer using {@link #getDate()}.
+	 * Set the author and committer using {@link #getInstant()}.
 	 *
 	 * @param c
 	 *            the commit builder to store.
 	 */
 	public void setAuthorAndCommitter(org.eclipse.jgit.lib.CommitBuilder c) {
-		c.setAuthor(new PersonIdent(defaultAuthor, getDate()));
-		c.setCommitter(new PersonIdent(defaultCommitter, getDate()));
+		c.setAuthor(new PersonIdent(defaultAuthor, getInstant()));
+		c.setCommitter(new PersonIdent(defaultCommitter, getInstant()));
 	}
 
 	/**
@@ -487,8 +504,8 @@ public ObjectId unparsedCommit(final int secDelta, final RevTree tree,
 		c = new org.eclipse.jgit.lib.CommitBuilder();
 		c.setTreeId(tree);
 		c.setParentIds(parents);
-		c.setAuthor(new PersonIdent(defaultAuthor, getDate()));
-		c.setCommitter(new PersonIdent(defaultCommitter, getDate()));
+		c.setAuthor(new PersonIdent(defaultAuthor, getInstant()));
+		c.setCommitter(new PersonIdent(defaultCommitter, getInstant()));
 		c.setMessage("");
 		ObjectId id;
 		try (ObjectInserter ins = inserter) {
@@ -528,7 +545,7 @@ public RevTag tag(String name, RevObject dst) throws Exception {
 		final TagBuilder t = new TagBuilder();
 		t.setObjectId(dst);
 		t.setTag(name);
-		t.setTagger(new PersonIdent(defaultCommitter, getDate()));
+		t.setTagger(new PersonIdent(defaultCommitter, getInstant()));
 		t.setMessage("");
 		ObjectId id;
 		try (ObjectInserter ins = inserter) {
@@ -797,7 +814,7 @@ public RevCommit cherryPick(AnyObjectId id) throws Exception {
 			b.setParentId(head);
 			b.setTreeId(merger.getResultTreeId());
 			b.setAuthor(commit.getAuthorIdent());
-			b.setCommitter(new PersonIdent(defaultCommitter, getDate()));
+			b.setCommitter(new PersonIdent(defaultCommitter, getInstant()));
 			b.setMessage(commit.getFullMessage());
 			ObjectId result;
 			try (ObjectInserter ins = inserter) {
@@ -1019,7 +1036,8 @@ public void close() {
 	private static void prunePacked(ObjectDirectory odb) throws IOException {
 		for (Pack p : odb.getPacks()) {
 			for (MutableEntry e : p)
-				FileUtils.delete(odb.fileFor(e.toObjectId()));
+				FileUtils.delete(odb.fileFor(e.toObjectId()),
+						FileUtils.SKIP_MISSING);
 		}
 	}
 
@@ -1144,15 +1162,18 @@ public class CommitBuilder {
 		}
 
 		/**
-		 * set parent commit
+		 * Set parent commit
 		 *
 		 * @param p
-		 *            parent commit
+		 *            parent commit, can be {@code null}
 		 * @return this commit builder
 		 * @throws Exception
 		 *             if an error occurred
 		 */
-		public CommitBuilder parent(RevCommit p) throws Exception {
+		public CommitBuilder parent(@Nullable RevCommit p) throws Exception {
+			if (p == null) {
+				return this;
+			}
 			if (parents.isEmpty()) {
 				DirCacheBuilder b = tree.builder();
 				parseBody(p);
@@ -1403,7 +1424,7 @@ public RevCommit create() throws Exception {
 					c.setAuthor(author);
 				if (committer != null) {
 					if (updateCommitterTime)
-						committer = new PersonIdent(committer, getDate());
+						committer = new PersonIdent(committer, getInstant());
 					c.setCommitter(committer);
 				}
 
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 8e41ee3..8feb8ef 100644
--- a/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.server.test
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server.test
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -26,24 +26,24 @@
  org.eclipse.jetty.util.component;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.security;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.thread;version="[12.0.0,13.0.0)",
- org.eclipse.jgit.api;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit.http;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.server;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.server.fs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.test;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit.http;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.server;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.test;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.rules;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.lfs.server.test/pom.xml b/org.eclipse.jgit.lfs.server.test/pom.xml
index 49c1023..176f4af 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 a59c82e..ed8cfff 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: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.lfs.server;version="7.0.0";
+Export-Package: org.eclipse.jgit.lfs.server;version="7.3.0";
   uses:="jakarta.servlet.http,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.fs;version="7.0.0";
+ org.eclipse.jgit.lfs.server.fs;version="7.3.0";
   uses:="jakarta.servlet,
    jakarta.servlet.http,
    org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.internal;version="7.0.0";x-internal:=true,
- org.eclipse.jgit.lfs.server.s3;version="7.0.0";
+ org.eclipse.jgit.lfs.server.internal;version="7.3.0";x-internal:=true,
+ org.eclipse.jgit.lfs.server.s3;version="7.3.0";
   uses:="org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib"
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -24,15 +24,15 @@
  jakarta.servlet.annotation;version="[6.0.0,7.0.0)",
  jakarta.servlet.http;version="[6.0.0,7.0.0)",
  org.apache.http;version="[4.3.0,5.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.http.apache;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.annotations;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.http;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.http.apache;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
  org.slf4j;version="[1.7.0,3.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 2cf86bc..7e26500 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.lfs.server;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.lfs.server;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.lfs.server/pom.xml b/org.eclipse.jgit.lfs.server/pom.xml
index befaa36..ebb815f 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 96c8953..b8a2ca7 100644
--- a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
@@ -3,27 +3,28 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs.test
 Bundle-SymbolicName: org.eclipse.jgit.lfs.test
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.eclipse.jgit.api;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.attributes;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
+Import-Package: org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.attributes;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.http;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
  org.hamcrest.core;version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.runner;version="[4.13,5.0.0)",
  org.junit.runners;version="[4.13,5.0.0)"
-Export-Package: org.eclipse.jgit.lfs.test;version="7.0.0";x-friends:="org.eclipse.jgit.lfs.server.test"
+Export-Package: org.eclipse.jgit.lfs.test;version="7.3.0";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 2c109c9..c9591b6 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.test</artifactId>
diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java
index badcb7d..ee8e893 100644
--- a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java
+++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java
@@ -97,7 +97,8 @@ public void lfsUrlFromRemoteUrlWithoutDotGit() throws Exception {
 	public void lfsUrlFromLocalConfig() throws Exception {
 		addRemoteUrl("https://localhost/repo");
 
-		StoredConfig cfg = ((Repository) db).getConfig();
+		@SuppressWarnings("restriction")
+		StoredConfig cfg = db.getConfig();
 		cfg.setString(ConfigConstants.CONFIG_SECTION_LFS,
 				null,
 				ConfigConstants.CONFIG_KEY_URL,
@@ -111,7 +112,8 @@ public void lfsUrlFromLocalConfig() throws Exception {
 	public void lfsUrlFromOriginConfig() throws Exception {
 		addRemoteUrl("https://localhost/repo");
 
-		StoredConfig cfg = ((Repository) db).getConfig();
+		@SuppressWarnings("restriction")
+		StoredConfig cfg = db.getConfig();
 		cfg.setString(ConfigConstants.CONFIG_SECTION_LFS,
 				org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME,
 				ConfigConstants.CONFIG_KEY_URL,
diff --git a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
index f4c1e56..5338f8b 100644
--- a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
@@ -3,32 +3,32 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.lfs
 Bundle-SymbolicName: org.eclipse.jgit.lfs
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
-Export-Package: org.eclipse.jgit.lfs;version="7.0.0",
- org.eclipse.jgit.lfs.errors;version="7.0.0",
- org.eclipse.jgit.lfs.internal;version="7.0.0";x-friends:="org.eclipse.jgit.lfs.test,org.eclipse.jgit.lfs.server.fs,org.eclipse.jgit.lfs.server",
- org.eclipse.jgit.lfs.lib;version="7.0.0"
+Export-Package: org.eclipse.jgit.lfs;version="7.3.0",
+ org.eclipse.jgit.lfs.errors;version="7.3.0",
+ org.eclipse.jgit.lfs.internal;version="7.3.0";x-friends:="org.eclipse.jgit.lfs.test,org.eclipse.jgit.lfs.server.fs,org.eclipse.jgit.lfs.server",
+ org.eclipse.jgit.lfs.lib;version="7.3.0"
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: com.google.gson;version="[2.8.2,3.0.0)",
  com.google.gson.stream;version="[2.8.2,3.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)";resolution:=optional,
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.attributes;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.diff;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.hooks;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.pack;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.0,7.1.0)"
+ org.eclipse.jgit.annotations;version="[7.3.0,7.4.0)";resolution:=optional,
+ org.eclipse.jgit.api.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.attributes;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.diff;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.dircache;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.hooks;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.pack;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.http;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util.io;version="[7.3.0,7.4.0)"
diff --git a/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/SOURCE-MANIFEST.MF
index a048aa1..fc12482 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.lfs;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.lfs;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.lfs/pom.xml b/org.eclipse.jgit.lfs/pom.xml
index 62508e2..d336f16 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs</artifactId>
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java
index 75d500e..a13a60c 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/AnyLongObjectId.java
@@ -41,24 +41,6 @@ public abstract class AnyLongObjectId implements Comparable<AnyLongObjectId> {
 	 * @param secondObjectId
 	 *            the second identifier to compare. Must not be null.
 	 * @return true if the two identifiers are the same.
-	 * @deprecated use {@link #isEqual(AnyLongObjectId, AnyLongObjectId)}
-	 *             instead.
-	 */
-	@Deprecated
-	@SuppressWarnings("AmbiguousMethodReference")
-	public static boolean equals(final AnyLongObjectId firstObjectId,
-			final AnyLongObjectId secondObjectId) {
-		return isEqual(firstObjectId, secondObjectId);
-	}
-
-	/**
-	 * Compare two object identifier byte sequences for equality.
-	 *
-	 * @param firstObjectId
-	 *            the first identifier to compare. Must not be null.
-	 * @param secondObjectId
-	 *            the second identifier to compare. Must not be null.
-	 * @return true if the two identifiers are the same.
 	 * @since 5.4
 	 */
 	public static boolean isEqual(final AnyLongObjectId firstObjectId,
@@ -263,7 +245,7 @@ public final int hashCode() {
 	 */
 	@SuppressWarnings({ "NonOverridingEquals", "AmbiguousMethodReference" })
 	public final boolean equals(AnyLongObjectId other) {
-		return other != null ? equals(this, other) : false;
+		return other != null ? isEqual(this, other) : false;
 	}
 
 	@Override
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 3dd7b10..adea43b 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="7.0.0.qualifier"
+      version="7.3.0.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 ea66a23..734b634 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 23fdd23..f86652f 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="7.0.0.qualifier"
+      version="7.3.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="7.0.0" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="7.3.0" 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 fc1bf49..a756a8d 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 92262a0..d999c15 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="7.0.0.qualifier"
+      version="7.3.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="7.0.0" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="7.3.0" 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 3344dcb..c2ca95a 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 6f03419..922622c 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="7.0.0.qualifier"
+      version="7.3.0.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="7.0.0" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="7.3.0" 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 45162c9..71e7373 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 8eacae1..53cb4bac 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="7.0.0.qualifier"
+      version="7.3.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="7.0.0" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="7.3.0" 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 287a723..31ba89f 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 179eab8..2d7ee4e 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="7.0.0.qualifier"
+      version="7.3.0.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="7.0.0" match="equivalent"/>
-      <import feature="org.eclipse.jgit.lfs" version="7.0.0" match="equivalent"/>
-      <import feature="org.eclipse.jgit.ssh.apache" version="7.0.0" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="7.3.0" match="equivalent"/>
+      <import feature="org.eclipse.jgit.lfs" version="7.3.0" match="equivalent"/>
+      <import feature="org.eclipse.jgit.ssh.apache" version="7.3.0" 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 cbb8e4f..2c80319 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/category.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/category.xml
index f46e08d..eef699c 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/category.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/category.xml
@@ -123,12 +123,6 @@
    <bundle id="org.eclipse.jetty.util.ajax.source">
       <category name="JGit-dependency-bundles"/>
    </bundle>
-   <bundle id="net.i2p.crypto.eddsa">
-      <category name="JGit-dependency-bundles"/>
-   </bundle>
-   <bundle id="net.i2p.crypto.eddsa.source">
-      <category name="JGit-dependency-bundles"/>
-   </bundle>
    <bundle id="org.apache.ant">
       <category name="JGit-dependency-bundles"/>
    </bundle>
@@ -147,10 +141,13 @@
    <bundle id="org.apache.commons.commons-compress.source">
       <category name="JGit-dependency-bundles"/>
    </bundle>
-   <bundle id="org.apache.commons.logging">
+   <bundle id="org.apache.commons.lang3">
       <category name="JGit-dependency-bundles"/>
    </bundle>
-   <bundle id="org.apache.commons.logging.source">
+   <bundle id="org.apache.commons.lang3.source">
+      <category name="JGit-dependency-bundles"/>
+   </bundle>
+   <bundle id="org.apache.commons.logging">
       <category name="JGit-dependency-bundles"/>
    </bundle>
    <bundle id="org.apache.httpcomponents.httpclient">
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 61b5dd6..032072f 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 4c0068b..139569d 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="7.0.0.qualifier"
+      version="7.3.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="7.0.0" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="7.3.0" 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 12ab069..43568f8 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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>7.0.0-SNAPSHOT</version>
+      <version>7.3.0-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 209274f..7f80bbf 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="7.0.0.qualifier"
+      version="7.3.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="7.0.0" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="7.3.0" 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 1d82d4a..fb9f73b 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 ff46d9e..5eaa3b7 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="7.0.0.qualifier"
+      version="7.3.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -23,7 +23,7 @@
    </url>
 
    <requires>
-      <import plugin="org.eclipse.jgit" version="7.0.0" match="equivalent"/>
+      <import plugin="org.eclipse.jgit" version="7.3.0" 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 c4d4fcf..d5da669 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
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
deleted file mode 100644
index 8899b51..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?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="1715125111">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2020-09/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
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
deleted file mode 100644
index b3ff205..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.17" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.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
deleted file mode 100644
index ee56a72..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.18" sequenceNumber="1715125111">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2020-12/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
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
deleted file mode 100644
index 719476a..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.18" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2020-12/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target
deleted file mode 100644
index ad35e58..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?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="1715125111">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2021-03/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd
deleted file mode 100644
index 9eb4436..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.19-staging" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2021-03/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target
deleted file mode 100644
index 4031c04..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.20" sequenceNumber="1715125111">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2021-06/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd
deleted file mode 100644
index 264c040..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.20" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2021-06/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target
deleted file mode 100644
index 1a119f2..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.21" sequenceNumber="1715125111">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2021-09/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd
deleted file mode 100644
index 5c7a112..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.21" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2021-09/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target
deleted file mode 100644
index 1f6ee4e..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.22" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2021-12/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd
deleted file mode 100644
index ecc776e..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.22" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2021-12/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target
deleted file mode 100644
index d0a0420..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.23" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2022-03/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd
deleted file mode 100644
index 16efb40..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.23" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2022-03/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target
deleted file mode 100644
index dc9e090..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.24" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2022-06/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd
deleted file mode 100644
index d0f8e8d..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.24" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2022-06/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target
deleted file mode 100644
index 53dcb36..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.25" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2022-09/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd
deleted file mode 100644
index be37c10..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.25" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2022-09/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.26.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.26.target
deleted file mode 100644
index 822c7cf..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.26.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.26" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2022-12/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.26.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.26.tpd
deleted file mode 100644
index e269919..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.26.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.26" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2022-12/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.27.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.27.target
deleted file mode 100644
index 9a0cab4..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.27.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.27" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2023-03/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.27.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.27.tpd
deleted file mode 100644
index b67718a..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.27.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.27" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2023-03/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.28.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.28.target
deleted file mode 100644
index 22aa30e..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.28.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.28" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2023-06"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.28.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.28.tpd
deleted file mode 100644
index 1a9a22a..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.28.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.28" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2023-06" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.29.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.29.target
deleted file mode 100644
index f7fb7c9..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.29.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.29" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </location>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.osgi" version="0.0.0"/>
-      <repository location="https://download.eclipse.org/releases/2023-09"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.29.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.29.tpd
deleted file mode 100644
index 4e34280..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.29.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.29" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/releases/2023-09" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.30.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.30.target
deleted file mode 100644
index 733559d..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.30.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.30" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </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/2023-12/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.30.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.30.tpd
deleted file mode 100644
index dfb4474..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.30.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.30" with source configurePhase
-
-include "orbit/orbit-4.30.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/staging/2023-12/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.31.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.31.target
deleted file mode 100644
index 78ef168..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.31.target
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?pde?>
-<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
-<target name="jgit-4.31" sequenceNumber="1715125110">
-  <locations>
-    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
-      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
-      <unit id="net.i2p.crypto.eddsa" version="0.3.0"/>
-      <unit id="net.i2p.crypto.eddsa.source" version="0.3.0"/>
-      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
-      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
-      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
-      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
-      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
-      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
-      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
-      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
-      <unit id="org.objenesis" version="3.3.0"/>
-      <unit id="org.objenesis.source" version="3.3.0"/>
-      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
-      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
-      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12"/>
-    </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/2024-03/"/>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.tukaani</groupId>
-    		<artifactId>xz</artifactId>
-    		<version>1.9</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-api</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.slf4j</groupId>
-    		<artifactId>slf4j-simple</artifactId>
-    		<version>1.7.36</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-osgi</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.sshd</groupId>
-    		<artifactId>sshd-sftp</artifactId>
-    		<version>2.12.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.mockito</groupId>
-    		<artifactId>mockito-core</artifactId>
-    		<version>5.10.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.java.dev.jna</groupId>
-    		<artifactId>jna-platform</artifactId>
-    		<version>5.14.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.eclipse.jetty.ee10</groupId>
-    		<artifactId>jetty-ee10-servlet</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-http</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-io</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-security</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-server</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-session</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.eclipse.jetty</groupId>
-    		<artifactId>jetty-util-ajax</artifactId>
-    		<version>12.0.9</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>jakarta.servlet</groupId>
-    		<artifactId>jakarta.servlet-api</artifactId>
-    		<version>6.0.0</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.googlecode.javaewah</groupId>
-    		<artifactId>JavaEWAH</artifactId>
-    		<version>1.2.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.hamcrest</groupId>
-    		<artifactId>hamcrest</artifactId>
-    		<version>2.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
-    <dependencies>
-    	<dependency>
-    		<groupId>com.google.code.gson</groupId>
-    		<artifactId>gson</artifactId>
-    		<version>2.10.1</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
-    <dependencies>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>net.bytebuddy</groupId>
-    		<artifactId>byte-buddy-agent</artifactId>
-    		<version>1.14.12</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpg-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcprov-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcpkix-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.bouncycastle</groupId>
-    		<artifactId>bcutil-jdk18on</artifactId>
-    		<version>1.77</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
-    <dependencies>
-    	<dependency>
-    		<groupId>org.assertj</groupId>
-    		<artifactId>assertj-core</artifactId>
-    		<version>3.25.3</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
-    <dependencies>
-    	<dependency>
-    		<groupId>args4j</groupId>
-    		<artifactId>args4j</artifactId>
-    		<version>2.33</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
-    <dependencies>
-    	<dependency>
-    		<groupId>commons-codec</groupId>
-    		<artifactId>commons-codec</artifactId>
-    		<version>1.16.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>org.apache.commons</groupId>
-    		<artifactId>commons-compress</artifactId>
-    		<version>1.26.0</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-io</groupId>
-    		<artifactId>commons-io</artifactId>
-    		<version>2.15.1</version>
-    		<type>jar</type>
-    	</dependency>
-    	<dependency>
-    		<groupId>commons-logging</groupId>
-    		<artifactId>commons-logging</artifactId>
-    		<version>1.2</version>
-    		<type>jar</type>
-    	</dependency>
-    </dependencies>
-    </location>
-  </locations>
-</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.31.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.31.tpd
deleted file mode 100644
index 58491c8..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.31.tpd
+++ /dev/null
@@ -1,8 +0,0 @@
-target "jgit-4.31" with source configurePhase
-
-include "orbit/orbit-4.31.tpd"
-include "maven/dependencies.tpd"
-
-location "https://download.eclipse.org/staging/2024-03/" {
-	org.eclipse.osgi lazy
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.target
new file mode 100644
index 0000000..60baf0b
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.target
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?pde?>
+<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
+<target name="jgit-4.32" sequenceNumber="1740521280">
+  <locations>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
+      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
+      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
+      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
+      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
+      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
+      <unit id="org.objenesis" version="3.4.0"/>
+      <unit id="org.objenesis.source" version="3.4.0"/>
+      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
+      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
+      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2024-06"/>
+    </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/2024-06/"/>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
+      <dependencies>
+        <dependency>
+          <groupId>org.tukaani</groupId>
+          <artifactId>xz</artifactId>
+          <version>1.10</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
+      <dependencies>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-api</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-simple</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
+      <dependencies>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-osgi</artifactId>
+          <version>2.15.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-sftp</artifactId>
+          <version>2.15.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
+      <dependencies>
+        <dependency>
+          <groupId>org.mockito</groupId>
+          <artifactId>mockito-core</artifactId>
+          <version>5.15.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
+      <dependencies>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna</artifactId>
+          <version>5.16.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna-platform</artifactId>
+          <version>5.16.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
+      <dependencies>
+        <dependency>
+          <groupId>org.eclipse.jetty.ee10</groupId>
+          <artifactId>jetty-ee10-servlet</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-http</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-io</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-security</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-server</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-session</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util-ajax</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>jakarta.servlet</groupId>
+          <artifactId>jakarta.servlet-api</artifactId>
+          <version>6.1.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
+      <dependencies>
+        <dependency>
+          <groupId>com.googlecode.javaewah</groupId>
+          <artifactId>JavaEWAH</artifactId>
+          <version>1.2.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
+      <dependencies>
+        <dependency>
+          <groupId>org.hamcrest</groupId>
+          <artifactId>hamcrest</artifactId>
+          <version>2.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
+      <dependencies>
+        <dependency>
+          <groupId>com.google.code.gson</groupId>
+          <artifactId>gson</artifactId>
+          <version>2.12.1</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
+      <dependencies>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy</artifactId>
+          <version>1.17.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy-agent</artifactId>
+          <version>1.17.1</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
+      <dependencies>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpg-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpkix-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcutil-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
+      <dependencies>
+        <dependency>
+          <groupId>org.assertj</groupId>
+          <artifactId>assertj-core</artifactId>
+          <version>3.27.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
+      <dependencies>
+        <dependency>
+          <groupId>args4j</groupId>
+          <artifactId>args4j</artifactId>
+          <version>2.37</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
+      <dependencies>
+        <dependency>
+          <groupId>commons-codec</groupId>
+          <artifactId>commons-codec</artifactId>
+          <version>1.18.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-compress</artifactId>
+          <version>1.27.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-lang3</artifactId>
+          <version>3.17.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-io</groupId>
+          <artifactId>commons-io</artifactId>
+          <version>2.18.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-logging</groupId>
+          <artifactId>commons-logging</artifactId>
+          <version>1.3.5</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+  </locations>
+</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.tpd
new file mode 100644
index 0000000..b8574c7
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.32.tpd
@@ -0,0 +1,8 @@
+target "jgit-4.32" with source configurePhase
+
+include "orbit/orbit-4.32.tpd"
+include "maven/dependencies.tpd"
+
+location "https://download.eclipse.org/staging/2024-06/" {
+	org.eclipse.osgi lazy
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.target
new file mode 100644
index 0000000..1558ad6
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.target
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?pde?>
+<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
+<target name="jgit-4.33" sequenceNumber="1740521283">
+  <locations>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
+      <unit id="org.apache.ant" version="1.10.14.v20230922-1200"/>
+      <unit id="org.apache.ant.source" version="1.10.14.v20230922-1200"/>
+      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
+      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.junit" version="4.13.2.v20230809-1000"/>
+      <unit id="org.junit.source" version="4.13.2.v20230809-1000"/>
+      <unit id="org.objenesis" version="3.4.0"/>
+      <unit id="org.objenesis.source" version="3.4.0"/>
+      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
+      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
+      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2024-09"/>
+    </location>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="org.eclipse.osgi" version="0.0.0"/>
+      <repository location="https://download.eclipse.org/releases/2024-09/"/>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
+      <dependencies>
+        <dependency>
+          <groupId>org.tukaani</groupId>
+          <artifactId>xz</artifactId>
+          <version>1.10</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
+      <dependencies>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-api</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-simple</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
+      <dependencies>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-osgi</artifactId>
+          <version>2.15.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-sftp</artifactId>
+          <version>2.15.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
+      <dependencies>
+        <dependency>
+          <groupId>org.mockito</groupId>
+          <artifactId>mockito-core</artifactId>
+          <version>5.15.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
+      <dependencies>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna</artifactId>
+          <version>5.16.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna-platform</artifactId>
+          <version>5.16.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
+      <dependencies>
+        <dependency>
+          <groupId>org.eclipse.jetty.ee10</groupId>
+          <artifactId>jetty-ee10-servlet</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-http</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-io</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-security</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-server</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-session</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util-ajax</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>jakarta.servlet</groupId>
+          <artifactId>jakarta.servlet-api</artifactId>
+          <version>6.1.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
+      <dependencies>
+        <dependency>
+          <groupId>com.googlecode.javaewah</groupId>
+          <artifactId>JavaEWAH</artifactId>
+          <version>1.2.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
+      <dependencies>
+        <dependency>
+          <groupId>org.hamcrest</groupId>
+          <artifactId>hamcrest</artifactId>
+          <version>2.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
+      <dependencies>
+        <dependency>
+          <groupId>com.google.code.gson</groupId>
+          <artifactId>gson</artifactId>
+          <version>2.12.1</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
+      <dependencies>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy</artifactId>
+          <version>1.17.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy-agent</artifactId>
+          <version>1.17.1</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
+      <dependencies>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpg-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpkix-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcutil-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
+      <dependencies>
+        <dependency>
+          <groupId>org.assertj</groupId>
+          <artifactId>assertj-core</artifactId>
+          <version>3.27.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
+      <dependencies>
+        <dependency>
+          <groupId>args4j</groupId>
+          <artifactId>args4j</artifactId>
+          <version>2.37</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
+      <dependencies>
+        <dependency>
+          <groupId>commons-codec</groupId>
+          <artifactId>commons-codec</artifactId>
+          <version>1.18.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-compress</artifactId>
+          <version>1.27.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-lang3</artifactId>
+          <version>3.17.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-io</groupId>
+          <artifactId>commons-io</artifactId>
+          <version>2.18.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-logging</groupId>
+          <artifactId>commons-logging</artifactId>
+          <version>1.3.5</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+  </locations>
+</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.tpd
new file mode 100644
index 0000000..74c6878
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.33.tpd
@@ -0,0 +1,8 @@
+target "jgit-4.33" with source configurePhase
+
+include "orbit/orbit-4.33.tpd"
+include "maven/dependencies.tpd"
+
+location "https://download.eclipse.org/releases/2024-09/" {
+	org.eclipse.osgi lazy
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.target
new file mode 100644
index 0000000..bc35d2c
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.target
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?pde?>
+<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
+<target name="jgit-4.34" sequenceNumber="1740521284">
+  <locations>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
+      <unit id="org.apache.ant" version="1.10.15.v20240901-1000"/>
+      <unit id="org.apache.ant.source" version="1.10.15.v20240901-1000"/>
+      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
+      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.junit" version="4.13.2.v20240929-1000"/>
+      <unit id="org.junit.source" version="4.13.2.v20240929-1000"/>
+      <unit id="org.objenesis" version="3.4.0"/>
+      <unit id="org.objenesis.source" version="3.4.0"/>
+      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
+      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
+      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2024-12"/>
+    </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/2024-12/"/>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
+      <dependencies>
+        <dependency>
+          <groupId>org.tukaani</groupId>
+          <artifactId>xz</artifactId>
+          <version>1.10</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
+      <dependencies>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-api</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-simple</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
+      <dependencies>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-osgi</artifactId>
+          <version>2.15.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-sftp</artifactId>
+          <version>2.15.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
+      <dependencies>
+        <dependency>
+          <groupId>org.mockito</groupId>
+          <artifactId>mockito-core</artifactId>
+          <version>5.15.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
+      <dependencies>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna</artifactId>
+          <version>5.16.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna-platform</artifactId>
+          <version>5.16.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
+      <dependencies>
+        <dependency>
+          <groupId>org.eclipse.jetty.ee10</groupId>
+          <artifactId>jetty-ee10-servlet</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-http</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-io</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-security</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-server</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-session</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util-ajax</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>jakarta.servlet</groupId>
+          <artifactId>jakarta.servlet-api</artifactId>
+          <version>6.1.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
+      <dependencies>
+        <dependency>
+          <groupId>com.googlecode.javaewah</groupId>
+          <artifactId>JavaEWAH</artifactId>
+          <version>1.2.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
+      <dependencies>
+        <dependency>
+          <groupId>org.hamcrest</groupId>
+          <artifactId>hamcrest</artifactId>
+          <version>2.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
+      <dependencies>
+        <dependency>
+          <groupId>com.google.code.gson</groupId>
+          <artifactId>gson</artifactId>
+          <version>2.12.1</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
+      <dependencies>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy</artifactId>
+          <version>1.17.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy-agent</artifactId>
+          <version>1.17.1</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
+      <dependencies>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpg-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpkix-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcutil-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
+      <dependencies>
+        <dependency>
+          <groupId>org.assertj</groupId>
+          <artifactId>assertj-core</artifactId>
+          <version>3.27.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
+      <dependencies>
+        <dependency>
+          <groupId>args4j</groupId>
+          <artifactId>args4j</artifactId>
+          <version>2.37</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
+      <dependencies>
+        <dependency>
+          <groupId>commons-codec</groupId>
+          <artifactId>commons-codec</artifactId>
+          <version>1.18.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-compress</artifactId>
+          <version>1.27.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-lang3</artifactId>
+          <version>3.17.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-io</groupId>
+          <artifactId>commons-io</artifactId>
+          <version>2.18.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-logging</groupId>
+          <artifactId>commons-logging</artifactId>
+          <version>1.3.5</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+  </locations>
+</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.tpd
new file mode 100644
index 0000000..4c38371
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.34.tpd
@@ -0,0 +1,8 @@
+target "jgit-4.34" with source configurePhase
+
+include "orbit/orbit-4.34.tpd"
+include "maven/dependencies.tpd"
+
+location "https://download.eclipse.org/staging/2024-12/" {
+	org.eclipse.osgi lazy
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.35.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.35.target
new file mode 100644
index 0000000..15cabc3
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.35.target
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?pde?>
+<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl -->
+<target name="jgit-4.35" sequenceNumber="1740521286">
+  <locations>
+    <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
+      <unit id="com.jcraft.jsch" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jsch.source" version="0.1.55.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib" version="1.1.3.v20230916-1400"/>
+      <unit id="com.jcraft.jzlib.source" version="1.1.3.v20230916-1400"/>
+      <unit id="org.apache.ant" version="1.10.15.v20240901-1000"/>
+      <unit id="org.apache.ant.source" version="1.10.15.v20240901-1000"/>
+      <unit id="org.apache.httpcomponents.httpclient" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.14"/>
+      <unit id="org.apache.httpcomponents.httpcore" version="4.4.16"/>
+      <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.16"/>
+      <unit id="org.hamcrest.core" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.core.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library" version="1.3.0.v20230809-1000"/>
+      <unit id="org.hamcrest.library.source" version="1.3.0.v20230809-1000"/>
+      <unit id="org.junit" version="4.13.2.v20240929-1000"/>
+      <unit id="org.junit.source" version="4.13.2.v20240929-1000"/>
+      <unit id="org.objenesis" version="3.4.0"/>
+      <unit id="org.objenesis.source" version="3.4.0"/>
+      <unit id="org.osgi.service.cm" version="1.6.1.202109301733"/>
+      <unit id="org.osgi.service.cm.source" version="1.6.1.202109301733"/>
+      <repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2025-03"/>
+    </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/2025-03/"/>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="xz">
+      <dependencies>
+        <dependency>
+          <groupId>org.tukaani</groupId>
+          <artifactId>xz</artifactId>
+          <version>1.10</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="slf4j">
+      <dependencies>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-api</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.slf4j</groupId>
+          <artifactId>slf4j-simple</artifactId>
+          <version>1.7.36</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="sshd">
+      <dependencies>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-osgi</artifactId>
+          <version>2.15.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.sshd</groupId>
+          <artifactId>sshd-sftp</artifactId>
+          <version>2.15.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="mockito">
+      <dependencies>
+        <dependency>
+          <groupId>org.mockito</groupId>
+          <artifactId>mockito-core</artifactId>
+          <version>5.15.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jna">
+      <dependencies>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna</artifactId>
+          <version>5.16.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.java.dev.jna</groupId>
+          <artifactId>jna-platform</artifactId>
+          <version>5.16.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="jetty">
+      <dependencies>
+        <dependency>
+          <groupId>org.eclipse.jetty.ee10</groupId>
+          <artifactId>jetty-ee10-servlet</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-http</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-io</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-security</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-server</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-session</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-util-ajax</artifactId>
+          <version>12.0.16</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>jakarta.servlet</groupId>
+          <artifactId>jakarta.servlet-api</artifactId>
+          <version>6.1.0</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="javaewah">
+      <dependencies>
+        <dependency>
+          <groupId>com.googlecode.javaewah</groupId>
+          <artifactId>JavaEWAH</artifactId>
+          <version>1.2.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="hamcrest">
+      <dependencies>
+        <dependency>
+          <groupId>org.hamcrest</groupId>
+          <artifactId>hamcrest</artifactId>
+          <version>2.2</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="gson">
+      <dependencies>
+        <dependency>
+          <groupId>com.google.code.gson</groupId>
+          <artifactId>gson</artifactId>
+          <version>2.12.1</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bytebuddy">
+      <dependencies>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy</artifactId>
+          <version>1.17.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>net.bytebuddy</groupId>
+          <artifactId>byte-buddy-agent</artifactId>
+          <version>1.17.1</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="bouncycastle">
+      <dependencies>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpg-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpkix-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcutil-jdk18on</artifactId>
+          <version>1.80</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="assertj">
+      <dependencies>
+        <dependency>
+          <groupId>org.assertj</groupId>
+          <artifactId>assertj-core</artifactId>
+          <version>3.27.3</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="args4j">
+      <dependencies>
+        <dependency>
+          <groupId>args4j</groupId>
+          <artifactId>args4j</artifactId>
+          <version>2.37</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+    <location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="error" type="Maven" label="apache">
+      <dependencies>
+        <dependency>
+          <groupId>commons-codec</groupId>
+          <artifactId>commons-codec</artifactId>
+          <version>1.18.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-compress</artifactId>
+          <version>1.27.1</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.commons</groupId>
+          <artifactId>commons-lang3</artifactId>
+          <version>3.17.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-io</groupId>
+          <artifactId>commons-io</artifactId>
+          <version>2.18.0</version>
+          <type>jar</type>
+        </dependency>
+        <dependency>
+          <groupId>commons-logging</groupId>
+          <artifactId>commons-logging</artifactId>
+          <version>1.3.5</version>
+          <type>jar</type>
+        </dependency>
+      </dependencies>
+    </location>
+  </locations>
+</target>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.35.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.35.tpd
new file mode 100644
index 0000000..3c0646e
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.35.tpd
@@ -0,0 +1,8 @@
+target "jgit-4.35" with source configurePhase
+
+include "orbit/orbit-4.35.tpd"
+include "maven/dependencies.tpd"
+
+location "https://download.eclipse.org/staging/2025-03/" {
+	org.eclipse.osgi lazy
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/maven/dependencies.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/maven/dependencies.tpd
index a25f9c9..b292cf5 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/maven/dependencies.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/maven/dependencies.tpd
@@ -10,22 +10,27 @@
 	dependency {
 		groupId = "commons-codec"
 		artifactId = "commons-codec"
-		version = "1.16.0"
+		version = "1.18.0"
 	}
 	dependency {
 		groupId = "org.apache.commons"
 		artifactId = "commons-compress"
-		version = "1.26.0"
+		version = "1.27.1"
+	}
+	dependency {
+		groupId = "org.apache.commons"
+		artifactId = "commons-lang3"
+		version = "3.17.0"
 	}
 	dependency {
 		groupId = "commons-io"
 		artifactId = "commons-io"
-		version = "2.15.1"
+		version = "2.18.0"
 	}
 	dependency {
 		groupId = "commons-logging"
 		artifactId = "commons-logging"
-		version = "1.2"
+		version = "1.3.5"
 	}
 }
 
@@ -38,7 +43,7 @@
 	dependency {
 		groupId = "args4j"
 		artifactId = "args4j"
-		version = "2.33"
+		version = "2.37"
 	}
 }
 
@@ -51,7 +56,7 @@
 	dependency {
 		groupId = "org.assertj"
 		artifactId = "assertj-core"
-		version = "3.25.3"
+		version = "3.27.3"
 	}
 }
 
@@ -64,22 +69,22 @@
 	dependency {
 		groupId = "org.bouncycastle"
 		artifactId = "bcpg-jdk18on"
-		version = "1.77"
+		version = "1.80"
 	}
 	dependency {
 		groupId = "org.bouncycastle"
 		artifactId = "bcprov-jdk18on"
-		version = "1.77"
+		version = "1.80"
 	}
 	dependency {
 		groupId = "org.bouncycastle"
 		artifactId = "bcpkix-jdk18on"
-		version = "1.77"
+		version = "1.80"
 	}
 	dependency {
 		groupId = "org.bouncycastle"
 		artifactId = "bcutil-jdk18on"
-		version = "1.77"
+		version = "1.80"
 	}
 }
 
@@ -92,12 +97,12 @@
 	dependency {
 		groupId = "net.bytebuddy"
 		artifactId = "byte-buddy"
-		version = "1.14.12"
+		version = "1.17.1"
 	}
 	dependency {
 		groupId = "net.bytebuddy"
 		artifactId = "byte-buddy-agent"
-		version = "1.14.12"
+		version = "1.17.1"
 	}
 }
 
@@ -110,7 +115,7 @@
 	dependency {
 		groupId = "com.google.code.gson"
 		artifactId = "gson"
-		version = "2.10.1"
+		version = "2.12.1"
 	}
 }
 
@@ -149,47 +154,47 @@
 	dependency {
 		groupId = "org.eclipse.jetty.ee10"
 		artifactId = "jetty-ee10-servlet"
-		version = "12.0.9"
+		version = "12.0.16"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-http"
-		version = "12.0.9"
+		version = "12.0.16"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-io"
-		version = "12.0.9"
+		version = "12.0.16"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-security"
-		version = "12.0.9"
+		version = "12.0.16"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-server"
-		version = "12.0.9"
+		version = "12.0.16"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-session"
-		version = "12.0.9"
+		version = "12.0.16"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-util"
-		version = "12.0.9"
+		version = "12.0.16"
 	}
 	dependency {
 		groupId = "org.eclipse.jetty"
 		artifactId = "jetty-util-ajax"
-		version = "12.0.9"
+		version = "12.0.16"
 	}
 	dependency {
 		groupId = "jakarta.servlet"
 		artifactId = "jakarta.servlet-api"
-		version = "6.0.0"
+		version = "6.1.0"
 	}
 }
 
@@ -202,12 +207,12 @@
 	dependency {
 		groupId = "net.java.dev.jna"
 		artifactId = "jna"
-		version = "5.14.0"
+		version = "5.16.0"
 	}
 	dependency {
 		groupId = "net.java.dev.jna"
 		artifactId = "jna-platform"
-		version = "5.14.0"
+		version = "5.16.0"
 	}
 }
 
@@ -220,7 +225,7 @@
 	dependency {
 		groupId = "org.mockito"
 		artifactId = "mockito-core"
-		version = "5.10.0"
+		version = "5.15.2"
 	}
 }
 
@@ -233,12 +238,12 @@
 	dependency {
 		groupId = "org.apache.sshd"
 		artifactId = "sshd-osgi"
-		version = "2.12.0"
+		version = "2.15.0"
 	}
 	dependency {
 		groupId = "org.apache.sshd"
 		artifactId = "sshd-sftp"
-		version = "2.12.0"
+		version = "2.15.0"
 	}
 }
 
@@ -269,6 +274,6 @@
 	dependency {
 		groupId = "org.tukaani"
 		artifactId = "xz"
-		version = "1.9"
+		version = "1.10"
 	}
 }
\ No newline at end of file
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20200831200620-2020-09.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20200831200620-2020-09.tpd
deleted file mode 100644
index 22e2b01..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20200831200620-2020-09.tpd
+++ /dev/null
@@ -1,66 +0,0 @@
-target "R20200831200620-2020-09" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20200831200620/repository" {
-	com.google.gson [2.8.2.v20180104-1110,2.8.2.v20180104-1110]
-	com.google.gson.source [2.8.2.v20180104-1110,2.8.2.v20180104-1110]
-	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.8.v20200515-1239,1.10.8.v20200515-1239]
-	org.apache.ant.source [1.10.8.v20200515-1239,1.10.8.v20200515-1239]
-	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.10.v20200830-2311,4.5.10.v20200830-2311]
-	org.apache.httpcomponents.httpclient.source [4.5.10.v20200830-2311,4.5.10.v20200830-2311]
-	org.apache.httpcomponents.httpcore [4.4.12.v20200108-1212,4.4.12.v20200108-1212]
-	org.apache.httpcomponents.httpcore.source [4.4.12.v20200108-1212,4.4.12.v20200108-1212]
-	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.4.0.v20200318-1614,2.4.0.v20200318-1614]
-	org.apache.sshd.osgi.source [2.4.0.v20200318-1614,2.4.0.v20200318-1614]
-	org.apache.sshd.sftp [2.4.0.v20200319-1547,2.4.0.v20200319-1547]
-	org.apache.sshd.sftp.source [2.4.0.v20200319-1547,2.4.0.v20200319-1547]
-	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.2.v20121108-1250,1.7.2.v20121108-1250]
-	org.slf4j.api.source [1.7.2.v20121108-1250,1.7.2.v20121108-1250]
-	org.slf4j.impl.log4j12 [1.7.2.v20131105-2200,1.7.2.v20131105-2200]
-	org.slf4j.impl.log4j12.source [1.7.2.v20131105-2200,1.7.2.v20131105-2200]
-	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/orbit/R20201130205003-2020-12.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20201130205003-2020-12.tpd
deleted file mode 100644
index 08a0846..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20201130205003-2020-12.tpd
+++ /dev/null
@@ -1,66 +0,0 @@
-target "R20201130205003-2020-12" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20201130205003/repository" {
-	com.google.gson [2.8.2.v20180104-1110,2.8.2.v20180104-1110]
-	com.google.gson.source [2.8.2.v20180104-1110,2.8.2.v20180104-1110]
-	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.10.v20200830-2311,4.5.10.v20200830-2311]
-	org.apache.httpcomponents.httpclient.source [4.5.10.v20200830-2311,4.5.10.v20200830-2311]
-	org.apache.httpcomponents.httpcore [4.4.12.v20200108-1212,4.4.12.v20200108-1212]
-	org.apache.httpcomponents.httpcore.source [4.4.12.v20200108-1212,4.4.12.v20200108-1212]
-	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.4.0.v20200318-1614,2.4.0.v20200318-1614]
-	org.apache.sshd.osgi.source [2.4.0.v20200318-1614,2.4.0.v20200318-1614]
-	org.apache.sshd.sftp [2.4.0.v20200319-1547,2.4.0.v20200319-1547]
-	org.apache.sshd.sftp.source [2.4.0.v20200319-1547,2.4.0.v20200319-1547]
-	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/orbit/R20210223232630-2021-03.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20210223232630-2021-03.tpd
deleted file mode 100644
index 605a43b..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20210223232630-2021-03.tpd
+++ /dev/null
@@ -1,66 +0,0 @@
-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/orbit/R20210602031627-2021-06.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20210602031627-2021-06.tpd
deleted file mode 100644
index 83b5bb3..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20210602031627-2021-06.tpd
+++ /dev/null
@@ -1,66 +0,0 @@
-target "R20210602031627-2021-06" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20210602031627/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.10.v20210426-1926,1.10.10.v20210426-1926]
-	org.apache.ant.source [1.10.10.v20210426-1926,1.10.10.v20210426-1926]
-	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/orbit/R20210825222808-2021-09.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20210825222808-2021-09.tpd
deleted file mode 100644
index 99f3520..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20210825222808-2021-09.tpd
+++ /dev/null
@@ -1,73 +0,0 @@
-target "R20210825222808-2021-09" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20210825222808/repository" {
-	com.google.gson [2.8.7.v20210624-1215,2.8.7.v20210624-1215]
-	com.google.gson.source [2.8.7.v20210624-1215,2.8.7.v20210624-1215]
-	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]
-	com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	javaewah [1.1.12.v20210622-2206,1.1.12.v20210622-2206]
-	javaewah.source [1.1.12.v20210622-2206,1.1.12.v20210622-2206]
-	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.11.v20210720-1445,1.10.11.v20210720-1445]
-	org.apache.ant.source [1.10.11.v20210720-1445,1.10.11.v20210720-1445]
-	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.20.0.v20210713-1928,1.20.0.v20210713-1928]
-	org.apache.commons.compress.source [1.20.0.v20210713-1928,1.20.0.v20210713-1928]
-	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.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.apache.sshd.osgi.source [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.apache.sshd.sftp [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.apache.sshd.sftp.source [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.bouncycastle.bcpg [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcpg.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcpkix [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcpkix.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcprov [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcprov.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcutil [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcutil.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.junit [4.13.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.9.0.v20210624-1259,1.9.0.v20210624-1259]
-	org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211122181901-2021-12.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211122181901-2021-12.tpd
deleted file mode 100644
index cd1d1c0..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211122181901-2021-12.tpd
+++ /dev/null
@@ -1,71 +0,0 @@
-target "R20211122181901-2021-12" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20211122181901/repository" {
-	com.google.gson [2.8.8.v20211029-0838,2.8.8.v20211029-0838]
-	com.google.gson.source [2.8.8.v20211029-0838,2.8.8.v20211029-0838]
-	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]
-	com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	net.bytebuddy.byte-buddy [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.bytebuddy.byte-buddy-agent [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy-agent.source [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy.source [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.i2p.crypto.eddsa [0.3.0.v20210923-1401,0.3.0.v20210923-1401]
-	net.i2p.crypto.eddsa.source [0.3.0.v20210923-1401,0.3.0.v20210923-1401]
-	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.commons.codec [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.codec.source [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.httpcomponents.httpclient [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpclient.source [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpcore [4.4.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.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.apache.sshd.osgi.source [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.apache.sshd.sftp [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.apache.sshd.sftp.source [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.bouncycastle.bcpg [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcpg.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcpkix [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcpkix.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcprov [1.69.0.v20210923-1401,1.69.0.v20210923-1401]
-	org.bouncycastle.bcprov.source [1.69.0.v20210923-1401,1.69.0.v20210923-1401]
-	org.bouncycastle.bcutil [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcutil.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.mockito [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.mockito.source [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.objenesis [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.objenesis.source [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.slf4j.api [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.api.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.binding.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.9.0.v20210624-1259,1.9.0.v20210624-1259]
-	org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211213173813-2021-12.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211213173813-2021-12.tpd
deleted file mode 100644
index 0c7c846..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211213173813-2021-12.tpd
+++ /dev/null
@@ -1,69 +0,0 @@
-target "R20211213173813-2021-12" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20211213173813/repository" {
-	com.google.gson [2.8.8.v20211029-0838,2.8.8.v20211029-0838]
-	com.google.gson.source [2.8.8.v20211029-0838,2.8.8.v20211029-0838]
-	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]
-	com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	net.bytebuddy.byte-buddy [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.bytebuddy.byte-buddy-agent [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy-agent.source [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy.source [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.i2p.crypto.eddsa [0.3.0.v20210923-1401,0.3.0.v20210923-1401]
-	net.i2p.crypto.eddsa.source [0.3.0.v20210923-1401,0.3.0.v20210923-1401]
-	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.commons.codec [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.codec.source [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.httpcomponents.httpclient [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpclient.source [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpcore [4.4.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.sshd.osgi [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.apache.sshd.osgi.source [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.apache.sshd.sftp [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.apache.sshd.sftp.source [2.7.0.v20210623-0618,2.7.0.v20210623-0618]
-	org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.bouncycastle.bcpg [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcpg.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcpkix [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcpkix.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcprov [1.69.0.v20210923-1401,1.69.0.v20210923-1401]
-	org.bouncycastle.bcprov.source [1.69.0.v20210923-1401,1.69.0.v20210923-1401]
-	org.bouncycastle.bcutil [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.bouncycastle.bcutil.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924]
-	org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.mockito [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.mockito.source [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.objenesis [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.objenesis.source [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.slf4j.api [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.api.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.binding.simple [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.binding.simple.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.tukaani.xz [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-	org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220302172233-2022-03.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220302172233-2022-03.tpd
deleted file mode 100644
index fafc689..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220302172233-2022-03.tpd
+++ /dev/null
@@ -1,69 +0,0 @@
-target "R20220302172233" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20220302172233/repository" {
-	com.google.gson [2.8.9.v20220111-1409,2.8.9.v20220111-1409]
-	com.google.gson.source [2.8.9.v20220111-1409,2.8.9.v20220111-1409]
-	com.jcraft.jsch [0.1.55.v20190404-1902,0.1.55.v20190404-1902]
-	com.jcraft.jsch.source [0.1.55.v20190404-1902,0.1.55.v20190404-1902]
-	com.jcraft.jzlib [1.1.1.v201205102305,1.1.1.v201205102305]
-	com.jcraft.jzlib.source [1.1.1.v201205102305,1.1.1.v201205102305]
-	com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	net.bytebuddy.byte-buddy [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.bytebuddy.byte-buddy-agent [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy-agent.source [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy.source [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.i2p.crypto.eddsa [0.3.0.v20210923-1401,0.3.0.v20210923-1401]
-	net.i2p.crypto.eddsa.source [0.3.0.v20210923-1401,0.3.0.v20210923-1401]
-	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.commons.codec [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.codec.source [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.httpcomponents.httpclient [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpclient.source [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpcore [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
-	org.apache.httpcomponents.httpcore.source [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
-	org.apache.sshd.osgi [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.apache.sshd.osgi.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.apache.sshd.sftp [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.apache.sshd.sftp.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.bouncycastle.bcpg [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcpg.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcpkix [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcpkix.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcprov [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcprov.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcutil [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcutil.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.mockito [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.mockito.source [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.objenesis [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.objenesis.source [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.slf4j.api [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.api.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.binding.simple [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.binding.simple.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.tukaani.xz [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-	org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd
deleted file mode 100644
index 3c74497..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd
+++ /dev/null
@@ -1,69 +0,0 @@
-target "R20220531185310-2022-06" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository" {
-	com.google.gson [2.8.9.v20220111-1409,2.8.9.v20220111-1409]
-	com.google.gson.source [2.8.9.v20220111-1409,2.8.9.v20220111-1409]
-	com.jcraft.jsch [0.1.55.v20190404-1902,0.1.55.v20190404-1902]
-	com.jcraft.jsch.source [0.1.55.v20190404-1902,0.1.55.v20190404-1902]
-	com.jcraft.jzlib [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	com.jcraft.jzlib.source [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	net.bytebuddy.byte-buddy [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.bytebuddy.byte-buddy-agent [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy-agent.source [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy.source [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.i2p.crypto.eddsa [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	net.i2p.crypto.eddsa.source [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.commons.codec [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.codec.source [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.httpcomponents.httpclient [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpclient.source [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpcore [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
-	org.apache.httpcomponents.httpcore.source [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
-	org.apache.sshd.osgi [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.apache.sshd.osgi.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.apache.sshd.sftp [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.apache.sshd.sftp.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.bouncycastle.bcpg [1.70.0.v20220507-1208,1.70.0.v20220507-1208]
-	org.bouncycastle.bcpg.source [1.70.0.v20220507-1208,1.70.0.v20220507-1208]
-	org.bouncycastle.bcpkix [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcpkix.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcprov [1.70.0.v20220507-1208,1.70.0.v20220507-1208]
-	org.bouncycastle.bcprov.source [1.70.0.v20220507-1208,1.70.0.v20220507-1208]
-	org.bouncycastle.bcutil [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.bouncycastle.bcutil.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522]
-	org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.mockito [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.mockito.source [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.objenesis [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.objenesis.source [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.slf4j.api [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.api.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.binding.simple [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.binding.simple.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.tukaani.xz [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-	org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220830213456-2022-09.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220830213456-2022-09.tpd
deleted file mode 100644
index 8db1018..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220830213456-2022-09.tpd
+++ /dev/null
@@ -1,69 +0,0 @@
-target "R20220830213456-2022-09" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20220830213456/repository" {
-	com.google.gson [2.8.9.v20220111-1409,2.8.9.v20220111-1409]
-	com.google.gson.source [2.8.9.v20220111-1409,2.8.9.v20220111-1409]
-	com.jcraft.jsch [0.1.55.v20190404-1902,0.1.55.v20190404-1902]
-	com.jcraft.jsch.source [0.1.55.v20190404-1902,0.1.55.v20190404-1902]
-	com.jcraft.jzlib [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	com.jcraft.jzlib.source [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343]
-	com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004]
-	javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	net.bytebuddy.byte-buddy [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.bytebuddy.byte-buddy-agent [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy-agent.source [1.9.0.v20181106-1534,1.9.0.v20181106-1534]
-	net.bytebuddy.byte-buddy.source [1.9.0.v20181107-1410,1.9.0.v20181107-1410]
-	net.i2p.crypto.eddsa [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	net.i2p.crypto.eddsa.source [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.commons.codec [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.codec.source [1.14.0.v20200818-1422,1.14.0.v20200818-1422]
-	org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.httpcomponents.httpclient [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpclient.source [4.5.13.v20210128-2225,4.5.13.v20210128-2225]
-	org.apache.httpcomponents.httpcore [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
-	org.apache.httpcomponents.httpcore.source [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
-	org.apache.sshd.osgi [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.apache.sshd.osgi.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.apache.sshd.sftp [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.apache.sshd.sftp.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750]
-	org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.bouncycastle.bcpg [1.71.0.v20220723-1943,1.71.0.v20220723-1943]
-	org.bouncycastle.bcpg.source [1.71.0.v20220723-1943,1.71.0.v20220723-1943]
-	org.bouncycastle.bcpkix [1.71.0.v20220723-1943,1.71.0.v20220723-1943]
-	org.bouncycastle.bcpkix.source [1.71.0.v20220723-1943,1.71.0.v20220723-1943]
-	org.bouncycastle.bcprov [1.71.0.v20220723-1943,1.71.0.v20220723-1943]
-	org.bouncycastle.bcprov.source [1.71.0.v20220723-1943,1.71.0.v20220723-1943]
-	org.bouncycastle.bcutil [1.71.0.v20220723-1943,1.71.0.v20220723-1943]
-	org.bouncycastle.bcutil.source [1.71.0.v20220723-1943,1.71.0.v20220723-1943]
-	org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.mockito [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.mockito.source [2.23.0.v20200310-1642,2.23.0.v20200310-1642]
-	org.objenesis [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.objenesis.source [2.6.0.v20180420-1519,2.6.0.v20180420-1519]
-	org.slf4j.api [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.api.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.binding.simple [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.slf4j.binding.simple.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150]
-	org.tukaani.xz [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-	org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20221123021534-2022-12.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20221123021534-2022-12.tpd
deleted file mode 100644
index 378b848..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20221123021534-2022-12.tpd
+++ /dev/null
@@ -1,69 +0,0 @@
-target "S20230101190934" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20221123021534/repository" {
-	com.google.gson [2.9.1.v20220915-1632,2.9.1.v20220915-1632]
-	com.google.gson.source [2.9.1.v20220915-1632,2.9.1.v20220915-1632]
-	com.jcraft.jsch [0.1.55.v20221112-0806,0.1.55.v20221112-0806]
-	com.jcraft.jsch.source [0.1.55.v20221112-0806,0.1.55.v20221112-0806]
-	com.jcraft.jzlib [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	com.jcraft.jzlib.source [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	com.sun.jna [5.12.1.v20221103-2317,5.12.1.v20221103-2317]
-	com.sun.jna.source [5.12.1.v20221103-2317,5.12.1.v20221103-2317]
-	com.sun.jna.platform [5.12.1.v20221103-2317,5.12.1.v20221103-2317]
-	com.sun.jna.platform.source [5.12.1.v20221103-2317,5.12.1.v20221103-2317]
-	javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839]
-	net.bytebuddy.byte-buddy [1.12.18.v20221114-2102,1.12.18.v20221114-2102]
-	net.bytebuddy.byte-buddy.source [1.12.18.v20221114-2102,1.12.18.v20221114-2102]
-	net.bytebuddy.byte-buddy-agent [1.12.18.v20221114-2102,1.12.18.v20221114-2102]
-	net.bytebuddy.byte-buddy-agent.source [1.12.18.v20221114-2102,1.12.18.v20221114-2102]
-	net.i2p.crypto.eddsa [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	net.i2p.crypto.eddsa.source [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.commons.codec [1.14.0.v20221112-0806,1.14.0.v20221112-0806]
-	org.apache.commons.codec.source [1.14.0.v20221112-0806,1.14.0.v20221112-0806]
-	org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100]
-	org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502]
-	org.apache.httpcomponents.httpclient [4.5.13.v20221112-0806,4.5.13.v20221112-0806]
-	org.apache.httpcomponents.httpclient.source [4.5.13.v20221112-0806,4.5.13.v20221112-0806]
-	org.apache.httpcomponents.httpcore [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
-	org.apache.httpcomponents.httpcore.source [4.4.15.v20220209-2345,4.4.15.v20220209-2345]
-	org.apache.sshd.osgi [2.9.2.v20221117-1942,2.9.2.v20221117-1942]
-	org.apache.sshd.osgi.source [2.9.2.v20221117-1942,2.9.2.v20221117-1942]
-	org.apache.sshd.sftp [2.9.2.v20221117-1942,2.9.2.v20221117-1942]
-	org.apache.sshd.sftp.source [2.9.2.v20221117-1942,2.9.2.v20221117-1942]
-	org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104]
-	org.bouncycastle.bcpg [1.72.0.v20221013-1810,1.72.0.v20221013-1810]
-	org.bouncycastle.bcpg.source [1.72.0.v20221013-1810,1.72.0.v20221013-1810]
-	org.bouncycastle.bcpkix [1.72.0.v20221013-1810,1.72.0.v20221013-1810]
-	org.bouncycastle.bcpkix.source [1.72.0.v20221013-1810,1.72.0.v20221013-1810]
-	org.bouncycastle.bcprov [1.72.0.v20221013-1810,1.72.0.v20221013-1810]
-	org.bouncycastle.bcprov.source [1.72.0.v20221013-1810,1.72.0.v20221013-1810]
-	org.bouncycastle.bcutil [1.72.0.v20221013-1810,1.72.0.v20221013-1810]
-	org.bouncycastle.bcutil.source [1.72.0.v20221013-1810,1.72.0.v20221013-1810]
-	org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821]
-	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218]
-	org.mockito.mockito-core [4.8.1.v20221103-2317,4.8.1.v20221103-2317]
-	org.mockito.mockito-core.source [4.8.1.v20221103-2317,4.8.1.v20221103-2317]
-	org.objenesis [3.3.0.v20221103-2317,3.3.0.v20221103-2317]
-	org.objenesis.source [3.3.0.v20221103-2317,3.3.0.v20221103-2317]
-	org.slf4j.api [1.7.30.v20221112-0806,1.7.30.v20221112-0806]
-	org.slf4j.api.source [1.7.30.v20221112-0806,1.7.30.v20221112-0806]
-	org.slf4j.binding.simple [1.7.30.v20221112-0806,1.7.30.v20221112-0806]
-	org.slf4j.binding.simple.source [1.7.30.v20221112-0806,1.7.30.v20221112-0806]
-	org.tukaani.xz [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-	org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20230302014618-2023-03.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20230302014618-2023-03.tpd
deleted file mode 100644
index 8578b2c..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20230302014618-2023-03.tpd
+++ /dev/null
@@ -1,27 +0,0 @@
-target "R20230302014618-2023-03" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20230302014618/repository" {
-	com.jcraft.jsch [0.1.55.v20221112-0806,0.1.55.v20221112-0806]
-	com.jcraft.jsch.source [0.1.55.v20221112-0806,0.1.55.v20221112-0806]
-	com.jcraft.jzlib [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	com.jcraft.jzlib.source [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	net.i2p.crypto.eddsa [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	net.i2p.crypto.eddsa.source [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.httpcomponents.httpclient [4.5.14.v20221207-1049,4.5.14.v20221207-1049]
-	org.apache.httpcomponents.httpclient.source [4.5.14.v20221207-1049,4.5.14.v20221207-1049]
-	org.apache.httpcomponents.httpcore [4.4.16.v20221207-1049,4.4.16.v20221207-1049]
-	org.apache.httpcomponents.httpcore.source [4.4.16.v20221207-1049,4.4.16.v20221207-1049]
-	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.mockito.mockito-core [4.8.1.v20221103-2317,4.8.1.v20221103-2317]
-	org.mockito.mockito-core.source [4.8.1.v20221103-2317,4.8.1.v20221103-2317]
-	org.objenesis [3.3.0.v20221103-2317,3.3.0.v20221103-2317]
-	org.objenesis.source [3.3.0.v20221103-2317,3.3.0.v20221103-2317]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20230531010532-2023-06.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20230531010532-2023-06.tpd
deleted file mode 100644
index 46055d3..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20230531010532-2023-06.tpd
+++ /dev/null
@@ -1,25 +0,0 @@
-target "R20230531010532-2023-06" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/downloads/drops/R20230531010532/repository" {
-	com.jcraft.jsch [0.1.55.v20221112-0806,0.1.55.v20221112-0806]
-	com.jcraft.jsch.source [0.1.55.v20221112-0806,0.1.55.v20221112-0806]
-	com.jcraft.jzlib [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	com.jcraft.jzlib.source [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	net.i2p.crypto.eddsa [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	net.i2p.crypto.eddsa.source [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.httpcomponents.httpclient [4.5.14.v20230516-1249,4.5.14.v20230516-1249]
-	org.apache.httpcomponents.httpclient.source [4.5.14.v20230516-1249,4.5.14.v20230516-1249]
-	org.apache.httpcomponents.httpcore [4.4.16.v20221207-1049,4.4.16.v20221207-1049]
-	org.apache.httpcomponents.httpcore.source [4.4.16.v20221207-1049,4.4.16.v20221207-1049]
-	org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519]
-	org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246]
-	org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956]
-	org.objenesis [3.3.0.v20221103-2317,3.3.0.v20221103-2317]
-	org.objenesis.source [3.3.0.v20221103-2317,3.3.0.v20221103-2317]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.29.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.29.tpd
deleted file mode 100644
index 70a17a1..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.29.tpd
+++ /dev/null
@@ -1,27 +0,0 @@
-target "orbit-4.29" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/release/4.29.0" {
-	com.jcraft.jsch [0.1.55.v20221112-0806,0.1.55.v20221112-0806]
-	com.jcraft.jsch.source [0.1.55.v20221112-0806,0.1.55.v20221112-0806]
-	com.jcraft.jzlib [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	com.jcraft.jzlib.source [1.1.3.v20220502-1820,1.1.3.v20220502-1820]
-	net.i2p.crypto.eddsa [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	net.i2p.crypto.eddsa.source [0.3.0.v20220506-1020,0.3.0.v20220506-1020]
-	org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452]
-	org.apache.httpcomponents.httpclient [4.5.14,4.5.14]
-	org.apache.httpcomponents.httpclient.source [4.5.14,4.5.14]
-	org.apache.httpcomponents.httpcore [4.4.16,4.4.16]
-	org.apache.httpcomponents.httpcore.source [4.4.16,4.4.16]
-	org.hamcrest.core [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.hamcrest.core.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.hamcrest.library [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.hamcrest.library.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.junit [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.junit.source [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.objenesis [3.3,3.3]
-	org.objenesis.source [3.3,3.3]
-	org.osgi.service.cm [1.6.1.202109301733,1.6.1.202109301733]
-	org.osgi.service.cm.source [1.6.1.202109301733,1.6.1.202109301733]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.31.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.31.tpd
deleted file mode 100644
index 0554a85..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.31.tpd
+++ /dev/null
@@ -1,27 +0,0 @@
-target "orbit-4.30" with source configurePhase
-// see https://download.eclipse.org/tools/orbit/downloads/
-
-location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12" {
-	com.jcraft.jsch [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
-	com.jcraft.jsch.source [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
-	com.jcraft.jzlib [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
-	com.jcraft.jzlib.source [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
-	net.i2p.crypto.eddsa [0.3.0,0.3.0]
-	net.i2p.crypto.eddsa.source [0.3.0,0.3.0]
-	org.apache.ant [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
-	org.apache.ant.source [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
-	org.apache.httpcomponents.httpclient [4.5.14,4.5.14]
-	org.apache.httpcomponents.httpclient.source [4.5.14,4.5.14]
-	org.apache.httpcomponents.httpcore [4.4.16,4.4.16]
-	org.apache.httpcomponents.httpcore.source [4.4.16,4.4.16]
-	org.hamcrest.core [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.hamcrest.core.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.hamcrest.library [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.hamcrest.library.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.junit [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.junit.source [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.objenesis [3.3,3.3]
-	org.objenesis.source [3.3,3.3]
-	org.osgi.service.cm [1.6.1.202109301733,1.6.1.202109301733]
-	org.osgi.service.cm.source [1.6.1.202109301733,1.6.1.202109301733]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.32.tpd
similarity index 85%
copy from org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd
copy to org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.32.tpd
index 0554a85..59fcd87 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.32.tpd
@@ -1,13 +1,11 @@
-target "orbit-4.30" with source configurePhase
+target "orbit-4.32" with source configurePhase
 // see https://download.eclipse.org/tools/orbit/downloads/
 
-location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12" {
+location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2024-06" {
 	com.jcraft.jsch [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
 	com.jcraft.jsch.source [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
 	com.jcraft.jzlib [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
 	com.jcraft.jzlib.source [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
-	net.i2p.crypto.eddsa [0.3.0,0.3.0]
-	net.i2p.crypto.eddsa.source [0.3.0,0.3.0]
 	org.apache.ant [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
 	org.apache.ant.source [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
 	org.apache.httpcomponents.httpclient [4.5.14,4.5.14]
@@ -20,8 +18,8 @@
 	org.hamcrest.library.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
 	org.junit [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
 	org.junit.source [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.objenesis [3.3,3.3]
-	org.objenesis.source [3.3,3.3]
+	org.objenesis [3.4,3.4]
+	org.objenesis.source [3.4,3.4]
 	org.osgi.service.cm [1.6.1.202109301733,1.6.1.202109301733]
 	org.osgi.service.cm.source [1.6.1.202109301733,1.6.1.202109301733]
 }
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.33.tpd
similarity index 85%
rename from org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd
rename to org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.33.tpd
index 0554a85..2cfa0a8 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.33.tpd
@@ -1,13 +1,11 @@
-target "orbit-4.30" with source configurePhase
+target "orbit-4.33" with source configurePhase
 // see https://download.eclipse.org/tools/orbit/downloads/
 
-location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12" {
+location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2024-09" {
 	com.jcraft.jsch [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
 	com.jcraft.jsch.source [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
 	com.jcraft.jzlib [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
 	com.jcraft.jzlib.source [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
-	net.i2p.crypto.eddsa [0.3.0,0.3.0]
-	net.i2p.crypto.eddsa.source [0.3.0,0.3.0]
 	org.apache.ant [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
 	org.apache.ant.source [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
 	org.apache.httpcomponents.httpclient [4.5.14,4.5.14]
@@ -20,8 +18,8 @@
 	org.hamcrest.library.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
 	org.junit [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
 	org.junit.source [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.objenesis [3.3,3.3]
-	org.objenesis.source [3.3,3.3]
+	org.objenesis [3.4,3.4]
+	org.objenesis.source [3.4,3.4]
 	org.osgi.service.cm [1.6.1.202109301733,1.6.1.202109301733]
 	org.osgi.service.cm.source [1.6.1.202109301733,1.6.1.202109301733]
 }
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.34.tpd
similarity index 68%
copy from org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd
copy to org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.34.tpd
index 0554a85..d3e15bb 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.34.tpd
@@ -1,15 +1,13 @@
-target "orbit-4.30" with source configurePhase
+target "orbit-4.34" with source configurePhase
 // see https://download.eclipse.org/tools/orbit/downloads/
 
-location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12" {
+location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2024-12" {
 	com.jcraft.jsch [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
 	com.jcraft.jsch.source [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
 	com.jcraft.jzlib [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
 	com.jcraft.jzlib.source [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
-	net.i2p.crypto.eddsa [0.3.0,0.3.0]
-	net.i2p.crypto.eddsa.source [0.3.0,0.3.0]
-	org.apache.ant [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
-	org.apache.ant.source [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
+	org.apache.ant [1.10.15.v20240901-1000,1.10.15.v20240901-1000]
+	org.apache.ant.source [1.10.15.v20240901-1000,1.10.15.v20240901-1000]
 	org.apache.httpcomponents.httpclient [4.5.14,4.5.14]
 	org.apache.httpcomponents.httpclient.source [4.5.14,4.5.14]
 	org.apache.httpcomponents.httpcore [4.4.16,4.4.16]
@@ -18,10 +16,10 @@
 	org.hamcrest.core.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
 	org.hamcrest.library [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
 	org.hamcrest.library.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.junit [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.junit.source [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.objenesis [3.3,3.3]
-	org.objenesis.source [3.3,3.3]
+	org.junit [4.13.2.v20240929-1000,4.13.2.v20240929-1000]
+	org.junit.source [4.13.2.v20240929-1000,4.13.2.v20240929-1000]
+	org.objenesis [3.4,3.4]
+	org.objenesis.source [3.4,3.4]
 	org.osgi.service.cm [1.6.1.202109301733,1.6.1.202109301733]
 	org.osgi.service.cm.source [1.6.1.202109301733,1.6.1.202109301733]
 }
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.35.tpd
similarity index 68%
copy from org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd
copy to org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.35.tpd
index 0554a85..ec6996e 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.30.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/orbit-4.35.tpd
@@ -1,15 +1,13 @@
-target "orbit-4.30" with source configurePhase
+target "orbit-4.35" with source configurePhase
 // see https://download.eclipse.org/tools/orbit/downloads/
 
-location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2023-12" {
+location "https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2025-03" {
 	com.jcraft.jsch [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
 	com.jcraft.jsch.source [0.1.55.v20230916-1400,0.1.55.v20230916-1400]
 	com.jcraft.jzlib [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
 	com.jcraft.jzlib.source [1.1.3.v20230916-1400,1.1.3.v20230916-1400]
-	net.i2p.crypto.eddsa [0.3.0,0.3.0]
-	net.i2p.crypto.eddsa.source [0.3.0,0.3.0]
-	org.apache.ant [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
-	org.apache.ant.source [1.10.14.v20230922-1200,1.10.14.v20230922-1200]
+	org.apache.ant [1.10.15.v20240901-1000,1.10.15.v20240901-1000]
+	org.apache.ant.source [1.10.15.v20240901-1000,1.10.15.v20240901-1000]
 	org.apache.httpcomponents.httpclient [4.5.14,4.5.14]
 	org.apache.httpcomponents.httpclient.source [4.5.14,4.5.14]
 	org.apache.httpcomponents.httpcore [4.4.16,4.4.16]
@@ -18,10 +16,10 @@
 	org.hamcrest.core.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
 	org.hamcrest.library [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
 	org.hamcrest.library.source [1.3.0.v20230809-1000,1.3.0.v20230809-1000]
-	org.junit [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.junit.source [4.13.2.v20230809-1000,4.13.2.v20230809-1000]
-	org.objenesis [3.3,3.3]
-	org.objenesis.source [3.3,3.3]
+	org.junit [4.13.2.v20240929-1000,4.13.2.v20240929-1000]
+	org.junit.source [4.13.2.v20240929-1000,4.13.2.v20240929-1000]
+	org.objenesis [3.4,3.4]
+	org.objenesis.source [3.4,3.4]
 	org.osgi.service.cm [1.6.1.202109301733,1.6.1.202109301733]
 	org.osgi.service.cm.source [1.6.1.202109301733,1.6.1.202109301733]
 }
diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml
index 06cd76c..6352d0a 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>7.0.0-SNAPSHOT</version>
+  <version>7.3.0-SNAPSHOT</version>
   <packaging>pom</packaging>
 
   <name>JGit Tycho Parent</name>
@@ -30,8 +30,8 @@
 
   <properties>
     <java.version>17</java.version>
-    <tycho-version>4.0.6</tycho-version>
-    <target-platform>jgit-4.17</target-platform>
+    <tycho-version>4.0.11</tycho-version>
+    <target-platform>jgit-4.32</target-platform>
     <project.build.outputTimestamp>${git.commit.time}</project.build.outputTimestamp>
   </properties>
 
@@ -174,7 +174,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-enforcer-plugin</artifactId>
-        <version>3.4.1</version>
+        <version>3.5.0</version>
         <executions>
           <execution>
             <id>enforce-maven</id>
@@ -184,7 +184,7 @@
             <configuration>
               <rules>
                 <requireMavenVersion>
-                  <version>3.6.3</version>
+                  <version>3.9.0</version>
                 </requireMavenVersion>
               </rules>
             </configuration>
@@ -204,7 +204,7 @@
       <plugin>
         <groupId>org.cyclonedx</groupId>
         <artifactId>cyclonedx-maven-plugin</artifactId>
-        <version>2.7.11</version>
+        <version>2.9.1</version>
         <configuration>
           <projectType>library</projectType>
           <schemaVersion>1.4</schemaVersion>
@@ -233,7 +233,7 @@
       <plugin>
         <groupId>io.github.git-commit-id</groupId>
         <artifactId>git-commit-id-maven-plugin</artifactId>
-        <version>7.0.0</version>
+        <version>9.0.1</version>
         <executions>
           <execution>
             <id>get-the-git-infos</id>
@@ -273,7 +273,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-jar-plugin</artifactId>
-          <version>3.3.0</version>
+          <version>3.4.2</version>
           <configuration>
             <archive>
               <manifestEntries>
@@ -381,36 +381,36 @@
         <plugin>
           <groupId>org.eclipse.cbi.maven.plugins</groupId>
           <artifactId>eclipse-jarsigner-plugin</artifactId>
-          <version>1.4.3</version>
+          <version>1.5.2</version>
         </plugin>
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>build-helper-maven-plugin</artifactId>
-          <version>3.5.0</version>
+          <version>3.6.0</version>
         </plugin>
         <plugin>
           <artifactId>maven-clean-plugin</artifactId>
-          <version>3.3.2</version>
+          <version>3.4.1</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-deploy-plugin</artifactId>
-          <version>3.1.1</version>
+          <version>3.1.3</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-install-plugin</artifactId>
-          <version>3.1.1</version>
+          <version>3.1.3</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-site-plugin</artifactId>
-          <version>4.0.0-M13</version>
+          <version>4.0.0-M16</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-artifact-plugin</artifactId>
-          <version>3.5.0</version>
+          <version>3.6.0</version>
           <configuration>
             <ignore>**/*cyclonedx.json</ignore>
             <reproducible>true</reproducible>
diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
index 955c5c0..2056f0b 100644
--- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
@@ -3,30 +3,30 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.pgm.test
 Bundle-SymbolicName: org.eclipse.jgit.pgm.test
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.eclipse.jgit.api;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.diff;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.diffmergetool;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.merge;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.pgm;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.pgm.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.pgm.opt;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.0,7.1.0)",
+Import-Package: org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.diff;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.dircache;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.merge;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.pgm;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.pgm.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.pgm.opt;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util.io;version="[7.3.0,7.4.0)",
  org.hamcrest.core;bundle-version="[1.1.0,3.0.0)",
  org.junit;version="[4.13,5.0.0)",
  org.junit.rules;version="[4.13,5.0.0)",
diff --git a/org.eclipse.jgit.pgm.test/pom.xml b/org.eclipse.jgit.pgm.test/pom.xml
index 90090d6..24c289c 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm.test</artifactId>
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java
index 6d6374f..a48fcbc 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 Google Inc. and others
+ * Copyright (C) 2012, 2025 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
@@ -12,7 +12,10 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.dircache.DirCache;
@@ -32,12 +35,7 @@ public void setUp() throws Exception {
 
 	@Test
 	public void testAddNothing() throws Exception {
-		try {
-			execute("git add");
-			fail("Must die");
-		} catch (Die e) {
-			// expected, requires argument
-		}
+		assertThrows(Die.class, () -> execute("git add"));
 	}
 
 	@Test
@@ -46,6 +44,17 @@ public void testAddUsage() throws Exception {
 	}
 
 	@Test
+	public void testAddInvalidOptionCombinations() throws Exception {
+		writeTrashFile("greeting", "Hello, world!");
+		assertThrows(Die.class, () -> execute("git add -u -A greeting"));
+		assertThrows(Die.class,
+				() -> execute("git add -u --ignore-removed greeting"));
+		// --renormalize implies -u
+		assertThrows(Die.class,
+				() -> execute("git add --renormalize --all greeting"));
+	}
+
+	@Test
 	public void testAddAFile() throws Exception {
 		writeTrashFile("greeting", "Hello, world!");
 		assertArrayEquals(new String[] { "" }, //
@@ -78,4 +87,34 @@ public void testAddAlreadyAdded() throws Exception {
 		assertNotNull(cache.getEntry("greeting"));
 		assertEquals(1, cache.getEntryCount());
 	}
+
+	@Test
+	public void testAddDeleted() throws Exception {
+		File greeting = writeTrashFile("greeting", "Hello, world!");
+		git.add().addFilepattern("greeting").call();
+		DirCache cache = db.readDirCache();
+		assertNotNull(cache.getEntry("greeting"));
+		assertEquals(1, cache.getEntryCount());
+		assertTrue(greeting.delete());
+		assertArrayEquals(new String[] { "" }, //
+				execute("git add greeting"));
+
+		cache = db.readDirCache();
+		assertEquals(0, cache.getEntryCount());
+	}
+
+	@Test
+	public void testAddDeleted2() throws Exception {
+		File greeting = writeTrashFile("greeting", "Hello, world!");
+		git.add().addFilepattern("greeting").call();
+		DirCache cache = db.readDirCache();
+		assertNotNull(cache.getEntry("greeting"));
+		assertEquals(1, cache.getEntryCount());
+		assertTrue(greeting.delete());
+		assertArrayEquals(new String[] { "" }, //
+				execute("git add -A"));
+
+		cache = db.readDirCache();
+		assertEquals(0, cache.getEntryCount());
+	}
 }
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 a1fb9fb..c56cc6b 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
@@ -126,7 +126,7 @@ private RevCommit createSecondCommit() throws Exception {
 		JGitTestUtil.writeTrashFile(db, "Test.txt", "Some change");
 		git.add().addFilepattern("Test.txt").call();
 		return git.commit()
-				.setCommitter(new PersonIdent(this.committer, tr.getDate()))
+				.setCommitter(new PersonIdent(this.committer, tr.getInstant()))
 				.setMessage("Second commit").call();
 	}
 
@@ -134,7 +134,7 @@ private RevCommit createThirdCommit() throws Exception {
 		JGitTestUtil.writeTrashFile(db, "change.txt", "another change");
 		git.add().addFilepattern("change.txt").call();
 		return git.commit()
-				.setCommitter(new PersonIdent(this.committer, tr.getDate()))
+				.setCommitter(new PersonIdent(this.committer, tr.getInstant()))
 				.setMessage("Third commit").call();
 	}
 
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
index c785443..595767d 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
@@ -123,6 +123,15 @@ public void testDescribeCommitMatch2() throws Exception {
 	}
 
 	@Test
+	public void testDescribeExclude() throws Exception {
+		initialCommitAndTag();
+		secondCommit();
+		git.tag().setName("v2.0").call();
+		assertArrayEquals(new String[] { "v1.0-1-g56f6ceb", "" },
+				execute("git describe --exclude v2.*"));
+	}
+
+	@Test
 	public void testDescribeCommitMultiMatch() throws Exception {
 		initialCommitAndTag();
 		secondCommit();
@@ -133,6 +142,17 @@ public void testDescribeCommitMultiMatch() throws Exception {
 	}
 
 	@Test
+	public void testDescribeCommitMultiExclude() throws Exception {
+		initialCommitAndTag();
+		secondCommit();
+		git.tag().setName("v2.0.0").call();
+		git.tag().setName("v2.1.1").call();
+		git.tag().setName("v2.2").call();
+		assertArrayEquals("git yields v2.2", new String[] { "v2.2", "" },
+				execute("git describe --exclude v2.0* --exclude v2.1.*"));
+	}
+
+	@Test
 	public void testDescribeCommitNoMatch() throws Exception {
 		initialCommitAndTag();
 		writeTrashFile("greeting", "Hello, world!");
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
index 54c4f26..6339831 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java
@@ -27,6 +27,7 @@
 import org.eclipse.jgit.internal.diffmergetool.MergeTools;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 /**
@@ -77,6 +78,7 @@ public void testUserToolWithCommandNotFoundError() throws Exception {
 				+ errorReturnCode);
 	}
 
+	@Ignore
 	@Test
 	public void testEmptyToolName() throws Exception {
 		assumeLinuxPlatform();
@@ -91,7 +93,7 @@ public void testEmptyToolName() throws Exception {
 
 		createMergeConflict();
 
-		String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123.";
+		String araxisErrorLine = "compare-im6.q16: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1131.";
 		String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, };
 		runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput),
 				MERGE_TOOL, "--no-prompt");
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/PackRefsTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/PackRefsTest.java
new file mode 100644
index 0000000..b4d4ea9
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/PackRefsTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2024 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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.pgm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PackRefsTest extends CLIRepositoryTestCase {
+	private Git git;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		git = new Git(db);
+		git.commit().setMessage("initial commit").call();
+	}
+
+	@Test
+	public void tagPacked() throws Exception {
+		git.tag().setName("test").call();
+		git.packRefs().call();
+		assertEquals(Ref.Storage.PACKED,
+				git.getRepository().exactRef("refs/tags/test").getStorage());
+	}
+
+	@Test
+	public void nonTagRefNotPackedWithoutAll() throws Exception {
+		git.branchCreate().setName("test").call();
+		git.packRefs().call();
+		assertEquals(Ref.Storage.LOOSE,
+				git.getRepository().exactRef("refs/heads/test").getStorage());
+	}
+
+	@Test
+	public void nonTagRefPackedWithAll() throws Exception {
+		git.branchCreate().setName("test").call();
+		git.packRefs().setAll(true).call();
+		assertEquals(Ref.Storage.PACKED,
+				git.getRepository().exactRef("refs/heads/test").getStorage());
+	}
+
+	@Test
+	public void refTableCompacted() throws Exception {
+		((FileRepository) git.getRepository()).convertRefStorage(
+				ConfigConstants.CONFIG_REF_STORAGE_REFTABLE, false, false);
+
+		git.commit().setMessage("test commit").call();
+		File tableDir = new File(db.getDirectory(), Constants.REFTABLE);
+		File[] reftables = tableDir.listFiles();
+		assertNotNull(reftables);
+		assertTrue(reftables.length > 2);
+
+		git.packRefs().call();
+
+		reftables = tableDir.listFiles();
+		assertNotNull(reftables);
+		assertEquals(2, reftables.length);
+	}
+}
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index bb0c23d..d91efd4 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.pgm
 Bundle-SymbolicName: org.eclipse.jgit.pgm
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -14,49 +14,50 @@
  org.eclipse.jetty.server.handler;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util;version="[12.0.0,13.0.0)",
  org.eclipse.jetty.util.component;version="[12.0.0,13.0.0)",
- org.eclipse.jgit.api;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.archive;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.awtui;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.blame;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.diff;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.gitrepo;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.diffmergetool;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.io;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.pack;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.server;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.server.fs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs.server.s3;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.merge;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.notes;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revplot;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk.filter;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.pack;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.http.apache;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.ssh.jsch;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.sshd;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.archive;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.awtui;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.blame;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.diff;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.dircache;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.gitrepo;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.io;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.midx;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.server;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs.server.s3;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.merge;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.notes;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revplot;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk.filter;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.pack;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.http.apache;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.ssh.jsch;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.sshd;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util.io;version="[7.3.0,7.4.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="7.0.0";
+Export-Package: org.eclipse.jgit.console;version="7.3.0";
  uses:="org.eclipse.jgit.transport,
   org.eclipse.jgit.util",
- org.eclipse.jgit.pgm;version="7.0.0";
+ org.eclipse.jgit.pgm;version="7.3.0";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.util.io,
    org.eclipse.jgit.awtui,
@@ -68,14 +69,14 @@
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.api,
    javax.swing",
- org.eclipse.jgit.pgm.debug;version="7.0.0";
+ org.eclipse.jgit.pgm.debug;version="7.3.0";
   uses:="org.eclipse.jgit.util.io,
    org.eclipse.jgit.pgm,
    org.eclipse.jetty.servlet",
- org.eclipse.jgit.pgm.internal;version="7.0.0";
+ org.eclipse.jgit.pgm.internal;version="7.3.0";
   x-friends:="org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.test",
- org.eclipse.jgit.pgm.opt;version="7.0.0";
+ org.eclipse.jgit.pgm.opt;version="7.3.0";
   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 8bb1b64..1c4a481 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="7.3.0.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 08d3727..6bf88d9 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
@@ -26,6 +26,8 @@
 org.eclipse.jgit.pgm.Merge
 org.eclipse.jgit.pgm.MergeBase
 org.eclipse.jgit.pgm.MergeTool
+org.eclipse.jgit.pgm.MultiPackIndex
+org.eclipse.jgit.pgm.PackRefs
 org.eclipse.jgit.pgm.Push
 org.eclipse.jgit.pgm.ReceivePack
 org.eclipse.jgit.pgm.Reflog
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index 7cd76c3..5890ce8 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 50ee809..e9630e9 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
@@ -12,6 +12,7 @@
 # default meta variable defined in the org.kohsuke.args4j.spi.OneArgumentOptionHandler
 N=N
 
+addIncompatibleOptions=--update/-u cannot be combined with --all/-A/--no-ignore-removal or --no-all/--ignore-removal. Note that --renormalize implies --update.
 alreadyOnBranch=Already on ''{0}''
 alreadyUpToDate=Already up-to-date.
 answerNo=n
@@ -255,8 +256,11 @@
 untrackedFiles=Untracked files:
 updating=Updating {0}..{1}
 usage_Abbrev=Instead of using the default number of hexadecimal digits (which will vary according to the number of objects in the repository with a default of 7) of the abbreviated object name, use <n> digits, or as many digits as needed to form a unique object name. An <n> of 0 will suppress long format, only showing the closest tag.
-usage_addRenormalize=Apply the "clean" process freshly to tracked files to forcibly add them again to the index. This implies -u.
+usage_addRenormalize=Apply the "clean" process freshly to tracked files to forcibly add them again to the index. This implies --update/-u.
+usage_addStageDeletions=Add, modify, or remove index entries to match the working tree. Cannot be used with --update/-u.
+usage_addDontStageDeletions=Only add or modify index entries, but do not remove index entries for which there is no file. (Don''t stage deletions.) Cannot be used with --update/-u.
 usage_Aggressive=This option will cause gc to more aggressively optimize the repository at the expense of taking much more time
+usage_All=Pack all refs, except hidden refs, broken refs, and symbolic refs.
 usage_AlwaysFallback=Show uniquely abbreviated commit object as fallback
 usage_bareClone=Make a bare Git repository. That is, instead of creating [DIRECTORY] and placing the administrative files in [DIRECTORY]/.git, make the [DIRECTORY] itself the $GIT_DIR.
 usage_extraArgument=Pass an extra argument to a merge driver. Currently supported are "-X ours" and "-X theirs".
@@ -278,6 +282,7 @@
 usage_Describe=Show the most recent tag that is reachable from a commit
 usage_DiffAlgorithms=Test performance of jgit's diff algorithms
 usage_DisplayTheVersionOfJgit=Display the version of jgit
+usage_Exclude=Do not consider tags matching the given glob(7) pattern, excluding the "refs/tags/" prefix
 usage_Gc=Cleanup unnecessary files and optimize the local repository
 usage_Glog=View commit history as a graph
 usage_DiffGuiTool=When git-difftool is invoked with the -g or --gui option the default diff tool will be read from the configured diff.guitool variable instead of diff.tool.
@@ -299,7 +304,9 @@
 usage_Match=Only consider tags matching the given glob(7) pattern or patterns, excluding the "refs/tags/" prefix.
 usage_MergeBase=Find as good common ancestors as possible for a merge
 usage_MergesTwoDevelopmentHistories=Merges two development histories
+usage_MultiPackIndex=Operations over the multipack index
 usage_PackKeptObjects=Include objects in packs locked by a ".keep" file when repacking
+usage_PackRefs=Pack heads and tags for efficient repository access
 usage_PreserveOldPacks=Preserve old pack files by moving them into the preserved subdirectory instead of deleting them after repacking
 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
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Add.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Add.java
index 2ebab5e..dc9d77d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Add.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Add.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, Sasa Zivkov <sasa.zivkov@sap.com> and others
+ * Copyright (C) 2010, 2025 Sasa Zivkov <sasa.zivkov@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
@@ -16,6 +16,7 @@
 import org.eclipse.jgit.api.AddCommand;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.pgm.internal.CLIText;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -28,17 +29,33 @@ class Add extends TextBuiltin {
 	@Option(name = "--update", aliases = { "-u" }, usage = "usage_onlyMatchAgainstAlreadyTrackedFiles")
 	private boolean update = false;
 
-	@Argument(required = true, metaVar = "metaVar_filepattern", usage = "usage_filesToAddContentFrom")
+	@Option(name = "--all", aliases = { "-A",
+			"--no-ignore-removal" }, usage = "usage_addStageDeletions")
+	private Boolean all;
+
+	@Option(name = "--no-all", aliases = {
+			"--ignore-removal" }, usage = "usage_addDontStageDeletions")
+	private void noAll(@SuppressWarnings("unused") boolean ignored) {
+		all = Boolean.FALSE;
+	}
+
+	@Argument(metaVar = "metaVar_filepattern", usage = "usage_filesToAddContentFrom")
 	private List<String> filepatterns = new ArrayList<>();
 
 	@Override
 	protected void run() throws Exception {
 		try (Git git = new Git(db)) {
-			AddCommand addCmd = git.add();
 			if (renormalize) {
 				update = true;
 			}
+			if (update && all != null) {
+				throw die(CLIText.get().addIncompatibleOptions);
+			}
+			AddCommand addCmd = git.add();
 			addCmd.setUpdate(update).setRenormalize(renormalize);
+			if (all != null) {
+				addCmd.setAll(all.booleanValue());
+			}
 			for (String p : filepatterns) {
 				addCmd.addFilepattern(p);
 			}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java
index d2285ae..285fe2a 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java
@@ -18,7 +18,7 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
-import java.text.SimpleDateFormat;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -91,7 +91,7 @@ void ignoreAllSpace(@SuppressWarnings("unused") boolean on) {
 
 	private final Map<RevCommit, String> abbreviatedCommits = new HashMap<>();
 
-	private SimpleDateFormat dateFmt;
+	private DateTimeFormatter dateFmt;
 
 	private int begin;
 
@@ -125,9 +125,9 @@ protected void run() {
 		}
 
 		if (showRawTimestamp) {
-			dateFmt = new SimpleDateFormat("ZZZZ"); //$NON-NLS-1$
+			dateFmt = DateTimeFormatter.ofPattern("ZZ"); //$NON-NLS-1$
 		} else {
-			dateFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZZZ"); //$NON-NLS-1$
+			dateFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss ZZ"); //$NON-NLS-1$
 		}
 
 		try (ObjectReader reader = db.newObjectReader();
@@ -335,12 +335,14 @@ private String date(int line) {
 		if (author == null)
 			return ""; //$NON-NLS-1$
 
-		dateFmt.setTimeZone(author.getTimeZone());
-		if (!showRawTimestamp)
-			return dateFmt.format(author.getWhen());
+		if (!showRawTimestamp) {
+			return dateFmt.withZone(author.getZoneId())
+					.format(author.getWhenAsInstant());
+		}
 		return String.format("%d %s", //$NON-NLS-1$
-				Long.valueOf(author.getWhen().getTime() / 1000L),
-				dateFmt.format(author.getWhen()));
+				Long.valueOf(author.getWhenAsInstant().getEpochSecond()),
+				dateFmt.withZone(author.getZoneId())
+						.format(author.getWhenAsInstant()));
 	}
 
 	private String abbreviate(ObjectReader reader, RevCommit commit)
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Config.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Config.java
index 52f40c2..f5de704 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Config.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Config.java
@@ -94,7 +94,7 @@ private void list() throws IOException, ConfigInvalidException {
 		if (global || isListAll())
 			list(SystemReader.getInstance().openUserConfig(null, fs));
 		if (local || isListAll())
-			list(new FileBasedConfig(fs.resolve(getRepository().getDirectory(),
+			list(new FileBasedConfig(fs.resolve(getRepository().getCommonDirectory(),
 					Constants.CONFIG), fs));
 	}
 
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
index 913d7c7..2633336 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
@@ -44,6 +44,9 @@ class Describe extends TextBuiltin {
 	@Option(name = "--match", usage = "usage_Match", metaVar = "metaVar_pattern")
 	private List<String> patterns = new ArrayList<>();
 
+	@Option(name = "--exclude", usage = "usage_Exclude", metaVar = "metaVar_pattern")
+	private List<String> excludes = new ArrayList<>();
+
 	@Option(name = "--abbrev", usage = "usage_Abbrev")
 	private Integer abbrev;
 
@@ -59,6 +62,7 @@ protected void run() {
 			cmd.setTags(useTags);
 			cmd.setAlways(always);
 			cmd.setMatch(patterns.toArray(new String[0]));
+			cmd.setExclude(excludes.toArray(new String[0]));
 			if (abbrev != null) {
 				cmd.setAbbrev(abbrev.intValue());
 			}
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 852a4b3..958e566 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
@@ -32,13 +32,12 @@
 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.SignatureVerifier.SignatureVerification;
 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.lib.SignatureVerifiers;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.pgm.internal.VerificationUtils;
@@ -174,8 +173,6 @@ void noPrefix(@SuppressWarnings("unused") boolean on) {
 	// END -- Options shared with Diff
 
 
-	private GpgSignatureVerifier verifier;
-
 	private GpgConfig config;
 
 	Log() {
@@ -227,9 +224,6 @@ protected void run() {
 			throw die(e.getMessage(), e);
 		} finally {
 			diffFmt.close();
-			if (verifier != null) {
-				verifier.clear();
-			}
 		}
 	}
 
@@ -293,21 +287,13 @@ 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);
+		SignatureVerification verification = SignatureVerifiers.verify(db,
+				config, c);
 		if (verification == null) {
 			return;
 		}
 		VerificationUtils.writeVerification(outw, verification,
-				verifier.getName(), c.getCommitterIdent());
+				verification.verifierName(), c.getCommitterIdent());
 	}
 
 	/**
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeBase.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeBase.java
index aacde2f..a29c4d9 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeBase.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeBase.java
@@ -26,11 +26,6 @@ class MergeBase extends TextBuiltin {
 	private boolean all;
 
 	@Argument(index = 0, metaVar = "metaVar_commitish", required = true)
-	void commit_0(final RevCommit c) {
-		commits.add(c);
-	}
-
-	@Argument(index = 1, metaVar = "metaVar_commitish", required = true)
 	private List<RevCommit> commits = new ArrayList<>();
 
 	@Override
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MultiPackIndex.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MultiPackIndex.java
new file mode 100644
index 0000000..1844223
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MultiPackIndex.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.pgm;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+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;
+import org.eclipse.jgit.internal.storage.midx.MultiPackIndexPrettyPrinter;
+import org.eclipse.jgit.internal.storage.midx.MultiPackIndexWriter;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+@Command(common = true, usage = "usage_MultiPackIndex")
+@SuppressWarnings("nls")
+class MultiPackIndex extends TextBuiltin {
+	@Argument(index = 0, required = true, usage = "write, print")
+	private String command;
+
+	@Option(name = "--midx")
+	private String midxPath;
+
+	/** {@inheritDoc} */
+	@Override
+	protected void run() throws IOException {
+		switch (command) {
+		case "print":
+			printMultiPackIndex();
+			break;
+		case "write":
+			writeMultiPackIndex();
+			break;
+		default:
+			outw.println("Unknown command " + command);
+		}
+	}
+
+	private void printMultiPackIndex() {
+		if (midxPath == null || midxPath.isEmpty()) {
+			throw die("'print' requires the path of a multipack "
+					+ "index file with --midx option.");
+		}
+
+		try (FileInputStream is = new FileInputStream(midxPath)) {
+			PrintWriter pw = new PrintWriter(outw, true);
+			MultiPackIndexPrettyPrinter.prettyPrint(is.readAllBytes(), pw);
+		} catch (FileNotFoundException e) {
+			throw die(true, e);
+		} catch (IOException e) {
+			throw die(true, e);
+		}
+	}
+
+	private void writeMultiPackIndex() throws IOException {
+		if (!(db.getObjectDatabase() instanceof ObjectDirectory)) {
+			throw die("This repository object db doesn't have packs");
+		}
+
+		File midx;
+		if (midxPath == null || midxPath.isEmpty()) {
+			midx = new File(((ObjectDirectory) db.getObjectDatabase())
+					.getPackDirectory(), "multi-pack-index");
+		} else {
+			midx = new File(midxPath);
+		}
+
+		errw.println("Writing " + midx.getAbsolutePath());
+
+		ObjectDirectory odb = (ObjectDirectory) db.getObjectDatabase();
+
+		Map<String, PackIndex> indexes = new HashMap<>();
+		for (Pack pack : odb.getPacks()) {
+			PackFile packFile = pack.getPackFile().create(PackExt.INDEX);
+			try {
+				indexes.put(packFile.getName(), pack.getIndex());
+			} catch (IOException e) {
+				throw die("Cannot open index in pack", e);
+			}
+		}
+
+		MultiPackIndexWriter writer = new MultiPackIndexWriter();
+		try (FileOutputStream out = new FileOutputStream(midxPath)) {
+			writer.write(NullProgressMonitor.INSTANCE, out, indexes);
+		} catch (IOException e) {
+			throw die("Cannot write midx " + midxPath, e);
+		}
+	}
+}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/PackRefs.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/PackRefs.java
new file mode 100644
index 0000000..ee05f5c
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/PackRefs.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2024 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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.pgm;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.kohsuke.args4j.Option;
+
+@Command(common = true, usage = "usage_PackRefs")
+class PackRefs extends TextBuiltin {
+	@Option(name = "--all", usage = "usage_All")
+	private boolean all;
+
+	@Override
+	protected void run() {
+		Git git = Git.wrap(db);
+		try {
+			git.packRefs().setProgressMonitor(new TextProgressMonitor(errw))
+					.setAll(all).call();
+		} catch (GitAPIException e) {
+			throw die(e.getMessage(), e);
+		}
+	}
+}
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 4feb090..a3a6782 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
@@ -14,11 +14,10 @@
 
 import java.io.BufferedOutputStream;
 import java.io.IOException;
-import java.text.DateFormat;
 import java.text.MessageFormat;
-import java.text.SimpleDateFormat;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.Locale;
-import java.util.TimeZone;
 
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.RawTextComparator;
@@ -30,12 +29,11 @@
 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.lib.SignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.SignatureVerifiers;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.pgm.internal.VerificationUtils;
 import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
@@ -52,9 +50,9 @@
 
 @Command(common = true, usage = "usage_show")
 class Show extends TextBuiltin {
-	private final TimeZone myTZ = TimeZone.getDefault();
+	private final ZoneId myTZ = ZoneId.systemDefault();
 
-	private final DateFormat fmt;
+	private final DateTimeFormatter fmt;
 
 	private DiffFormatter diffFmt;
 
@@ -158,7 +156,8 @@ void noPrefix(@SuppressWarnings("unused") boolean on) {
 	// END -- Options shared with Diff
 
 	Show() {
-		fmt = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy ZZZZZ", Locale.US); //$NON-NLS-1$
+		fmt = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy ZZ", //$NON-NLS-1$
+				Locale.US);
 	}
 
 	@Override
@@ -233,15 +232,17 @@ private void show(RevTag tag) throws IOException {
 		outw.print(tag.getTagName());
 		outw.println();
 
-		final PersonIdent tagger = tag.getTaggerIdent();
+		PersonIdent tagger = tag.getTaggerIdent();
 		if (tagger != null) {
 			outw.println(MessageFormat.format(CLIText.get().taggerInfo,
 					tagger.getName(), tagger.getEmailAddress()));
 
-			final TimeZone taggerTZ = tagger.getTimeZone();
-			fmt.setTimeZone(taggerTZ != null ? taggerTZ : myTZ);
+			ZoneId taggerTZ = tagger.getZoneId();
+			String formattedTaggerTime = fmt
+					.withZone(taggerTZ != null ? taggerTZ : myTZ)
+					.format(tagger.getWhenAsInstant());
 			outw.println(MessageFormat.format(CLIText.get().dateInfo,
-					fmt.format(tagger.getWhen())));
+					formattedTaggerTime));
 		}
 
 		outw.println();
@@ -294,10 +295,12 @@ private void show(RevWalk rw, RevCommit c) throws IOException {
 		outw.println(MessageFormat.format(CLIText.get().authorInfo,
 				author.getName(), author.getEmailAddress()));
 
-		final TimeZone authorTZ = author.getTimeZone();
-		fmt.setTimeZone(authorTZ != null ? authorTZ : myTZ);
+		final ZoneId authorTZ = author.getZoneId();
+		String formattedAuthorTime = fmt
+				.withZone(authorTZ != null ? authorTZ : myTZ)
+				.format(author.getWhenAsInstant());
 		outw.println(MessageFormat.format(CLIText.get().dateInfo,
-				fmt.format(author.getWhen())));
+				formattedAuthorTime));
 
 		outw.println();
 		final String[] lines = c.getFullMessage().split("\n"); //$NON-NLS-1$
@@ -335,23 +338,13 @@ private void showSignature(RevCommit c) throws IOException {
 		if (c.getRawGpgSignature() == null) {
 			return;
 		}
-		GpgSignatureVerifierFactory factory = GpgSignatureVerifierFactory
-				.getDefault();
-		if (factory == null) {
+		GpgConfig config = new GpgConfig(db.getConfig());
+		SignatureVerification verification = SignatureVerifiers.verify(db,
+				config, c);
+		if (verification == 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();
-		}
+		VerificationUtils.writeVerification(outw, verification,
+				verification.verifierName(), c.getCommitterIdent());
 	}
 }
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 4ea67ab..6be30c9 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
@@ -27,10 +27,10 @@
 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.lib.SignatureVerifier.SignatureVerification;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.pgm.internal.VerificationUtils;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -106,7 +106,8 @@ protected void run() {
 						if (error != null) {
 							throw die(error.getMessage(), error);
 						}
-						writeVerification(verifySig.getVerifier().getName(),
+						writeVerification(
+								verification.getVerification().verifierName(),
 								(RevTag) verification.getObject(),
 								verification.getVerification());
 					}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildCommitGraph.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildCommitGraph.java
index 2f96ef7..22d9e34 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildCommitGraph.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildCommitGraph.java
@@ -18,8 +18,8 @@
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.ListIterator;
@@ -166,7 +166,8 @@ private void recreateCommitGraph() throws IOException {
 
 					final CommitBuilder newc = new CommitBuilder();
 					newc.setTreeId(emptyTree);
-					newc.setAuthor(new PersonIdent(me, new Date(t.commitTime)));
+					newc.setAuthor(new PersonIdent(me,
+							Instant.ofEpochSecond(t.commitTime)));
 					newc.setCommitter(newc.getAuthor());
 					newc.setParentIds(newParents);
 					newc.setMessage("ORIGINAL " + t.oldId.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java
index c95f138..74e322f 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java
@@ -31,6 +31,7 @@
 import org.eclipse.jgit.pgm.TextBuiltin;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.kohsuke.args4j.Argument;
 
@@ -68,7 +69,7 @@ protected void run() throws Exception {
 		ObjectReuseAsIs asis = (ObjectReuseAsIs) reader;
 		ObjectToPack target = asis.newObjectToPack(obj, obj.getType());
 
-		PackWriter pw = new PackWriter(reader) {
+		PackWriter pw = new PackWriter(new PackConfig(), reader) {
 			@Override
 			public void select(ObjectToPack otp, StoredObjectRepresentation next) {
 				otp.select(next);
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/WriteReftable.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/WriteReftable.java
index faa2bce..7aff2dd 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/WriteReftable.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/WriteReftable.java
@@ -24,6 +24,8 @@
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -209,14 +211,15 @@ private static List<LogEntry> readLog(String logPath)
 				}
 				String ref = m.group(1);
 				double t = Double.parseDouble(m.group(2));
-				long time = ((long) t) * 1000L;
+				Instant time = Instant.ofEpochSecond((long) t);
 				long index = (long) (t * 1e6);
 				String user = m.group(3);
 				ObjectId oldId = parseId(m.group(4));
 				ObjectId newId = parseId(m.group(5));
 				String msg = m.group(6);
 				String email = user + "@gerrit"; //$NON-NLS-1$
-				PersonIdent who = new PersonIdent(user, email, time, -480);
+				PersonIdent who = new PersonIdent(user, email, time,
+						ZoneOffset.ofHours(-8));
 				log.add(new LogEntry(ref, index, who, oldId, newId, msg));
 			}
 		}
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 b5bf6d2..bb1e950 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, 2021 Obeo and others
+ * Copyright (C) 2013, 2025 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
@@ -91,6 +91,7 @@ public static String fatalError(String message) {
 	}
 
 	// @formatter:off
+	/***/ public String addIncompatibleOptions;
 	/***/ public String alreadyOnBranch;
 	/***/ public String alreadyUpToDate;
 	/***/ public String answerNo;
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
index c1f8a86..64ee602 100644
--- 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
@@ -11,7 +11,7 @@
 
 import java.io.IOException;
 
-import org.eclipse.jgit.lib.GpgSignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.SignatureVerifier.SignatureVerification;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.util.GitDateFormatter;
 import org.eclipse.jgit.util.SignatureUtils;
diff --git a/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF
index ad01640..8942a41 100644
--- a/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.agent/META-INF/MANIFEST.MF
@@ -2,16 +2,16 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %Bundle-Name
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.agent;singleton:=true
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Localization: OSGI-INF/l10n/agent
 Bundle-Vendor: %Bundle-Vendor
-Fragment-Host: org.eclipse.jgit.ssh.apache;bundle-version="[7.0.0,7.1.0)"
+Fragment-Host: org.eclipse.jgit.ssh.apache;bundle-version="[7.3.0,7.4.0)"
 Bundle-ActivationPolicy: lazy
 Automatic-Module-Name: org.eclipse.jgit.ssh.apache.agent
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.eclipse.jgit.transport.sshd;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)"
+Import-Package: org.eclipse.jgit.transport.sshd;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)"
 Require-Bundle: com.sun.jna;bundle-version="[5.8.0,6.0.0)",
  com.sun.jna.platform;bundle-version="[5.8.0,6.0.0)"
-Export-Package: org.eclipse.jgit.internal.transport.sshd.agent.connector;version="7.0.0";x-internal:=true
+Export-Package: org.eclipse.jgit.internal.transport.sshd.agent.connector;version="7.3.0";x-internal:=true
diff --git a/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF
index 684ab70..37b442e 100644
--- a/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.agent/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.ssh.apache.agent - Sources
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.agent.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache.agent;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache.agent;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.apache.agent/pom.xml b/org.eclipse.jgit.ssh.apache.agent/pom.xml
index f407ce1..2d34495 100644
--- a/org.eclipse.jgit.ssh.apache.agent/pom.xml
+++ b/org.eclipse.jgit.ssh.apache.agent/pom.xml
@@ -17,7 +17,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache.agent</artifactId>
diff --git a/org.eclipse.jgit.ssh.apache.test/.classpath b/org.eclipse.jgit.ssh.apache.test/.classpath
index 6fdb99a..5be47af 100644
--- a/org.eclipse.jgit.ssh.apache.test/.classpath
+++ b/org.eclipse.jgit.ssh.apache.test/.classpath
@@ -11,5 +11,10 @@
 			<attribute name="test" value="true"/>
 		</attributes>
 	</classpathentry>
+	<classpathentry kind="src" path="tst-rsrc">
+		<attributes>
+			<attribute name="test" value="true"/>
+		</attributes>
+	</classpathentry>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/org.eclipse.jgit.ssh.apache.test/.gitattributes b/org.eclipse.jgit.ssh.apache.test/.gitattributes
new file mode 100644
index 0000000..b5b9375
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/.gitattributes
@@ -0,0 +1,2 @@
+/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle binary
+/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl* binary
diff --git a/org.eclipse.jgit.ssh.apache.test/BUILD b/org.eclipse.jgit.ssh.apache.test/BUILD
index b384464..bf796c0 100644
--- a/org.eclipse.jgit.ssh.apache.test/BUILD
+++ b/org.eclipse.jgit.ssh.apache.test/BUILD
@@ -1,19 +1,51 @@
 load(
+    "@com_googlesource_gerrit_bazlets//tools:genrule2.bzl",
+    "genrule2",
+)
+load(
     "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
     "junit_tests",
 )
 
+DEPS = [
+    "//lib:bcpkix",
+    "//lib:bcprov",
+    "//lib:bcutil",
+    "//lib:junit",
+    "//lib:slf4j-api",
+    "//lib:sshd-osgi",
+    "//lib:sshd-sftp",
+    "//org.eclipse.jgit:jgit",
+    "//org.eclipse.jgit.junit:junit",
+    "//org.eclipse.jgit.junit.ssh:junit-ssh",
+    "//org.eclipse.jgit.ssh.apache:ssh-apache",
+]
+
+HELPERS = ["tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java"]
+
 junit_tests(
     name = "sshd_apache",
-    srcs = glob(["tst/**/*.java"]),
+    srcs = glob(
+        ["tst/**/*.java"],
+        exclude = HELPERS,
+    ),
     tags = ["sshd"],
-    deps = [
-        "//lib:eddsa",
-        "//lib:junit",
-        "//lib:sshd-osgi",
-        "//lib:sshd-sftp",
-        "//org.eclipse.jgit:jgit",
-        "//org.eclipse.jgit.junit.ssh:junit-ssh",
-        "//org.eclipse.jgit.ssh.apache:ssh-apache",
+    runtime_deps = [":tst_rsrc"],
+    deps = DEPS + [
+        ":helpers",
     ],
 )
+
+java_library(
+    name = "helpers",
+    testonly = 1,
+    srcs = HELPERS,
+    deps = DEPS,
+)
+
+genrule2(
+    name = "tst_rsrc",
+    srcs = glob(["tst-rsrc/**"]),
+    outs = ["tst_rsrc.jar"],
+    cmd = "tar cf - $(SRCS) | tar -C $$TMP --strip-components=2 -xf - && cd $$TMP && zip -qr $$ROOT/$@ .",
+)
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 54d610a..88ab277 100644
--- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
@@ -3,35 +3,47 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.apache.test
 Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.test
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Require-Bundle: org.hamcrest.core;bundle-version="[1.3.0,2.0.0)"
-Import-Package: org.apache.sshd.client.config.hosts;version="[2.12.0,2.13.0)",
- org.apache.sshd.common;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.auth;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.config.keys;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.helpers;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.kex;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.keyprovider;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.session;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.signature;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.net;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.security;version="[2.12.0,2.13.0)",
- org.apache.sshd.core;version="[2.12.0,2.13.0)",
- org.apache.sshd.server;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.forward;version="[2.12.0,2.13.0)",
- org.eclipse.jgit.api;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.transport.sshd.proxy;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit.ssh;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.sshd;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.sshd.agent;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
+Import-Package: org.apache.sshd.certificate;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.config.hosts;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.auth;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.cipher;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.config.keys;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.helpers;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.kex;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.keyprovider;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.session;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.signature;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.buffer;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.net;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.security;version="[2.15.0,2.16.0)",
+ org.apache.sshd.core;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.forward;version="[2.15.0,2.16.0)",
+ org.eclipse.jgit.annotations;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.signing.ssh;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.transport.sshd;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.transport.sshd.proxy;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit.ssh;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.sshd;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.sshd.agent;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.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)"
+ org.junit.rules;version="[4.13.0,5.0.0)",
+ org.junit.runner;version="[4.13,5.0.0)",
+ org.junit.runners;version="[4.13.0,5.0.0)",
+ org.slf4j;version="[1.7.0,3.0.0)"
diff --git a/org.eclipse.jgit.ssh.apache.test/pom.xml b/org.eclipse.jgit.ssh.apache.test/pom.xml
index 967b781..b86a560 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache.test</artifactId>
@@ -91,6 +91,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.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/allowed_signers b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/allowed_signers
new file mode 100644
index 0000000..ec74409
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/allowed_signers
@@ -0,0 +1,2 @@
+tester@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO
+*@example.com cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBV
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key
new file mode 100644
index 0000000..b8de8c3
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAJAhCMgzIQjI
+MwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQ
+AAAEBmcXpast20+B4IzA0Xex2CKYiiWJj3NFJ5F0kil113vcdEl+iOTEbf1RC3uicECtid
++SaIMsAw7wrlWhOQTyBVAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub
new file mode 100644
index 0000000..842415b
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBV
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2
new file mode 100644
index 0000000..a4af047
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgwAAAJAa2jfBGto3
+wQAAAAtzc2gtZWQyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgw
+AAAEBothGMqFaA5aTO8MLx9wm1oDRfzQCSsu7uJwrOiUFTTTtLjJwgHqhPHhQ3yX0367TX
+pSAzOSttPsT7ZdsbfmODAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub
new file mode 100644
index 0000000..e46c87e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/ca_key2.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDtLjJwgHqhPHhQ3yX0367TXpSAzOSttPsT7ZdsbfmOD
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert
new file mode 100644
index 0000000..9da63ec
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/expired.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAINFZ5NKywAWh1G1P6BiBKArmYKs1BDhJBOawJKlS29VXAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAEAAAABAAAADGV4cGlyZWRfY2VydAAAAAAAAAAAZtOugAAAAABm1QAAAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgx0SX6I5MRt/VELe6JwQK2J35JogywDDvCuVaE5BPIFUAAABTAAAAC3NzaC1lZDI1NTE5AAAAQNf8i5dhRqWRe06epIRrZ5V+QZHq3ZrlJtlx98UJya9GAeCrJ5oHwBjr5O5TL5wNJS5Hz+T1qsJNFU9d1wdcuwI=
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert
new file mode 100644
index 0000000..101e374
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/no_principals.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAILzuED1RSloB/enTghTEKSACVOuEARP0f8UVXSRwEXN6AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAIAAAABAAAADW5vX3ByaW5jaXBhbHMAAAAAAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBwEQ2D0OHn4QDHnsINlgWUWpmhukseQCJu3Adulz28fFtewp1LLqkBy50wR6vJe1ifYbY4hzReXOSyoTmHSXEN
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert
new file mode 100644
index 0000000..752fee1
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other-ca.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIHNW2bzSS61lvgHippv3Ymx4cVEAXBVCb8lFXHnVpsSyAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAYAAAABAAAAB3Rlc3RlcjIAAAAWAAAAEnRlc3RlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACA7S4ycIB6oTx4UN8l9N+u016UgMzkrbT7E+2XbG35jgwAAAFMAAAALc3NoLWVkMjU1MTkAAABAuJ8zBazcaYTbUEr9QtoYox0MkVBg+8LANxJxc885M2vmg9yPHpTfV/emupqhBwuYcPJSskTxl7WX4TUNvhMsAA==
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert
new file mode 100644
index 0000000..15825f6
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/other.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGKXzyrvDzj9ObQ4SuzqytK6nomOV8DhgdzODfWuup1sAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAQAAAABAAAABW90aGVyAAAAFQAAABFvdGhlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAFMAAAALc3NoLWVkMjU1MTkAAABA1ycFqWehyC6pIISEkXSTtHbatLWl9HHAoUFouQiDdubAnMDRSkyHipXR62rq+8yEAvtqm1mXBzO8nLalkF9xAA==
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert
new file mode 100644
index 0000000..a2b241c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/tester.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICSl1xsyTWb23YlKo21musxOzj4L4eD2coTkHbBw2uOyAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAMAAAABAAAABnRlc3RlcgAAABYAAAASdGVzdGVyQGV4YW1wbGUuY29tAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDyjzq/0Egm1OxwrvqPZKUihE3w357Ji9Nd3j7VnUuvSYTXAdB9P0E+a2hyCcemmsil1MsvWTiCSSOsrHVB6FEO
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert
new file mode 100644
index 0000000..5f7164a
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/certs/two_principals.cert
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIFmWKr9gNSQT0vna7k3uOyUF9CTcMGw2zxTFBf2Ev8TzAAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzOAAAAAAAAAAgAAAABAAAABnRlc3RlcgAAACkAAAAPZm9vQGV4YW1wbGUuY29tAAAAEnRlc3RlckBleGFtcGxlLmNvbQAAAABm066AAAAAAGciyIAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDHRJfojkxG39UQt7onBArYnfkmiDLAMO8K5VoTkE8gVQAAAFMAAAALc3NoLWVkMjU1MTkAAABAqlSX2GzLz5U+hN/gF9UUyAkE6h5BgVFYhsyf1MR/B7Hoxa29wGLbJpUplrqEHMxoud2zfH2Nhj00unc3lr5bBA== ./signing_key.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl
new file mode 100644
index 0000000..9469340
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-all b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-all
new file mode 100644
index 0000000..6f744c3
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-all
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-ca b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-ca
new file mode 100644
index 0000000..84a8bc6
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-ca
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-cert b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-cert
new file mode 100644
index 0000000..26f29b2
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-cert
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-empty b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-empty
new file mode 100644
index 0000000..78e5187
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-empty
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-hash b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-hash
new file mode 100644
index 0000000..cdd1351
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-hash
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid
new file mode 100644
index 0000000..1a65243
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid-wild b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid-wild
new file mode 100644
index 0000000..9ba549f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keyid-wild
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keys b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keys
new file mode 100644
index 0000000..8dd496d
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-keys
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial
new file mode 100644
index 0000000..9965e2e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial-wild b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial-wild
new file mode 100644
index 0000000..aefd2b1
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-serial-wild
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha1 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha1
new file mode 100644
index 0000000..3928543
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha1
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha256 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha256
new file mode 100644
index 0000000..cdd1351
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-sha256
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-text b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-text
new file mode 100644
index 0000000..77ddd5e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/krl-text
@@ -0,0 +1,11 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YTh 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQx 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5w 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmy 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6Q 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BN 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6e 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KB 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCA 
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6A 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001
new file mode 100644
index 0000000..893fd5e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAtvUGe9h7cgnWjaOqdhh93yaVMp8JL7PAHMJ+uhu2E4QAAAIhUa4KiVGuC
+ogAAAAtzc2gtZWQyNTUxOQAAACAtvUGe9h7cgnWjaOqdhh93yaVMp8JL7PAHMJ+uhu2E4Q
+AAAECKgy+3FBgpdfxjOtNy9TamhadMWSyPlPiwu06mYVReyS29QZ72HtyCdaNo6p2GH3fJ
+pUynwkvs8Acwn66G7YThAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001-cert.pub
new file mode 100644
index 0000000..e2bcd25
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIE1UUsQ+sncsuST6eGe3B5Se7purqhGcWrkyIwUnQM/jAAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YThAAAAAAAAAAEAAAABAAAACXJldm9rZWQgMQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQCjDATJVQs3odl9fsqaxyx/18qrodZEDyYZAsdqg0GMx8CvLYt4xHENyVm7kyBRxOeh3EKfII0WFoYCV4mGZ/wU= ./tst-keys/revoked-0001.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001.pub
new file mode 100644
index 0000000..f561982
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0001.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YTh 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004
new file mode 100644
index 0000000..e50a4fe
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACC9GQuH6oGwYETnl9bDhD6+LjFTXz3Setm1aP870jEUMQAAAIiQzZzikM2c
+4gAAAAtzc2gtZWQyNTUxOQAAACC9GQuH6oGwYETnl9bDhD6+LjFTXz3Setm1aP870jEUMQ
+AAAEBpn5dxbvHhqAsSVN3IqRwzbFFgOhdmpkOP+nvoKq+rSr0ZC4fqgbBgROeX1sOEPr4u
+MVNfPdJ62bVo/zvSMRQxAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004-cert.pub
new file mode 100644
index 0000000..8e92fa7
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIC5jMLPDlEVbyPU/Icb04BF5jxN+OT8kpuO5c0CV6/AYAAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQxAAAAAAAAAAQAAAABAAAACXJldm9rZWQgNAAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQOH4yNn7+zyvsCV8BCoop5xYv4uFk27VZRjmscuy3J66KNwLay9XkvkRNArDaWBwH47dmkcU7F6fLLpY4vN2jgM= ./tst-keys/revoked-0004.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004.pub
new file mode 100644
index 0000000..1d7fe7f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0004.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQx 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010
new file mode 100644
index 0000000..fb457df
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBuSB/U+xWOO1SQ1KMjpQf4qgjeKTvHYDq8XJZijeUecAAAAIgvtSiML7Uo
+jAAAAAtzc2gtZWQyNTUxOQAAACBuSB/U+xWOO1SQ1KMjpQf4qgjeKTvHYDq8XJZijeUecA
+AAAECI2si7/SGjMM1UyhrFPXx4laQIfFUsb1+yfXKwQyeOXW5IH9T7FY47VJDUoyOlB/iq
+CN4pO8dgOrxclmKN5R5wAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010-cert.pub
new file mode 100644
index 0000000..9492f88
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIN2arXaBzVIdxAFfU+XU1Uc788HKlDH3tOLdDtcoORLmAAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5wAAAAAAAAAAoAAAABAAAACnJldm9rZWQgMTAAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDwhgQsYOG/eKf8EfH+fAmEW+88/ZJCmxAExEFPxkGL59waZcGiOJqquTKiqN5Kod8hpUrvZywrA0tjrRkYw8wH ./tst-keys/revoked-0010.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010.pub
new file mode 100644
index 0000000..37a0d84
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0010.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5w 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050
new file mode 100644
index 0000000..b02e9df
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDY4RuPhJ0MvGBy7HxPPiEMDVmVZ9RgWF++acMQQmRpsgAAAIgCZLe5AmS3
+uQAAAAtzc2gtZWQyNTUxOQAAACDY4RuPhJ0MvGBy7HxPPiEMDVmVZ9RgWF++acMQQmRpsg
+AAAEB9Q6rpWK04mQDoeKSB2I7p/rb8pu00ClhR+vRATl4TYdjhG4+EnQy8YHLsfE8+IQwN
+WZVn1GBYX75pwxBCZGmyAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050-cert.pub
new file mode 100644
index 0000000..90bb86f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIIecNj2Es6VfyCrhol4swP9lutvphd3seh+/b2LpD0EsAAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmyAAAAAAAAADIAAAABAAAACnJldm9rZWQgNTAAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEA2q8tCXV8FXkB0QWnFNWfCL7zz5jCXL9ZQADM1DaGi8oUU/dxmlQtWgMxuu5vNuvOYQGPDcBLj+by8VqAdvZMP ./tst-keys/revoked-0050.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050.pub
new file mode 100644
index 0000000..f3ad249
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0050.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmy 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090
new file mode 100644
index 0000000..efa3d5e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCV5vqoE7PrDvvc76GQcZEA9/Udq0KkAlXs8UKBoyrouQAAAIg3mgznN5oM
+5wAAAAtzc2gtZWQyNTUxOQAAACCV5vqoE7PrDvvc76GQcZEA9/Udq0KkAlXs8UKBoyrouQ
+AAAEAkRynGUH9n5hcp/S1WALvuIEDtbkMi2A7yNWze0o4gWpXm+qgTs+sO+9zvoZBxkQD3
+9R2rQqQCVezxQoGjKui5AAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090-cert.pub
new file mode 100644
index 0000000..26e61e0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIOjIztpPiaKY0hztHWtWpX+4LEoyy8qYPPT277K3bykSAAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5AAAAAAAAAFoAAAABAAAACnJldm9rZWQgOTAAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBUaAWyv/jZrbrCO5zw2HuZcWYBig8R2jdvkKr5yzWMWEVRtn97gnAUsIGxkgUnUAs3B2En2FH2NaicC1F1n3sF ./tst-keys/revoked-0090.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090.pub
new file mode 100644
index 0000000..e51b88c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0090.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500
new file mode 100644
index 0000000..900d444
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACC/PmkCo2vkm6sX1ketRQnLGwcNo2hfyh73KnT9hW6ekAAAAIhDam0PQ2pt
+DwAAAAtzc2gtZWQyNTUxOQAAACC/PmkCo2vkm6sX1ketRQnLGwcNo2hfyh73KnT9hW6ekA
+AAAED606GrYWlY7TOXcr8vAr3fjMtCtetdpwFHi2pzgf2Bbb8+aQKja+SbqxfWR61FCcsb
+Bw2jaF/KHvcqdP2Fbp6QAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500-cert.pub
new file mode 100644
index 0000000..0709618
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIIEblAg4b1eJ5KnT7KvYoOfe24La+nAKKLIYdsR6CdreAAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6QAAAAAAAAAfQAAAABAAAAC3Jldm9rZWQgNTAwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAc0WEuRfi9LG9uTfKY4Dh5MJCHUG7Dqp1J4S4Gs1iOzFX2YKgYXc0O+9j3jJ5/fB4z960Y1AxYR4TWEo1pNjzBQ== ./tst-keys/revoked-0500.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500.pub
new file mode 100644
index 0000000..13d1aa4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0500.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6Q 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510
new file mode 100644
index 0000000..a58675e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACADi+yFC2pU9iM6PU4EX9bdhzaWeVgmSR+aEQnfSytwTQAAAIgigF2AIoBd
+gAAAAAtzc2gtZWQyNTUxOQAAACADi+yFC2pU9iM6PU4EX9bdhzaWeVgmSR+aEQnfSytwTQ
+AAAEBWpyFpK0a+cdNPFMsvHTHtjBJpX4aMHxBAcEPN8hnpWAOL7IULalT2Izo9TgRf1t2H
+NpZ5WCZJH5oRCd9LK3BNAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510-cert.pub
new file mode 100644
index 0000000..1431af3
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAII8u8ho0YtDyXWYKv4WeOXSaRUxU8sUV0dQujB2J9VLaAAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BNAAAAAAAAAf4AAAABAAAAC3Jldm9rZWQgNTEwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABA3aijJnt8mJ8vLtr7H2PBVJHtNJpL6MQZNXHC6svzygIqZwEq3tDHGR00TPHaCYAqDEXQZysONciOQtQHzKXuBw== ./tst-keys/revoked-0510.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510.pub
new file mode 100644
index 0000000..33ad644
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0510.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BN 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520
new file mode 100644
index 0000000..630316c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBlWknnYT5Jdh68dZzjik9hOdgA7AXm2sMLPOzTdfj+ngAAAIghEm1OIRJt
+TgAAAAtzc2gtZWQyNTUxOQAAACBlWknnYT5Jdh68dZzjik9hOdgA7AXm2sMLPOzTdfj+ng
+AAAEDfVYURudvfzK3ZFx6T2O1CWi0emOZ0MYPcDzUVlu1WmGVaSedhPkl2Hrx1nOOKT2E5
+2ADsBebawws87NN1+P6eAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520-cert.pub
new file mode 100644
index 0000000..b290943
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAID/r9T2Sv0NGmlcHl6Fw8rVPIupmsqwq3WAG1NvW7WRcAAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6eAAAAAAAAAggAAAABAAAAC3Jldm9rZWQgNTIwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAF8zkeAqwtlxF4iy4mDEHkzVaRqcS0sZ57gcZBWGn/peGFy3MpSxlFQM/IC2pNZ7GuCVSIPV6rRLJC65YMMOEDQ== ./tst-keys/revoked-0520.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520.pub
new file mode 100644
index 0000000..fc13d37
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0520.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6e 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550
new file mode 100644
index 0000000..5e671b4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAscmsY+6UVQ3of4MSXpvQS4aFEChpylx3w6wfxrQtSgQAAAIj/9GKZ//Ri
+mQAAAAtzc2gtZWQyNTUxOQAAACAscmsY+6UVQ3of4MSXpvQS4aFEChpylx3w6wfxrQtSgQ
+AAAEDKC3eEgvCMy86rktq7VU1YQjjKY1iDFPVxWgKKcGJKkyxyaxj7pRVDeh/gxJem9BLh
+oUQKGnKXHfDrB/GtC1KBAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550-cert.pub
new file mode 100644
index 0000000..f529a91
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIF9q+Cg+9DSKt09eW1NXqVC4dZ3v80sZIYtc0/yqHRb+AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KBAAAAAAAAAiYAAAABAAAAC3Jldm9rZWQgNTUwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAovTuFOXLNCc4hQcI2hatXe2hbBQYbcnUo2BNdJ9EvIOsH/T0DzzEfRQajMQ+QD6oujIx7fb1Z2sRVPOAb3AcBg== ./tst-keys/revoked-0550.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550.pub
new file mode 100644
index 0000000..e09316a
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0550.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KB 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799
new file mode 100644
index 0000000..8edd736
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA/ehirY+MDfjsL60LID9+30FVF04QjC+/eqqhTS1QwgAAAAIjdntzR3Z7c
+0QAAAAtzc2gtZWQyNTUxOQAAACA/ehirY+MDfjsL60LID9+30FVF04QjC+/eqqhTS1QwgA
+AAAEDQEb+IFCIz+yvkhmrOQ85GafOm9ra0oNRontpox62UTj96GKtj4wN+OwvrQsgP37fQ
+VUXThCML796qqFNLVDCAAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799-cert.pub
new file mode 100644
index 0000000..80312fb
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIC1z1LkrZhMz1mBWPU8sJIuH59v+ig4OK/B4/x8jLAtUAAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCAAAAAAAAAAx8AAAABAAAAC3Jldm9rZWQgNzk5AAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABASNkJSbdRDARfgbqPOnuES0o6m6VZ7RC2XLPm3uwTqCvMqtHbFvq9etMddSUIR4XXah6ef+O7CJDk/Yjpkn+2CA== ./tst-keys/revoked-0799.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799.pub
new file mode 100644
index 0000000..1f0556c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0799.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCA 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999
new file mode 100644
index 0000000..f05a1e4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBIdwRXE815oNRYuS71olA1jkbB81YVApvHpKxCRuvugAAAAIgzBpObMwaT
+mwAAAAtzc2gtZWQyNTUxOQAAACBIdwRXE815oNRYuS71olA1jkbB81YVApvHpKxCRuvugA
+AAAECxY5wx3XKIhMT+ajMZXPl51x8rkCPBq6gUgZV3Uqpu7Eh3BFcTzXmg1Fi5LvWiUDWO
+RsHzVhUCm8ekrEJG6+6AAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999-cert.pub
new file mode 100644
index 0000000..4aedb77
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGt3nV/XJmtz9sQGP2fiZiKOH7mkPhezN3S+8TnsVcQjAAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6AAAAAAAAAA+cAAAABAAAAC3Jldm9rZWQgOTk5AAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAvLVCRCs7CV0JSXYL8ge4iRxL4y48bYuvu3YimKZDg7NdCXqw/jkaCsxJykRzb/xVnQDoNVCQQuzydt/I13FdBA== ./tst-keys/revoked-0999.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999.pub
new file mode 100644
index 0000000..c837fe0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-0999.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6A 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca
new file mode 100644
index 0000000..47e01fb
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAIgok4I2KJOC
+NgAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzg
+AAAEAEN+knz2qOyj+jbY+SJSHYQhlJoB1u9jLqoQoiAerI3hbReZevLKczhayKUADRdAvZ
+5DXVzAJpQkcB4MPdQu/OAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca.pub
new file mode 100644
index 0000000..2b92f89
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/O 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2
new file mode 100644
index 0000000..770ceee
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDsEBCX5jBwggkpt4XZXct1fOhDBuvgLL0KMpGoHRtj9wAAAIjMEwOtzBMD
+rQAAAAtzc2gtZWQyNTUxOQAAACDsEBCX5jBwggkpt4XZXct1fOhDBuvgLL0KMpGoHRtj9w
+AAAEAurE2/d7VhoEJeNFdDnVS7lpBRoMe/zAjA8dJRP1Z/I+wQEJfmMHCCCSm3hdldy3V8
+6EMG6+AsvQoykagdG2P3AAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2.pub
new file mode 100644
index 0000000..a177fd0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-ca2.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOwQEJfmMHCCCSm3hdldy3V86EMG6+AsvQoykagdG2P3 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-hash b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-hash
new file mode 100644
index 0000000..c6f2361
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-hash
@@ -0,0 +1,11 @@
+hash: SHA256:RvNFBEc/N9jsm3toDkitgr/wnWu/6qWBHo4Xmh5ZUpM
+hash: SHA256:qu2IwCnItWWX+orXv0rjCeT4i++2O6ViTzLye6kyWzU
+hash: SHA256:qQTACAkAJxYk1zvSQ+Rx9wa2IuOFJKtaEy/XwxM89J0
+hash: SHA256:Fe4GdmipzulS9oMB/h3U69tSm5wil6bTUKSJCT+Jf3E
+hash: SHA256:esUK/whZ5oJeRFNeOrHK1bbx9dKC+nRITZ7up7HJaGA
+hash: SHA256:xkii+r6t9rEBFYkx1b3dGNXzEs69M5NUMfHP05ypSdI
+hash: SHA256:lZrSycKcBNvUafU9y4R0EEbDaQWqMFvIGM9M+VKt2zk
+hash: SHA256:/2bgZOiYEH2UVahUllNaQ5P0advEB7liCPkp+aNVKDk
+hash: SHA256:He3c0W5o/P1I0pK5/VusqD5V6duAMeZl6f+6Yy5P1z0
+hash: SHA256:5V5Xw2lgcAGR8dO9cbgRmCNlhcCsBBv/hmEstKsqKr4
+hash: SHA256:T7s26JPzzRP2WHOcw3OjLwWo8ZZTkfo2jBCrRfJ6BR4
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-keyid b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-keyid
new file mode 100644
index 0000000..592ddb4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-keyid
@@ -0,0 +1,512 @@
+id: revoked 1
+id: revoked 2
+id: revoked 3
+id: revoked 4
+id: revoked 10
+id: revoked 15
+id: revoked 30
+id: revoked 50
+id: revoked 90
+id: revoked 300
+id: revoked 301
+id: revoked 302
+id: revoked 303
+id: revoked 304
+id: revoked 305
+id: revoked 306
+id: revoked 307
+id: revoked 308
+id: revoked 309
+id: revoked 310
+id: revoked 311
+id: revoked 312
+id: revoked 313
+id: revoked 314
+id: revoked 315
+id: revoked 316
+id: revoked 317
+id: revoked 318
+id: revoked 319
+id: revoked 320
+id: revoked 321
+id: revoked 322
+id: revoked 323
+id: revoked 324
+id: revoked 325
+id: revoked 326
+id: revoked 327
+id: revoked 328
+id: revoked 329
+id: revoked 330
+id: revoked 331
+id: revoked 332
+id: revoked 333
+id: revoked 334
+id: revoked 335
+id: revoked 336
+id: revoked 337
+id: revoked 338
+id: revoked 339
+id: revoked 340
+id: revoked 341
+id: revoked 342
+id: revoked 343
+id: revoked 344
+id: revoked 345
+id: revoked 346
+id: revoked 347
+id: revoked 348
+id: revoked 349
+id: revoked 350
+id: revoked 351
+id: revoked 352
+id: revoked 353
+id: revoked 354
+id: revoked 355
+id: revoked 356
+id: revoked 357
+id: revoked 358
+id: revoked 359
+id: revoked 360
+id: revoked 361
+id: revoked 362
+id: revoked 363
+id: revoked 364
+id: revoked 365
+id: revoked 366
+id: revoked 367
+id: revoked 368
+id: revoked 369
+id: revoked 370
+id: revoked 371
+id: revoked 372
+id: revoked 373
+id: revoked 374
+id: revoked 375
+id: revoked 376
+id: revoked 377
+id: revoked 378
+id: revoked 379
+id: revoked 380
+id: revoked 381
+id: revoked 382
+id: revoked 383
+id: revoked 384
+id: revoked 385
+id: revoked 386
+id: revoked 387
+id: revoked 388
+id: revoked 389
+id: revoked 390
+id: revoked 391
+id: revoked 392
+id: revoked 393
+id: revoked 394
+id: revoked 395
+id: revoked 396
+id: revoked 397
+id: revoked 398
+id: revoked 399
+id: revoked 400
+id: revoked 401
+id: revoked 402
+id: revoked 403
+id: revoked 404
+id: revoked 405
+id: revoked 406
+id: revoked 407
+id: revoked 408
+id: revoked 409
+id: revoked 410
+id: revoked 411
+id: revoked 412
+id: revoked 413
+id: revoked 414
+id: revoked 415
+id: revoked 416
+id: revoked 417
+id: revoked 418
+id: revoked 419
+id: revoked 420
+id: revoked 421
+id: revoked 422
+id: revoked 423
+id: revoked 424
+id: revoked 425
+id: revoked 426
+id: revoked 427
+id: revoked 428
+id: revoked 429
+id: revoked 430
+id: revoked 431
+id: revoked 432
+id: revoked 433
+id: revoked 434
+id: revoked 435
+id: revoked 436
+id: revoked 437
+id: revoked 438
+id: revoked 439
+id: revoked 440
+id: revoked 441
+id: revoked 442
+id: revoked 443
+id: revoked 444
+id: revoked 445
+id: revoked 446
+id: revoked 447
+id: revoked 448
+id: revoked 449
+id: revoked 450
+id: revoked 451
+id: revoked 452
+id: revoked 453
+id: revoked 454
+id: revoked 455
+id: revoked 456
+id: revoked 457
+id: revoked 458
+id: revoked 459
+id: revoked 460
+id: revoked 461
+id: revoked 462
+id: revoked 463
+id: revoked 464
+id: revoked 465
+id: revoked 466
+id: revoked 467
+id: revoked 468
+id: revoked 469
+id: revoked 470
+id: revoked 471
+id: revoked 472
+id: revoked 473
+id: revoked 474
+id: revoked 475
+id: revoked 476
+id: revoked 477
+id: revoked 478
+id: revoked 479
+id: revoked 480
+id: revoked 481
+id: revoked 482
+id: revoked 483
+id: revoked 484
+id: revoked 485
+id: revoked 486
+id: revoked 487
+id: revoked 488
+id: revoked 489
+id: revoked 490
+id: revoked 491
+id: revoked 492
+id: revoked 493
+id: revoked 494
+id: revoked 495
+id: revoked 496
+id: revoked 497
+id: revoked 498
+id: revoked 500
+id: revoked 501
+id: revoked 502
+id: revoked 503
+id: revoked 504
+id: revoked 505
+id: revoked 506
+id: revoked 507
+id: revoked 508
+id: revoked 509
+id: revoked 510
+id: revoked 511
+id: revoked 512
+id: revoked 513
+id: revoked 514
+id: revoked 515
+id: revoked 516
+id: revoked 517
+id: revoked 518
+id: revoked 519
+id: revoked 520
+id: revoked 521
+id: revoked 522
+id: revoked 523
+id: revoked 524
+id: revoked 525
+id: revoked 526
+id: revoked 527
+id: revoked 528
+id: revoked 529
+id: revoked 530
+id: revoked 531
+id: revoked 532
+id: revoked 533
+id: revoked 534
+id: revoked 535
+id: revoked 536
+id: revoked 537
+id: revoked 538
+id: revoked 539
+id: revoked 540
+id: revoked 541
+id: revoked 542
+id: revoked 543
+id: revoked 544
+id: revoked 545
+id: revoked 546
+id: revoked 547
+id: revoked 548
+id: revoked 549
+id: revoked 550
+id: revoked 551
+id: revoked 552
+id: revoked 553
+id: revoked 554
+id: revoked 555
+id: revoked 556
+id: revoked 557
+id: revoked 558
+id: revoked 559
+id: revoked 560
+id: revoked 561
+id: revoked 562
+id: revoked 563
+id: revoked 564
+id: revoked 565
+id: revoked 566
+id: revoked 567
+id: revoked 568
+id: revoked 569
+id: revoked 570
+id: revoked 571
+id: revoked 572
+id: revoked 573
+id: revoked 574
+id: revoked 575
+id: revoked 576
+id: revoked 577
+id: revoked 578
+id: revoked 579
+id: revoked 580
+id: revoked 581
+id: revoked 582
+id: revoked 583
+id: revoked 584
+id: revoked 585
+id: revoked 586
+id: revoked 587
+id: revoked 588
+id: revoked 589
+id: revoked 590
+id: revoked 591
+id: revoked 592
+id: revoked 593
+id: revoked 594
+id: revoked 595
+id: revoked 596
+id: revoked 597
+id: revoked 598
+id: revoked 599
+id: revoked 600
+id: revoked 601
+id: revoked 602
+id: revoked 603
+id: revoked 604
+id: revoked 605
+id: revoked 606
+id: revoked 607
+id: revoked 608
+id: revoked 609
+id: revoked 610
+id: revoked 611
+id: revoked 612
+id: revoked 613
+id: revoked 614
+id: revoked 615
+id: revoked 616
+id: revoked 617
+id: revoked 618
+id: revoked 619
+id: revoked 620
+id: revoked 621
+id: revoked 622
+id: revoked 623
+id: revoked 624
+id: revoked 625
+id: revoked 626
+id: revoked 627
+id: revoked 628
+id: revoked 629
+id: revoked 630
+id: revoked 631
+id: revoked 632
+id: revoked 633
+id: revoked 634
+id: revoked 635
+id: revoked 636
+id: revoked 637
+id: revoked 638
+id: revoked 639
+id: revoked 640
+id: revoked 641
+id: revoked 642
+id: revoked 643
+id: revoked 644
+id: revoked 645
+id: revoked 646
+id: revoked 647
+id: revoked 648
+id: revoked 649
+id: revoked 650
+id: revoked 651
+id: revoked 652
+id: revoked 653
+id: revoked 654
+id: revoked 655
+id: revoked 656
+id: revoked 657
+id: revoked 658
+id: revoked 659
+id: revoked 660
+id: revoked 661
+id: revoked 662
+id: revoked 663
+id: revoked 664
+id: revoked 665
+id: revoked 666
+id: revoked 667
+id: revoked 668
+id: revoked 669
+id: revoked 670
+id: revoked 671
+id: revoked 672
+id: revoked 673
+id: revoked 674
+id: revoked 675
+id: revoked 676
+id: revoked 677
+id: revoked 678
+id: revoked 679
+id: revoked 680
+id: revoked 681
+id: revoked 682
+id: revoked 683
+id: revoked 684
+id: revoked 685
+id: revoked 686
+id: revoked 687
+id: revoked 688
+id: revoked 689
+id: revoked 690
+id: revoked 691
+id: revoked 692
+id: revoked 693
+id: revoked 694
+id: revoked 695
+id: revoked 696
+id: revoked 697
+id: revoked 698
+id: revoked 699
+id: revoked 700
+id: revoked 701
+id: revoked 702
+id: revoked 703
+id: revoked 704
+id: revoked 705
+id: revoked 706
+id: revoked 707
+id: revoked 708
+id: revoked 709
+id: revoked 710
+id: revoked 711
+id: revoked 712
+id: revoked 713
+id: revoked 714
+id: revoked 715
+id: revoked 716
+id: revoked 717
+id: revoked 718
+id: revoked 719
+id: revoked 720
+id: revoked 721
+id: revoked 722
+id: revoked 723
+id: revoked 724
+id: revoked 725
+id: revoked 726
+id: revoked 727
+id: revoked 728
+id: revoked 729
+id: revoked 730
+id: revoked 731
+id: revoked 732
+id: revoked 733
+id: revoked 734
+id: revoked 735
+id: revoked 736
+id: revoked 737
+id: revoked 738
+id: revoked 739
+id: revoked 740
+id: revoked 741
+id: revoked 742
+id: revoked 743
+id: revoked 744
+id: revoked 745
+id: revoked 746
+id: revoked 747
+id: revoked 748
+id: revoked 749
+id: revoked 750
+id: revoked 751
+id: revoked 752
+id: revoked 753
+id: revoked 754
+id: revoked 755
+id: revoked 756
+id: revoked 757
+id: revoked 758
+id: revoked 759
+id: revoked 760
+id: revoked 761
+id: revoked 762
+id: revoked 763
+id: revoked 764
+id: revoked 765
+id: revoked 766
+id: revoked 767
+id: revoked 768
+id: revoked 769
+id: revoked 770
+id: revoked 771
+id: revoked 772
+id: revoked 773
+id: revoked 774
+id: revoked 775
+id: revoked 776
+id: revoked 777
+id: revoked 778
+id: revoked 779
+id: revoked 780
+id: revoked 781
+id: revoked 782
+id: revoked 783
+id: revoked 784
+id: revoked 785
+id: revoked 786
+id: revoked 787
+id: revoked 788
+id: revoked 789
+id: revoked 790
+id: revoked 791
+id: revoked 792
+id: revoked 793
+id: revoked 794
+id: revoked 795
+id: revoked 796
+id: revoked 797
+id: revoked 798
+id: revoked 799
+id: revoked 999
+id: revoked 1000
+id: revoked 1001
+id: revoked 1002
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-serials b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-serials
new file mode 100644
index 0000000..b20fec2
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-serials
@@ -0,0 +1,19 @@
+serial: 1-4
+serial: 10
+serial: 15
+serial: 30
+serial: 50
+serial: 90
+serial: 999
+# The following sum to 500-799
+serial: 500
+serial: 501
+serial: 502
+serial: 503-600
+serial: 700-797
+serial: 798
+serial: 799
+serial: 599-701
+# Some multiple consecutive serial number ranges
+serial: 10000-20000
+serial: 30000-40000
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha1 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha1
new file mode 100644
index 0000000..475e90c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha1
@@ -0,0 +1,11 @@
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YTh 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQx 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5w 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmy 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6Q 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BN 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6e 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KB 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCA 
+sha1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6A 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha256 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha256
new file mode 100644
index 0000000..13109e9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/revoked-sha256
@@ -0,0 +1,11 @@
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC29QZ72HtyCdaNo6p2GH3fJpUynwkvs8Acwn66G7YTh 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0ZC4fqgbBgROeX1sOEPr4uMVNfPdJ62bVo/zvSMRQx 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG5IH9T7FY47VJDUoyOlB/iqCN4pO8dgOrxclmKN5R5w 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjhG4+EnQy8YHLsfE8+IQwNWZVn1GBYX75pwxBCZGmy 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXm+qgTs+sO+9zvoZBxkQD39R2rQqQCVezxQoGjKui5 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8+aQKja+SbqxfWR61FCcsbBw2jaF/KHvcqdP2Fbp6Q 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOL7IULalT2Izo9TgRf1t2HNpZ5WCZJH5oRCd9LK3BN 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGVaSedhPkl2Hrx1nOOKT2E52ADsBebawws87NN1+P6e 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICxyaxj7pRVDeh/gxJem9BLhoUQKGnKXHfDrB/GtC1KB 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID96GKtj4wN+OwvrQsgP37fQVUXThCML796qqFNLVDCA 
+sha256: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEh3BFcTzXmg1Fi5LvWiUDWORsHzVhUCm8ekrEJG6+6A 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005
new file mode 100644
index 0000000..d82a0b5
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDqONQediveIXoseoT+MWp9yEdMO7hP7F4fAno6gunyoAAAAIig1MZroNTG
+awAAAAtzc2gtZWQyNTUxOQAAACDqONQediveIXoseoT+MWp9yEdMO7hP7F4fAno6gunyoA
+AAAEBSEPLoX4NVkAchYZEGi7hjd5NoVBWuoxqluCGt/fWrYeo41B52K94heix6hP4xan3I
+R0w7uE/sXh8CejqC6fKgAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005-cert.pub
new file mode 100644
index 0000000..59ea422
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGnzDhP/hp83ipkW8T7f0CIXJuPK7ldbJFKDUrkvn6J1AAAAIOo41B52K94heix6hP4xan3IR0w7uE/sXh8CejqC6fKgAAAAAAAAAAUAAAABAAAACXJldm9rZWQgNQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQO9W58IrK+I0o2us9Hs/QBkrEe1YIgl6PzCMsu/Zu/tdZxGDK5Pxoz7tKzXezS9LPGQfZ3fVdl58PZC1DtxQ5gU= ./tst-keys/unrevoked-0005.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005.pub
new file mode 100644
index 0000000..081ac6c
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0005.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOo41B52K94heix6hP4xan3IR0w7uE/sXh8CejqC6fKg 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009
new file mode 100644
index 0000000..9479498
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDXQqTeALQCMo64B4EX5abjRvrjVu69Mnxgg2q0SB5/oQAAAIgIqeXLCKnl
+ywAAAAtzc2gtZWQyNTUxOQAAACDXQqTeALQCMo64B4EX5abjRvrjVu69Mnxgg2q0SB5/oQ
+AAAECubGChJGu90ZNiP/zF+tTtr0+l7y8BrTDMQ0m0+cU0qtdCpN4AtAIyjrgHgRflpuNG
++uNW7r0yfGCDarRIHn+hAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009-cert.pub
new file mode 100644
index 0000000..9ee8890
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIERRY0M1bHm2Qjyo105OCHWp0UCRHLP0xkMuHnkMDP5eAAAAINdCpN4AtAIyjrgHgRflpuNG+uNW7r0yfGCDarRIHn+hAAAAAAAAAAkAAAABAAAACXJldm9rZWQgOQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQFsA4xJHRCXSyq6GHkKdemfbg+jvUZxHlu/UBoZf4esEHAtx0mXiajbUwkWzkh1vCtxZNZhiLIhxqDcNMu+O+wo= ./tst-keys/unrevoked-0009.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009.pub
new file mode 100644
index 0000000..74a797b
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0009.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINdCpN4AtAIyjrgHgRflpuNG+uNW7r0yfGCDarRIHn+h 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014
new file mode 100644
index 0000000..6fa4fd9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACDvTTMHyjozzZabuUzy61XOKBm4klUjUGSWYtX6T4XtEwAAAIhyFdxYchXc
+WAAAAAtzc2gtZWQyNTUxOQAAACDvTTMHyjozzZabuUzy61XOKBm4klUjUGSWYtX6T4XtEw
+AAAEBtC+f4bz1/qtq5K2Rf+0bPeY3P0OWdD3rvrlGPh8wN5u9NMwfKOjPNlpu5TPLrVc4o
+GbiSVSNQZJZi1fpPhe0TAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014-cert.pub
new file mode 100644
index 0000000..bb954f9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIPes2n/Xk4mm4OpuvHDqx9+76vm+SmFgc9d7ATGT1+C8AAAAIO9NMwfKOjPNlpu5TPLrVc4oGbiSVSNQZJZi1fpPhe0TAAAAAAAAAA4AAAABAAAACnJldm9rZWQgMTQAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEDGVORypw3DoMuWBu0V4cH/OgRBstD5cY37CfLrVZpmGv9jDRXVNQee7vYowk0r3XvQPoUecQBIMZGAQtEiw18E ./tst-keys/unrevoked-0014.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014.pub
new file mode 100644
index 0000000..4a866e4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0014.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO9NMwfKOjPNlpu5TPLrVc4oGbiSVSNQZJZi1fpPhe0T 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016
new file mode 100644
index 0000000..62d5027
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBWKMDlSwSGo4dcBAZmL+Xxk64Wp/ZfFSu2vkp82JXQCQAAAIjUcNt51HDb
+eQAAAAtzc2gtZWQyNTUxOQAAACBWKMDlSwSGo4dcBAZmL+Xxk64Wp/ZfFSu2vkp82JXQCQ
+AAAEC1V7PD5tJSOUZtpfqVfWyiSIMJkCDFZzTmFs7GBpJE71YowOVLBIajh1wEBmYv5fGT
+rhan9l8VK7a+SnzYldAJAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016-cert.pub
new file mode 100644
index 0000000..367e4ab
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAICGqa0xwr0etbKquuBy5/hYQ/rbMrKfEE6XShgb4YWpUAAAAIFYowOVLBIajh1wEBmYv5fGTrhan9l8VK7a+SnzYldAJAAAAAAAAABAAAAABAAAACnJldm9rZWQgMTYAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBKVetE3dsch2wjMIHGoiH8zp6gFMn1KgGKn01EPc1A08a/JKNvaSDYhlARLjiBzjIUGlykhHTTr4EcHTPWl58P ./tst-keys/unrevoked-0016.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016.pub
new file mode 100644
index 0000000..47cac1e
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0016.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYowOVLBIajh1wEBmYv5fGTrhan9l8VK7a+SnzYldAJ 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029
new file mode 100644
index 0000000..589daa6
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA3B1NQ9RFEkJUGcIUcCL22yMVEeob8/PUsk9lYH43vPwAAAIjxPrzV8T68
+1QAAAAtzc2gtZWQyNTUxOQAAACA3B1NQ9RFEkJUGcIUcCL22yMVEeob8/PUsk9lYH43vPw
+AAAED89ht9KdlYRfsKwh+pzh6BOvPf/U58QBkw1d3LfKnn+jcHU1D1EUSQlQZwhRwIvbbI
+xUR6hvz89SyT2Vgfje8/AAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029-cert.pub
new file mode 100644
index 0000000..1bf3883
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIEVLRuchC4z7/EqITmyqCxOyhC7/enmFWsalP8FFFYiXAAAAIDcHU1D1EUSQlQZwhRwIvbbIxUR6hvz89SyT2Vgfje8/AAAAAAAAAB0AAAABAAAACnJldm9rZWQgMjkAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEChRFz/Zb6b3znoIWJjd8OTmCIEH7YE/fKWtyWHoGjz02G4VnCfwuHp23yD+k1XsoOGC7xcSnQeqZ19160HDNgC ./tst-keys/unrevoked-0029.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029.pub
new file mode 100644
index 0000000..4072d92
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0029.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDcHU1D1EUSQlQZwhRwIvbbIxUR6hvz89SyT2Vgfje8/ 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049
new file mode 100644
index 0000000..b5788a0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACD2mB5GBuavtb/bX7W54OmUCCJzUWBwG7cQ4q/jon1MBQAAAIjRkEU40ZBF
+OAAAAAtzc2gtZWQyNTUxOQAAACD2mB5GBuavtb/bX7W54OmUCCJzUWBwG7cQ4q/jon1MBQ
+AAAECuUtJb+T0um2mGvjD/ZZpbtjIhWc3jGVbzuDnEovOjnPaYHkYG5q+1v9tftbng6ZQI
+InNRYHAbtxDir+OifUwFAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049-cert.pub
new file mode 100644
index 0000000..587cf62
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAILZPLEL5xQ8HDLa8pJhchJ3EEhZcjMqACCAEeL+U6c/QAAAAIPaYHkYG5q+1v9tftbng6ZQIInNRYHAbtxDir+OifUwFAAAAAAAAADEAAAABAAAACnJldm9rZWQgNDkAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEB2GglzoC1VgsYNAVd5BDsLbeR5M5hHcVVvNsGnK1QCXMj56cgfkbXLj6W6tjJEEFY4G+KPJh1F/SGJi02P5lkJ ./tst-keys/unrevoked-0049.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049.pub
new file mode 100644
index 0000000..07d5369
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0049.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPaYHkYG5q+1v9tftbng6ZQIInNRYHAbtxDir+OifUwF 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051
new file mode 100644
index 0000000..52d3283
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACD39ygfAlHPhZWU8inWu1hypIlQTChQxSKKB6iaV6Q0lQAAAIgMawsqDGsL
+KgAAAAtzc2gtZWQyNTUxOQAAACD39ygfAlHPhZWU8inWu1hypIlQTChQxSKKB6iaV6Q0lQ
+AAAEB4Ng9MekhsMKYDaBcOUWdxmi1rjgCsPOOfpABTxiCef/f3KB8CUc+FlZTyKda7WHKk
+iVBMKFDFIooHqJpXpDSVAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051-cert.pub
new file mode 100644
index 0000000..5b4bd11
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGTNYRrlJ1vExK7dume319Krn4YW6wyZc4PzZLjZoB8zAAAAIPf3KB8CUc+FlZTyKda7WHKkiVBMKFDFIooHqJpXpDSVAAAAAAAAADMAAAABAAAACnJldm9rZWQgNTEAAAAAAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIBbReZevLKczhayKUADRdAvZ5DXVzAJpQkcB4MPdQu/OAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEAgUiwWKerMo8nuejTER/EmM6ZUpmXjgFwPCpb1LAxBJH71iOnyF9S0gp+CSmjqiTS2yuQajSMen64wOdJCX7wF ./tst-keys/unrevoked-0051.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051.pub
new file mode 100644
index 0000000..88867e5
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0051.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPf3KB8CUc+FlZTyKda7WHKkiVBMKFDFIooHqJpXpDSV 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499
new file mode 100644
index 0000000..8f59be9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCpwI1aCbAOVvA7NJhLtBNpR4tiGGtTQ019wjKL6zJ/uQAAAIhllrzrZZa8
+6wAAAAtzc2gtZWQyNTUxOQAAACCpwI1aCbAOVvA7NJhLtBNpR4tiGGtTQ019wjKL6zJ/uQ
+AAAECQ6o+3J9W3wXFWEcrPJl5qJZudUPmPdKF7SYxcMTrVP6nAjVoJsA5W8Ds0mEu0E2lH
+i2IYa1NDTX3CMovrMn+5AAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499-cert.pub
new file mode 100644
index 0000000..a6e76f1
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIJvt1IxZsGIIS9DDCCKiD13Dbs5Af5ouews+YwZ9FoydAAAAIKnAjVoJsA5W8Ds0mEu0E2lHi2IYa1NDTX3CMovrMn+5AAAAAAAAAfMAAAABAAAAC3Jldm9rZWQgNDk5AAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABAMaA4UjND4LX9kdHjhgWJjGzzs/xUBwxQQcAmNgwmmQzmkwj8ctWBBA1+TkBMcZbSNUWBdclT4UcnDPEYqG1NBg== ./tst-keys/unrevoked-0499.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499.pub
new file mode 100644
index 0000000..5a3acbb
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0499.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKnAjVoJsA5W8Ds0mEu0E2lHi2IYa1NDTX3CMovrMn+5 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800
new file mode 100644
index 0000000..9684d72
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAn5h8A2vYJ1+IWVtdLMulUQKCqlVLHpcHEFqYC5gtGlwAAAIh2lf7UdpX+
+1AAAAAtzc2gtZWQyNTUxOQAAACAn5h8A2vYJ1+IWVtdLMulUQKCqlVLHpcHEFqYC5gtGlw
+AAAEAEXGgMPKs3HwkQmNdVkbO3PcaBVCBEv1l8yy/ly30jPSfmHwDa9gnX4hZW10sy6VRA
+oKqVUselwcQWpgLmC0aXAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800-cert.pub
new file mode 100644
index 0000000..ab47a2b
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIPAKFTJ25v9CsCppsQ/FwXAZgntAIdQHUXo0KQ3FrlTzAAAAICfmHwDa9gnX4hZW10sy6VRAoKqVUselwcQWpgLmC0aXAAAAAAAAAyAAAAABAAAAC3Jldm9rZWQgODAwAAAAAAAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAW0XmXryynM4WsilAA0XQL2eQ11cwCaUJHAeDD3ULvzgAAAFMAAAALc3NoLWVkMjU1MTkAAABA16aKfsgD0iZ+qc2b1AxBHZ/nyczN2Xjbhg4eJm/6cPSkBHs8uan5e8yPBIQJq2LztC3If6Z6PARoWUnIKb43CQ== ./tst-keys/unrevoked-0800.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800.pub
new file mode 100644
index 0000000..3a41f29
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-0800.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICfmHwDa9gnX4hZW10sy6VRAoKqVUselwcQWpgLmC0aX 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010
new file mode 100644
index 0000000..89df717
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACAg0jawQzRMO/ESfFm6yDc66J5kjasOqTb7rmQSU6Nk3QAAAIhczXMoXM1z
+KAAAAAtzc2gtZWQyNTUxOQAAACAg0jawQzRMO/ESfFm6yDc66J5kjasOqTb7rmQSU6Nk3Q
+AAAEAdeQiqpyZqBaffmgy+UrvFVpygD0n8isn3zjumVNtKxiDSNrBDNEw78RJ8WbrINzro
+nmSNqw6pNvuuZBJTo2TdAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010-cert.pub
new file mode 100644
index 0000000..2d0fe53
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIITg9nSjjofIKXTKf2byvYL3Ce43PP9Dtrbj/+AlfgEtAAAAICDSNrBDNEw78RJ8WbrINzronmSNqw6pNvuuZBJTo2TdAAAAAAAAA/IAAAABAAAADHJldm9rZWQgMTAxMAAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQIndHhKILtU0+FkKKw1KmhaHQS3p1KiQdld/2P5jpcEgb292iY+ICU+aHXKvS8qGM2aMImv8835NEyWy/MB74QM= ./tst-keys/unrevoked-1010.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010.pub
new file mode 100644
index 0000000..05c5eac
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1010.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICDSNrBDNEw78RJ8WbrINzronmSNqw6pNvuuZBJTo2Td 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011 b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011
new file mode 100644
index 0000000..38b8232
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCd4IBQx9BhO9FzYMOKu3cKgBcwUwb7XzS3uI26RgmEYgAAAIjHvhtux74b
+bgAAAAtzc2gtZWQyNTUxOQAAACCd4IBQx9BhO9FzYMOKu3cKgBcwUwb7XzS3uI26RgmEYg
+AAAEBsteyDUYUNwgY3SMkMs0guy8MJfek2kuvH35zEpVf6Hp3ggFDH0GE70XNgw4q7dwqA
+FzBTBvtfNLe4jbpGCYRiAAAAAAECAwQF
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011-cert.pub
new file mode 100644
index 0000000..4671638
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIMjD2+xjmUC1VviOH+peT9C81Y4xjyTue/F69nFKmQBMAAAAIJ3ggFDH0GE70XNgw4q7dwqAFzBTBvtfNLe4jbpGCYRiAAAAAAAAA/MAAAABAAAADHJldm9rZWQgMTAxMQAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgFtF5l68spzOFrIpQANF0C9nkNdXMAmlCRwHgw91C784AAABTAAAAC3NzaC1lZDI1NTE5AAAAQNENdVFCE02X6z+wFJtm2DQcgdc4oov9DyFKLPqLrogo+pVao5QwOkeJ2J/tmp40H2+uP/jrDlQuCvOcoQGHqwY= ./tst-keys/unrevoked-1011.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011.pub
new file mode 100644
index 0000000..0809077
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/krl/unrevoked-1011.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ3ggFDH0GE70XNgw4q7dwqAFzBTBvtfNLe4jbpGCYRi 
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key
new file mode 100644
index 0000000..ee3f922
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACA3ivU7wf37jE1ITC5KQjVeVlyFTkgWJxub8t380ovjiwAAAJDdMhQO3TIU
+DgAAAAtzc2gtZWQyNTUxOQAAACA3ivU7wf37jE1ITC5KQjVeVlyFTkgWJxub8t380ovjiw
+AAAEA4NlTFs3h2zqt5pSZ5S3dJb42GE7EjG16coKj70eELNDeK9TvB/fuMTUhMLkpCNV5W
+XIVOSBYnG5vy3fzSi+OLAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub
new file mode 100644
index 0000000..2be08be
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIGXo4+L/NyBl1VQDP39PxJP3LSzaqopqZGVP3cG0WoFAAAAAIDeK9TvB/fuMTUhMLkpCNV5WXIVOSBYnG5vy3fzSi+OLAAAAAAAAAAUAAAABAAAABnRlc3RlcgAAABYAAAASdGVzdGVyQGV4YW1wbGUuY29tAAAAAGbTroAAAAAAZyLIgAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIMdEl+iOTEbf1RC3uicECtid+SaIMsAw7wrlWhOQTyBVAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEA/HwKB8J/kvkEsdxDou+UebnR9u30xPH6FEnbHLlfKbKMIXwLFIHnf9F6bTL36WhFDEDcSBGS19VBWBDRosM8L
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub
new file mode 100644
index 0000000..0255005
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/other_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDeK9TvB/fuMTUhMLkpCNV5WXIVOSBYnG5vy3fzSi+OL
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle
new file mode 100644
index 0000000..c402f54
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/repo.bundle
Binary files differ
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key
new file mode 100644
index 0000000..3dd37be
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBgEzmfD3DinWPe/H8yLLZ2dPhbnnyFiqe8EWcp0C3czgAAAJDhSMqA4UjK
+gAAAAAtzc2gtZWQyNTUxOQAAACBgEzmfD3DinWPe/H8yLLZ2dPhbnnyFiqe8EWcp0C3czg
+AAAEB1yC00NMYEAVzhDj9odGVL0EonaIkf5jdUZ/czJ0+SPWATOZ8PcOKdY978fzIstnZ0
++FuefIWKp7wRZynQLdzOAAAADVRIV09AU0VBR044MDA=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub
new file mode 100644
index 0000000..de191d1
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key-cert.pub
@@ -0,0 +1 @@
+ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIFEmoWkYraMju0JI0b/0RQtR6RYo/OVp53EVf48L/Pu/AAAAIBmHlkHFlA7HkoTZcau80PH5zduQu41m8BqnH/1v2BwVAAAAAAAAAAEAAAABAAAACGFfa2V5X2lkAAAAFgAAABJ0ZXN0ZXJAZXhhbXBsZS5jb20AAAAAZtOugAAAAABm1QAAAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg2ifM9NMuXwQf7H/H5LCMhMjVqugyyN+jmcMoJUL2YLAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQG1kXUido46YOnmwvkJuIAKyp6Q9Gr+lbdOQvU0St/Hc9HTTIxgDGyLpv0alIJpHOuSYUUUxDufvGKtLJK1duwg= ./signing_key.pub
diff --git a/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub
new file mode 100644
index 0000000..e1210e7
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst-rsrc/org/eclipse/jgit/internal/signing/ssh/signing_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java
new file mode 100644
index 0000000..fdfffce
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AbstractSshSignatureTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.time.ZoneOffset;
+
+import org.eclipse.jgit.api.CommitCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Common setup for SSH signature tests.
+ */
+public abstract class AbstractSshSignatureTest extends RepositoryTestCase {
+
+	@Rule
+	public TemporaryFolder keys = new TemporaryFolder();
+
+	protected File certs;
+
+	protected Instant commitTime;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		copyResource("allowed_signers", keys.getRoot());
+		copyResource("other_key", keys.getRoot());
+		copyResource("other_key.pub", keys.getRoot());
+		copyResource("other_key-cert.pub", keys.getRoot());
+		copyResource("signing_key", keys.getRoot());
+		copyResource("signing_key.pub", keys.getRoot());
+		certs = keys.newFolder("certs");
+		copyResource("certs/expired.cert", certs);
+		copyResource("certs/no_principals.cert", certs);
+		copyResource("certs/other.cert", certs);
+		copyResource("certs/other-ca.cert", certs);
+		copyResource("certs/tester.cert", certs);
+		copyResource("certs/two_principals.cert", certs);
+		Repository repo = db;
+		StoredConfig config = repo.getConfig();
+		config.setString("gpg", null, "format", "ssh");
+		config.setString("gpg", "ssh", "allowedSignersFile",
+				keys.getRoot().toPath().resolve("allowed_signers").toString()
+						.replace('\\', '/'));
+		config.save();
+		// Run all tests with commit times on 2024-10-02T12:00:00Z. The test
+		// certificates are valid from 2024-09-01 to 2024-10-31, except the
+		// "expired" certificate which is valid only on 2024-09-01.
+		commitTime = Instant.parse("2024-10-02T12:00:00.00Z");
+	}
+
+	private void copyResource(String name, File directory) throws IOException {
+		try (InputStream in = this.getClass().getResourceAsStream(name)) {
+			int i = name.lastIndexOf('/');
+			String fileName = i < 0 ? name : name.substring(i + 1);
+			Files.copy(in, directory.toPath().resolve(fileName));
+		}
+	}
+
+	protected RevCommit createSignedCommit(String certificate,
+			String signingKey) throws Exception {
+		Repository repo = db;
+		Path key = keys.getRoot().toPath().resolve(signingKey);
+		if (certificate != null) {
+			Files.copy(certs.toPath().resolve(certificate),
+					keys.getRoot().toPath().resolve(signingKey),
+					StandardCopyOption.REPLACE_EXISTING);
+		}
+		PersonIdent commitAuthor = new PersonIdent("tester",
+				"tester@example.com", commitTime, ZoneOffset.UTC);
+		try (Git git = Git.wrap(repo)) {
+			writeTrashFile("foo.txt", "foo");
+			git.add().addFilepattern("foo.txt").call();
+			CommitCommand commit = git.commit();
+			commit.setAuthor(commitAuthor);
+			commit.setCommitter(commitAuthor);
+			commit.setMessage("Message");
+			commit.setSign(Boolean.TRUE);
+			commit.setSigningKey(key.toAbsolutePath().toString());
+			return commit.call();
+		}
+	}
+
+	protected RevCommit checkSshSignature(RevCommit c) {
+		byte[] sig = c.getRawGpgSignature();
+		assertNotNull(sig);
+		String signature = new String(sig, StandardCharsets.US_ASCII);
+		assertTrue("Not an SSH signature:\n" + signature,
+				signature.startsWith(Constants.SSH_SIGNATURE_PREFIX));
+		return c;
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AllowedSignersParseTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AllowedSignersParseTest.java
new file mode 100644
index 0000000..84d8179
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/AllowedSignersParseTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import java.io.StreamCorruptedException;
+import java.time.Instant;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.eclipse.jgit.util.SystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the line parsing in {@link AllowedSigners}.
+ */
+public class AllowedSignersParseTest {
+
+	@Before
+	public void setup() {
+		// Uses GMT-03:30 as time zone.
+		SystemReader.setInstance(new MockSystemReader());
+	}
+
+	@After
+	public void tearDown() {
+		SystemReader.setInstance(null);
+	}
+
+	@Test
+	public void testValidDate() {
+		assertEquals(Instant.parse("2024-09-01T00:00:00.00Z"),
+				AllowedSigners.parseDate("20240901Z"));
+		assertEquals(Instant.parse("2024-09-01T01:02:00.00Z"),
+				AllowedSigners.parseDate("202409010102Z"));
+		assertEquals(Instant.parse("2024-09-01T01:02:03.00Z"),
+				AllowedSigners.parseDate("20240901010203Z"));
+		assertEquals(Instant.parse("2024-09-01T03:30:00.00Z"),
+				AllowedSigners.parseDate("20240901"));
+		assertEquals(Instant.parse("2024-09-01T04:32:00.00Z"),
+				AllowedSigners.parseDate("202409010102"));
+		assertEquals(Instant.parse("2024-09-01T04:32:03.00Z"),
+				AllowedSigners.parseDate("20240901010203"));
+	}
+
+	@Test
+	public void testInvalidDate() {
+		assertThrows(Exception.class, () -> AllowedSigners.parseDate("1234"));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parseDate("09/01/2024"));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parseDate("2024-09-01"));
+	}
+
+	private void checkValidKey(String expected, String input, int from)
+			throws StreamCorruptedException {
+		assertEquals(expected, AllowedSigners.parsePublicKey(input, from));
+	}
+	@Test
+	public void testValidPublicKey() throws StreamCorruptedException {
+		checkValidKey(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				0);
+		checkValidKey(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				"xyzssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				3);
+		checkValidKey(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				"xyz ssh-ed25519   AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO abc",
+				3);
+		checkValidKey(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO",
+				"xyz\tssh-ed25519 \tAAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO abc",
+				3);
+	}
+
+	@Test
+	public void testInvalidPublicKey() {
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey(null, 0));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("", 0));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("foo", 0));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("ssh-ed25519 bar", -1));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("ssh-ed25519 bar", 12));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("ssh-ed25519 bar", 13));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.parsePublicKey("ssh-ed25519 bar", 16));
+	}
+
+	@Test
+	public void testValidDequote() {
+		assertEquals(new AllowedSigners.Dequoted("a\\bc", 4),
+				AllowedSigners.dequote("a\\bc", 0));
+		assertEquals(new AllowedSigners.Dequoted("a\\bc\"", 5),
+				AllowedSigners.dequote("a\\bc\"", 0));
+		assertEquals(new AllowedSigners.Dequoted("a\\b\"c", 5),
+				AllowedSigners.dequote("a\\b\"c", 0));
+		assertEquals(new AllowedSigners.Dequoted("a\\b\"c", 8),
+				AllowedSigners.dequote("\"a\\b\\\"c\"", 0));
+		assertEquals(new AllowedSigners.Dequoted("a\\b\"c", 11),
+				AllowedSigners.dequote("xyz\"a\\b\\\"c\"", 3));
+		assertEquals(new AllowedSigners.Dequoted("abc", 6),
+				AllowedSigners.dequote("   abc def", 3));
+	}
+
+	@Test
+	public void testInvalidDequote() {
+		assertThrows(Exception.class, () -> AllowedSigners.dequote("\"abc", 0));
+		assertThrows(Exception.class,
+				() -> AllowedSigners.dequote("\"abc\\\"", 0));
+	}
+
+	@Test
+	public void testValidLine() throws Exception {
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "*@a.com" },
+				true, null, null, null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"*@a.com cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "*@a.com", "*@b.a.com" },
+				true, null, null, null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"*@a.com,*@b.a.com cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "foo@a.com" },
+				false, null, null, null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"foo@a.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "foo@a.com" },
+				false, new String[] { "foo", "bar" }, null, null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"foo@a.com namespaces=\"foo,bar\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "foo@a.com" },
+				false, null, Instant.parse("2024-09-01T03:30:00.00Z"), null,
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"foo@a.com valid-After=\"20240901\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "*@a.com", "*@b.a.com" },
+				true, new String[] { "git" },
+				Instant.parse("2024-09-01T03:30:00.00Z"),
+				Instant.parse("2024-09-01T12:00:00.00Z"),
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"),
+				AllowedSigners.parseLine(
+						"*@a.com,*@b.a.com cert-authority namespaces=\"git\" valid-after=\"20240901\" valid-before=\"202409011200Z\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertEquals(new AllowedSigners.AllowedEntry(
+				new String[] { "foo@a.com" },
+				false, new String[] { "git" },
+				Instant.parse("2024-09-01T03:30:00.00Z"),
+				Instant.parse("2024-09-01T12:00:00.00Z"),
+				"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGxkz2AUld8eitmyIYlVV+Sot4jT3CigyBmvFRff0q4cSsKLx4x2TxGQeKKVueJEawtsUC2GNRV9FxXsTCUGcZU="),
+				AllowedSigners.parseLine(
+						"foo@a.com namespaces=\"git\" valid-after=\"20240901\" valid-before=\"202409011200Z\" ecdsa-sha2-nistp256   AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGxkz2AUld8eitmyIYlVV+Sot4jT3CigyBmvFRff0q4cSsKLx4x2TxGQeKKVueJEawtsUC2GNRV9FxXsTCUGcZU="));
+	}
+
+	@Test
+	public void testInvalidLine() {
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"cert-authority ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"namespaces=\"git\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"valid-after=\"20240901\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"valid-before=\"20240901\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO foo@bar.com"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"a@a.com namespaces=\"\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"a@a.com namespaces=\",,,\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+		assertThrows(Exception.class, () -> AllowedSigners.parseLine(
+				"a@a.com,,b@a.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGATOZ8PcOKdY978fzIstnZ0+FuefIWKp7wRZynQLdzO"));
+	}
+
+	@Test
+	public void testSkippedLine() throws Exception {
+		assertNull(AllowedSigners.parseLine(null));
+		assertNull(AllowedSigners.parseLine(""));
+		assertNull(AllowedSigners.parseLine("# Comment"));
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrlLoadTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrlLoadTest.java
new file mode 100644
index 0000000..9f9c3ca
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrlLoadTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+
+import org.junit.Test;
+
+/**
+ * Tests loading an {@link OpenSshBinaryKrl}.
+ */
+public class OpenSshBinaryKrlLoadTest {
+
+	@Test
+	public void testLoad() throws Exception {
+		try (InputStream in = new BufferedInputStream(
+				this.getClass().getResourceAsStream("krl/krl"))) {
+			OpenSshBinaryKrl krl = OpenSshBinaryKrl.load(in, false);
+			assertNotNull(krl);
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshKrlTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshKrlTest.java
new file mode 100644
index 0000000..2fd7756
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/OpenSshKrlTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.eclipse.jgit.util.FileUtils;
+import org.junit.AfterClass;
+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;
+
+/**
+ * Tests for {@link OpenSshKrl} using binary KRLs.
+ */
+@RunWith(Parameterized.class)
+public class OpenSshKrlTest {
+
+	// The test data was generated using the public domain OpenSSH test script
+	// with some minor modifications (une ed25519 always, generate a "includes
+	// everything KRl, name the unrekoked keys "unrevoked*", generate a plain
+	// text KRL, and don't run the ssh-keygen tests). The original script is
+	// available at
+	// https://github.com/openssh/openssh-portable/blob/67a115e/regress/krl.sh
+
+	private static final String[] KRLS = {
+			"krl-empty", "krl-keys", "krl-all",
+			"krl-sha1", "krl-sha256", "krl-hash",
+			"krl-serial", "krl-keyid", "krl-cert", "krl-ca",
+			"krl-serial-wild", "krl-keyid-wild", "krl-text" };
+
+	private static final int[] REVOKED = { 1, 4, 10, 50, 90, 500, 510, 520, 550,
+			799, 999 };
+
+	private static final int[] UNREVOKED = { 5, 9, 14, 16, 29, 49, 51, 499, 800,
+			1010, 1011 };
+
+	private static class TestData {
+
+		String key;
+
+		String krl;
+
+		Boolean expected;
+
+		TestData(String key, String krl, boolean expected) {
+			this.key = key;
+			this.krl = krl;
+			this.expected = Boolean.valueOf(expected);
+		}
+
+		@Override
+		public String toString() {
+			return key + '-' + krl;
+		}
+	}
+
+	@Parameters(name = "{0}")
+	public static List<TestData> initTestData() {
+		List<TestData> tests = new ArrayList<>();
+		for (int i = 0; i < REVOKED.length; i++) {
+			String key = String.format("revoked-%04d",
+					Integer.valueOf(REVOKED[i]));
+			for (String krl : KRLS) {
+				boolean expected = !krl.endsWith("-empty");
+				tests.add(new TestData(key + "-cert.pub", krl, expected));
+				expected = krl.endsWith("-keys") || krl.endsWith("-all")
+						|| krl.endsWith("-hash") || krl.endsWith("-sha1")
+						|| krl.endsWith("-sha256") || krl.endsWith("-text");
+				tests.add(new TestData(key + ".pub", krl, expected));
+			}
+		}
+		for (int i = 0; i < UNREVOKED.length; i++) {
+			String key = String.format("unrevoked-%04d",
+					Integer.valueOf(UNREVOKED[i]));
+			for (String krl : KRLS) {
+				boolean expected = false;
+				tests.add(new TestData(key + ".pub", krl, expected));
+				expected = krl.endsWith("-ca");
+				tests.add(new TestData(key + "-cert.pub", krl, expected));
+			}
+		}
+		return tests;
+	}
+
+	private static Path tmp;
+
+	@BeforeClass
+	public static void setUp() throws IOException {
+		tmp = Files.createTempDirectory("krls");
+		for (String krl : KRLS) {
+			copyResource("krl/" + krl, tmp);
+		}
+	}
+
+	private static void copyResource(String name, Path directory)
+			throws IOException {
+		try (InputStream in = OpenSshKrlTest.class
+				.getResourceAsStream(name)) {
+			int i = name.lastIndexOf('/');
+			String fileName = i < 0 ? name : name.substring(i + 1);
+			Files.copy(in, directory.resolve(fileName));
+		}
+	}
+
+	@AfterClass
+	public static void cleanUp() throws Exception {
+		FileUtils.delete(tmp.toFile(), FileUtils.RECURSIVE);
+	}
+
+	// Injected by JUnit
+	@Parameter
+	public TestData data;
+
+	@Test
+	public void testIsRevoked() throws Exception {
+		OpenSshKrl krl = new OpenSshKrl(tmp.resolve(data.krl));
+		try (InputStream in = this.getClass()
+				.getResourceAsStream("krl/" + data.key)) {
+			PublicKey key = AuthorizedKeyEntry.readAuthorizedKeys(in, true)
+					.get(0)
+					.resolvePublicKey(null, PublicKeyEntryResolver.FAILING);
+			assertEquals(data.expected, Boolean.valueOf(krl.isRevoked(key)));
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SerialRangeSetTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SerialRangeSetTest.java
new file mode 100644
index 0000000..e6709ad
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SerialRangeSetTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Tests for the set of serial number ranges.
+ */
+public class SerialRangeSetTest {
+
+	private SerialRangeSet ranges = new SerialRangeSet();
+
+	@Test
+	public void testInsertSimple() {
+		ranges.add(1);
+		ranges.add(3);
+		ranges.add(5);
+		assertEquals(3, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertFalse(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertFalse(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertFalse(ranges.contains(6));
+	}
+
+	@Test
+	public void testInsertSimpleRanges() {
+		ranges.add(1, 2);
+		ranges.add(4, 5);
+		ranges.add(7, 8);
+		assertEquals(3, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertFalse(ranges.contains(3));
+		assertTrue(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertFalse(ranges.contains(6));
+		assertTrue(ranges.contains(7));
+		assertTrue(ranges.contains(8));
+		assertFalse(ranges.contains(9));
+	}
+
+	@Test
+	public void testInsertCoalesce() {
+		ranges.add(5);
+		ranges.add(1);
+		ranges.add(2);
+		ranges.add(4);
+		ranges.add(7);
+		ranges.add(3);
+		assertEquals(2, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertTrue(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertFalse(ranges.contains(6));
+		assertTrue(ranges.contains(7));
+		assertFalse(ranges.contains(8));
+	}
+
+	@Test
+	public void testInsertOverlap() {
+		ranges.add(1, 3);
+		ranges.add(6);
+		ranges.add(2, 5);
+		assertEquals(1, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertTrue(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertTrue(ranges.contains(6));
+		assertFalse(ranges.contains(7));
+	}
+
+	@Test
+	public void testInsertOverlapMultiple() {
+		ranges.add(1, 3);
+		ranges.add(5, 6);
+		ranges.add(8);
+		ranges.add(2, 5);
+		assertEquals(2, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertTrue(ranges.contains(4));
+		assertTrue(ranges.contains(5));
+		assertTrue(ranges.contains(6));
+		assertFalse(ranges.contains(7));
+		assertTrue(ranges.contains(8));
+		assertFalse(ranges.contains(9));
+	}
+
+	@Test
+	public void testInsertOverlapTotal() {
+		ranges.add(1, 3);
+		ranges.add(2, 3);
+		assertEquals(1, ranges.size());
+		assertFalse(ranges.contains(0));
+		assertTrue(ranges.contains(1));
+		assertTrue(ranges.contains(2));
+		assertTrue(ranges.contains(3));
+		assertFalse(ranges.contains(4));
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java
new file mode 100644
index 0000000..79ca21f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtilsTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.InputStream;
+import java.security.PublicKey;
+import java.time.Instant;
+import java.util.List;
+
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for {@link SshCertificateUtils}. They use a certificate valid from
+ * 2024-09-01 00:00:00 to 2024-09-02 00:00:00 UTC.
+ */
+public class SshCertificateUtilsTest {
+
+	private OpenSshCertificate certificate;
+
+	@Before
+	public void loadCertificate() throws Exception {
+		try (InputStream in = this.getClass().getResourceAsStream(
+				"certs/expired.cert")) {
+			List<AuthorizedKeyEntry> keys = AuthorizedKeyEntry
+					.readAuthorizedKeys(in, true);
+			if (keys.isEmpty()) {
+				certificate = null;
+			}
+			PublicKey key = keys.get(0).resolvePublicKey(null,
+					PublicKeyEntryResolver.FAILING);
+			assertTrue(
+					"Expected an OpenSshKeyCertificate but got a "
+							+ key.getClass().getName(),
+					key instanceof OpenSshCertificate);
+			certificate = (OpenSshCertificate) key;
+		}
+	}
+
+	@Test
+	public void testValidUserCertificate() {
+		assertNull(SshCertificateUtils.verify(certificate, null));
+		Instant validTime = Instant.parse("2024-09-01T00:00:00.00Z");
+		assertNull(SshCertificateUtils.verify(certificate, validTime));
+		assertNull(SshCertificateUtils.checkExpiration(certificate, validTime));
+	}
+
+	@Test
+	public void testCheckTooEarly() {
+		Instant invalidTime = Instant.parse("2024-08-31T23:59:59.00Z");
+		assertNotNull(
+				SshCertificateUtils.checkExpiration(certificate, invalidTime));
+		assertNotNull(SshCertificateUtils.verify(certificate, invalidTime));
+	}
+
+	@Test
+	public void testCheckExpired() {
+		Instant invalidTime = Instant.parse("2024-09-02T00:00:01.00Z");
+		assertNotNull(
+				SshCertificateUtils.checkExpiration(certificate, invalidTime));
+		assertNotNull(SshCertificateUtils.verify(certificate, invalidTime));
+	}
+
+	@Test
+	public void testInvalidSignature() throws Exception {
+		// Modify the serialized certificate, then re-load it again. To check that
+		// serialization per se works fine, also check an unmodified version.
+		Buffer buffer = new ByteArrayBuffer();
+		buffer.putPublicKey(certificate);
+		int pos = buffer.rpos();
+		PublicKey unchanged = buffer.getPublicKey();
+		assertTrue(
+				"Expected an OpenSshCertificate but got a "
+						+ unchanged.getClass().getName(),
+				unchanged instanceof OpenSshCertificate);
+		assertNull(SshCertificateUtils.verify((OpenSshCertificate) unchanged,
+				null));
+		buffer.rpos(pos);
+		// Change a byte. The test certificate has the key ID at offset 128.
+		// Changing a byte in the key ID should still result in a successful
+		// deserialization, but then fail the signature check.
+		buffer.array()[pos + 128]++;
+		PublicKey changed = buffer.getPublicKey();
+		assertTrue(
+				"Expected an OpenSshCertificate but got a "
+						+ changed.getClass().getName(),
+				changed instanceof OpenSshCertificate);
+		assertNotNull(
+				SshCertificateUtils.verify((OpenSshCertificate) changed, null));
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifierTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifierTest.java
new file mode 100644
index 0000000..e5dfe49
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifierTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+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 java.util.Map;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.VerificationResult;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.StringUtils;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link SshSignatureVerifier}.
+ */
+public class SshSignatureVerifierTest extends AbstractSshSignatureTest {
+
+	@Test
+	public void testPlainSignature() throws Exception {
+		RevCommit c = checkSshSignature(
+				createSignedCommit(null, "signing_key.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertTrue(v.verified());
+			assertFalse(v.expired());
+			assertTrue(StringUtils.isEmptyOrNull(v.message()));
+			assertEquals("tester@example.com", v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.FULL, v.trustLevel());
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+
+	@Test
+	public void testCertificateSignature() throws Exception {
+		RevCommit c = checkSshSignature(
+				createSignedCommit("tester.cert", "signing_key-cert.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertTrue(v.verified());
+			assertFalse(v.expired());
+			assertTrue(StringUtils.isEmptyOrNull(v.message()));
+			assertEquals("tester@example.com", v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.FULL, v.trustLevel());
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+
+	@Test
+	public void testNoPrincipalsSignature() throws Exception {
+		RevCommit c = checkSshSignature(createSignedCommit("no_principals.cert",
+				"signing_key-cert.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertFalse(v.verified());
+			assertFalse(v.expired());
+			assertNull(v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.NEVER, v.trustLevel());
+			assertTrue(v.message().contains("*@example.com"));
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+
+	@Test
+	public void testOtherCertificateSignature() throws Exception {
+		RevCommit c = checkSshSignature(
+				createSignedCommit("other.cert", "signing_key-cert.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertTrue(v.verified());
+			assertFalse(v.expired());
+			assertTrue(StringUtils.isEmptyOrNull(v.message()));
+			assertEquals("other@example.com", v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.FULL, v.trustLevel());
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+
+	@Test
+	public void testTwoPrincipalsCertificateSignature() throws Exception {
+		RevCommit c = checkSshSignature(createSignedCommit(
+				"two_principals.cert", "signing_key-cert.pub"));
+		try (Git git = new Git(db)) {
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addName(c.getName()).call();
+			assertEquals(1, results.size());
+			VerificationResult verified = results.get(c.getName());
+			assertNotNull(verified);
+			assertNull(verified.getException());
+			SignatureVerifier.SignatureVerification v = verified
+					.getVerification();
+			assertTrue(v.verified());
+			assertFalse(v.expired());
+			assertTrue(StringUtils.isEmptyOrNull(v.message()));
+			assertEquals("foo@example.com,tester@example.com", v.keyUser());
+			assertEquals("SHA256:GKW0xy+XKnJGs0CJqP6j5bd4FdiwWNaUbwvUbHvhQKo",
+					v.keyFingerprint());
+			assertEquals(SignatureVerifier.TrustLevel.FULL, v.trustLevel());
+			assertEquals(commitTime, v.creationDate().toInstant());
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java
new file mode 100644
index 0000000..b3a4482
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/SshSignerTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+/**
+ * Tests for the {@link SshSigner}.
+ */
+public class SshSignerTest extends AbstractSshSignatureTest {
+
+	@Test
+	public void testPlainSignature() throws Exception {
+		checkSshSignature(createSignedCommit(null, "signing_key.pub"));
+	}
+
+	@Test
+	public void testExpiredSignature() throws Exception {
+		Throwable t = assertThrows(Throwable.class,
+				() -> createSignedCommit("expired.cert",
+						"signing_key-cert.pub"));
+		// The exception or one of its causes should mention "[Ee]xpired" and
+		// "[Cc]ertificate" in the message
+		while (t != null) {
+			String message = t.getMessage();
+			if (message.contains("xpired") && message.contains("ertificate")) {
+				return;
+			}
+			t = t.getCause();
+		}
+		fail("Expected exception message not found");
+	}
+
+	@Test
+	public void testCertificateSignature() throws Exception {
+		checkSshSignature(createSignedCommit("tester.cert", "signing_key.pub"));
+	}
+
+	@Test
+	public void testNoPrincipalsSignature() throws Exception {
+		// Certificate has no principals; should still work
+		checkSshSignature(
+				createSignedCommit("no_principals.cert", "signing_key.pub"));
+	}
+
+	@Test
+	public void testOtherSignature() throws Exception {
+		// Certificate has a principal different that tester@example.com; should
+		// still work
+		checkSshSignature(createSignedCommit("other.cert", "signing_key.pub"));
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/VerifyGitSignaturesTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/VerifyGitSignaturesTest.java
new file mode 100644
index 0000000..30ddee5
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/signing/ssh/VerifyGitSignaturesTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.VerificationResult;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.util.GitDateFormatter;
+import org.eclipse.jgit.util.SignatureUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Verifies signatures made with C git and OpenSSH 9.0 to ensure we arrive at
+ * the same good/bad decisions, and that we can verify signatures not created by
+ * ourselves.
+ * <p>
+ * Clones a JGit repo from a git bundle file created with C git, then checks all
+ * the commits and their signatures. (All commits in that bundle have SSH
+ * signatures.)
+ * </p>
+ */
+public class VerifyGitSignaturesTest extends LocalDiskRepositoryTestCase {
+
+	private static final Logger LOG = LoggerFactory
+			.getLogger(VerifyGitSignaturesTest.class);
+
+	@Rule
+	public TemporaryFolder bundleDir = new TemporaryFolder();
+
+	@Before
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		try (InputStream in = this.getClass()
+				.getResourceAsStream("repo.bundle")) {
+			Files.copy(in, bundleDir.getRoot().toPath().resolve("repo.bundle"));
+		}
+		try (InputStream in = this.getClass()
+				.getResourceAsStream("allowed_signers")) {
+			Files.copy(in,
+					bundleDir.getRoot().toPath().resolve("allowed_signers"));
+		}
+	}
+
+	/**
+	 * Tests signatures created by C git using OpenSSH 9.0.
+	 */
+	@Test
+	public void testGitSignatures() throws Exception {
+		File gitDir = new File(getTemporaryDirectory(), "repo.git");
+		try (Git git = Git.cloneRepository().setBare(true)
+				.setGitDir(gitDir)
+				.setURI(new File(bundleDir.getRoot(), "repo.bundle").toURI()
+						.toString())
+				.setBranch("master")
+				.call()) {
+			StoredConfig config = git.getRepository().getConfig();
+			config.setString("gpg", "ssh", "allowedSignersFile",
+					bundleDir.getRoot().toPath().resolve("allowed_signers")
+							.toAbsolutePath().toString().replace('\\', '/'));
+			config.save();
+			List<String> commits = new ArrayList<>();
+			Map<String, PersonIdent> committers = new HashMap<>();
+			git.log().all().call().forEach(c -> {
+				commits.add(c.getName());
+				committers.put(c.getName(), c.getCommitterIdent());
+			});
+			Map<String, Boolean> expected = new HashMap<>();
+			// These two commits do have multiple principals. GIT just reports
+			// the first one; we report both.
+			expected.put("9f79a7b661a22ab1ddf8af880d23678ae7696b71",
+					Boolean.TRUE);
+			expected.put("435108d157440e77d61a914b6a5736bc831c874d",
+					Boolean.TRUE);
+			// This commit has a wrong commit message; the certificate used
+			// did _not_ have two principals, but only a single principal
+			// foo@example.org.
+			expected.put("779dac7de40ebc3886af87d5e6680a09f8b13a3e",
+					Boolean.TRUE);
+			// Signed with other_key-cert.pub: we still don't know the key,
+			// but we do know the certificate's CA key, and trust it, so it's
+			// accepted as a signature from the principal(s) listed in the
+			// certificate.
+			expected.put("951f06d5b5598b721b98d98b04e491f234c1926a",
+					Boolean.TRUE);
+			// Signature with other_key.pub not listed in allowed_signers
+			expected.put("984e629c6d543a7f77eb49a8c9316f2ae4416375",
+					Boolean.FALSE);
+			// Signed with other-ca.cert (CA key not in allowed_signers), but
+			// the certified key _is_ listed in allowed_signers.
+			expected.put("1d7ac6d91747a9c9a777df238fbdaeffa7731a6c",
+					Boolean.FALSE);
+			expected.put("a297bcfbf5c4a850f9770655fef7315328a4b3fb",
+					Boolean.TRUE);
+			expected.put("852729d54676cb83826ed821dc7734013e97950d",
+					Boolean.TRUE);
+			// Signature with a certificate without principals.
+			expected.put("e39a049f75fe127eb74b30aba4b64e171d4281dd",
+					Boolean.FALSE);
+			// Signature made with expired.cert (expired at the commit time).
+			// git/OpenSSH 9.0 allows to create such signatures, but reports
+			// them as FALSE. Our SshSigner doesn't allow creating such
+			// signatures.
+			expected.put("303ea5e61feacdad4cb012b4cb6b0cea3fbcef9f",
+					Boolean.FALSE);
+			expected.put("1ae4b120a869b72a7a2d4ad4d7a8c9d454384333",
+					Boolean.TRUE);
+			Map<String, VerificationResult> results = git.verifySignature()
+					.addNames(commits).call();
+			GitDateFormatter dateFormat = new GitDateFormatter(
+					GitDateFormatter.Format.ISO);
+			for (String oid : commits) {
+				VerificationResult v = results.get(oid);
+				assertNotNull(v);
+				assertNull(v.getException());
+				SignatureVerifier.SignatureVerification sv = v
+						.getVerification();
+				assertNotNull(sv);
+				LOG.info("Commit {}\n{}", oid, SignatureUtils.toString(sv,
+						committers.get(oid), dateFormat));
+				Boolean wanted = expected.get(oid);
+				assertNotNull(wanted);
+				assertEquals(wanted, Boolean.valueOf(sv.verified()));
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReaderTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReaderTest.java
new file mode 100644
index 0000000..d36c38f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReaderTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.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.transport.sshd;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.apache.sshd.client.config.hosts.KnownHostEntry;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.junit.Test;
+
+public class KnownHostEntryReaderTest {
+
+	@Test
+	public void testUnsupportedHostKeyLine() {
+		KnownHostEntry entry = KnownHostEntryReader.parseHostEntry(
+				"[localhost]:2222 ssh-unknown AAAAC3NzaC1lZDI1NTE5AAAAIPu6ntmyfSOkqLl3qPxD5XxwW7OONwwSG3KO+TGn+PFu");
+		AuthorizedKeyEntry keyEntry = entry.getKeyEntry();
+		assertNotNull(keyEntry);
+		assertEquals("ssh-unknown", keyEntry.getKeyType());
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabaseTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabaseTest.java
new file mode 100644
index 0000000..6b61821
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyDatabaseTest.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2025 Thomas Wolf <twolf@apache.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.transport.sshd;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.certificate.OpenSshCertificateBuilder;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.cipher.ECCurves;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.transport.CredentialItem;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Tests for {@link OpenSshServerKeyDatabase}.
+ */
+public class OpenSshServerKeyDatabaseTest {
+
+	private static final InetSocketAddress LOCAL = new InetSocketAddress(
+			InetAddress.getLoopbackAddress(), SshConstants.DEFAULT_PORT);
+
+	private static final InetSocketAddress LOCAL_29418 = new InetSocketAddress(
+			InetAddress.getLoopbackAddress(), 29418);
+
+	private static PublicKey rsa1024;
+	private static PublicKey rsa2048;
+	private static PublicKey ec256;
+	private static PublicKey ec384;
+	private static PublicKey caKey;
+	private static PublicKey certificate;
+
+	@BeforeClass
+	public static void initKeys() throws Exception {
+		// Generate a few keys that we can use
+		KeyPairGenerator gen = SecurityUtils.getKeyPairGenerator(KeyUtils.RSA_ALGORITHM);
+		gen.initialize(1024);
+		rsa1024 = gen.generateKeyPair().getPublic();
+		gen.initialize(2048);
+		rsa2048 = gen.generateKeyPair().getPublic();
+		gen = SecurityUtils.getKeyPairGenerator(KeyUtils.EC_ALGORITHM);
+		ECCurves curve = ECCurves.fromCurveSize(256);
+		gen.initialize(curve.getParameters());
+		ec256 = gen.generateKeyPair().getPublic();
+		PublicKey certKey = gen.generateKeyPair().getPublic();
+		curve = ECCurves.fromCurveSize(384);
+		gen.initialize(curve.getParameters());
+		ec384 = gen.generateKeyPair().getPublic();
+		// Generate a certificate for some key
+		gen.initialize(curve.getParameters());
+		KeyPair ca = gen.generateKeyPair();
+		caKey = ca.getPublic();
+		certificate = OpenSshCertificateBuilder
+				.hostCertificate()
+				.serial(System.currentTimeMillis())
+				.publicKey(certKey)
+				.id("test-host-cert")
+				.validBefore(
+						System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1))
+				.principals(List.of("localhost", "127.0.0.1"))
+				.sign(ca, "ecdsa-sha2-nistp384");
+	}
+
+	@Rule
+	public TemporaryFolder tmp = new TemporaryFolder();
+
+	private Path knownHosts;
+	private Path knownHosts2;
+	private ServerKeyDatabase database;
+
+	@Before
+	public void setupDatabase() throws Exception {
+		Path root = tmp.getRoot().toPath();
+		knownHosts = root.resolve("known_hosts");
+		knownHosts2 = root.resolve("known_hosts2");
+		database = new OpenSshServerKeyDatabase(false, List.of(knownHosts, knownHosts2));
+	}
+
+	@Test
+	public void testFindInSecondFile() throws Exception {
+		Files.write(knownHosts,
+				List.of(
+						"some.other.host " + PublicKeyEntry.toString(rsa1024),
+						"some.other.com " + PublicKeyEntry.toString(ec384)));
+		Files.write(knownHosts2,
+				List.of(
+						"localhost,127.0.0.1 " + PublicKeyEntry.toString(ec256),
+						"some.other.com " + PublicKeyEntry.toString(rsa2048)));
+		assertTrue(database.accept("localhost", LOCAL, ec256,
+				new KnownHostsConfig(), null));
+		assertFalse(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testNoFirstFile() throws Exception {
+		Files.write(knownHosts2,
+				List.of("localhost,127.0.0.1 " + PublicKeyEntry.toString(ec256),
+						"some.other.com " + PublicKeyEntry.toString(rsa2048)));
+		assertTrue(database.accept("localhost", LOCAL, ec256,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testFind() throws Exception {
+		Files.write(knownHosts,
+				List.of("localhost,127.0.0.1 "
+						+ PublicKeyEntry.toString(rsa1024),
+						"some.other.com " + PublicKeyEntry.toString(ec384)));
+		Files.write(knownHosts2,
+				List.of("localhost,127.0.0.1 " + PublicKeyEntry.toString(ec256),
+						"some.other.com " + PublicKeyEntry.toString(rsa2048)));
+		assertTrue(database.accept("localhost", LOCAL, ec256,
+				new KnownHostsConfig(), null));
+		assertTrue(database.accept("localhost:22", LOCAL, ec256,
+				new KnownHostsConfig(), null));
+		assertTrue(database.accept("127.0.0.1", LOCAL, rsa1024,
+				new KnownHostsConfig(), null));
+		assertTrue(database.accept("[127.0.0.1]:22", LOCAL, rsa1024,
+				new KnownHostsConfig(), null));
+		assertFalse(database.accept("localhost:29418", LOCAL_29418, ec256,
+				new KnownHostsConfig(), null));
+		assertFalse(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testFindCertificate() throws Exception {
+		Files.write(knownHosts,
+				List.of("localhost,127.0.0.1 "
+						+ PublicKeyEntry.toString(rsa1024),
+						"some.other.com " + PublicKeyEntry.toString(ec384),
+						"@cert-authority localhost,127.0.0.1 "
+								+ PublicKeyEntry.toString(caKey)));
+		assertTrue(database.accept("localhost", LOCAL, certificate,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testCaKeyNotConsidered() throws Exception {
+		Files.write(knownHosts,
+				List.of("localhost,127.0.0.1 "
+						+ PublicKeyEntry.toString(rsa1024),
+						"some.other.com " + PublicKeyEntry.toString(ec384),
+						"@cert-authority localhost,127.0.0.1 "
+								+ PublicKeyEntry.toString(ec256)));
+		assertFalse(database.accept("localhost", LOCAL, ec256,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testkeyPlainAndCa() throws Exception {
+		Files.write(knownHosts, List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(ec384),
+				"@cert-authority localhost,127.0.0.1 "
+						+ PublicKeyEntry.toString(ec256),
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(ec256)));
+		// ec256 is a CA key, but also a valid direct host key for localhost
+		assertTrue(database.accept("localhost", LOCAL, ec256,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testLookupCertificate() throws Exception {
+		List<PublicKey> keys = database.lookup("localhost", LOCAL,
+				new KnownHostsConfig());
+		// Certificates or CA keys are not reported via lookup.
+		assertTrue(keys.isEmpty());
+	}
+
+	@Test
+	public void testCertificateNotAdded() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(ec384));
+		Files.write(knownHosts, initialKnownHosts);
+		assertFalse(database.accept("localhost", LOCAL, certificate,
+				new KnownHostsConfig(), null));
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertFalse(
+				database.accept("localhost", LOCAL, certificate,
+						new KnownHostsConfig(
+								KnownHostsConfig.StrictHostKeyChecking.ASK),
+						ui));
+		assertEquals(0, ui.invocations);
+		assertFile(knownHosts, initialKnownHosts);
+	}
+
+	@Test
+	public void testCertificateNotModified() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"@cert-authority localhost,127.0.0.1 "
+						+ PublicKeyEntry.toString(ec384),
+				"some.other.com " + PublicKeyEntry.toString(ec256));
+		Files.write(knownHosts, initialKnownHosts);
+		assertFalse(database.accept("localhost", LOCAL, certificate,
+				new KnownHostsConfig(), null));
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertFalse(
+				database.accept("localhost", LOCAL, certificate,
+						new KnownHostsConfig(
+								KnownHostsConfig.StrictHostKeyChecking.ASK),
+						ui));
+		assertEquals(0, ui.invocations);
+		assertFile(knownHosts, initialKnownHosts);
+	}
+
+	@Test
+	public void testModifyFile() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"some.other.host " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(ec384));
+		Files.write(knownHosts, initialKnownHosts);
+		List<String> initialKnownHosts2 = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(ec256),
+				"some.other.com " + PublicKeyEntry.toString(rsa2048));
+		Files.write(knownHosts2, initialKnownHosts2);
+		assertFalse(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+		assertFile(knownHosts, initialKnownHosts);
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertFalse(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ACCEPT_NEW),
+				null));
+		assertFile(knownHosts, initialKnownHosts);
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertTrue(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ACCEPT_ANY),
+				null));
+		assertFile(knownHosts, initialKnownHosts);
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertFalse(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				null));
+		assertFile(knownHosts, initialKnownHosts);
+		assertFile(knownHosts2, initialKnownHosts2);
+
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, false);
+		assertTrue(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		assertFile(knownHosts, initialKnownHosts);
+		assertFile(knownHosts2, initialKnownHosts2);
+
+		ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		assertFile(knownHosts, initialKnownHosts);
+		assertFile(knownHosts2, List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(ec384),
+				"some.other.com " + PublicKeyEntry.toString(rsa2048)));
+		assertTrue(database.accept("127.0.0.1", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testModifyFirstFile() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(ec384));
+		Files.write(knownHosts, initialKnownHosts);
+		List<String> initialKnownHosts2 = List.of(
+				"some.other.host " + PublicKeyEntry.toString(ec256),
+				"some.other.com " + PublicKeyEntry.toString(rsa2048));
+		Files.write(knownHosts2, initialKnownHosts2);
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		assertFile(knownHosts,
+				List.of("localhost,127.0.0.1 " + PublicKeyEntry.toString(ec384),
+						"some.other.com " + PublicKeyEntry.toString(ec384)));
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertTrue(database.accept("127.0.0.1:22", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testModifyMatchingKeyType() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(ec384));
+		Files.write(knownHosts, initialKnownHosts);
+		List<String> initialKnownHosts2 = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(ec256),
+				"some.other.com " + PublicKeyEntry.toString(rsa2048));
+		Files.write(knownHosts2, initialKnownHosts2);
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("localhost", LOCAL, rsa2048,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		assertFile(knownHosts,
+				List.of("localhost,127.0.0.1 "
+						+ PublicKeyEntry.toString(rsa2048),
+						"some.other.com " + PublicKeyEntry.toString(ec384)));
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertTrue(database.accept("127.0.0.1:22", LOCAL, rsa2048,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testModifyMatchingKeyType2() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(ec256),
+				"some.other.com " + PublicKeyEntry.toString(ec384));
+		Files.write(knownHosts, initialKnownHosts);
+		List<String> initialKnownHosts2 = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(rsa2048));
+		Files.write(knownHosts2, initialKnownHosts2);
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		assertFile(knownHosts,
+				List.of("localhost,127.0.0.1 " + PublicKeyEntry.toString(ec384),
+						"some.other.com " + PublicKeyEntry.toString(ec384)));
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertTrue(database.accept("127.0.0.1:22", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testModifySecondFile() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(ec384));
+		Files.write(knownHosts, initialKnownHosts);
+		List<String> initialKnownHosts2 = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(ec256),
+				"some.other.com " + PublicKeyEntry.toString(rsa2048));
+		Files.write(knownHosts2, initialKnownHosts2);
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		assertFile(knownHosts, initialKnownHosts);
+		assertFile(knownHosts2,
+				List.of("localhost,127.0.0.1 " + PublicKeyEntry.toString(ec384),
+						"some.other.com " + PublicKeyEntry.toString(rsa2048)));
+		assertTrue(database.accept("127.0.0.1:22", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testAddNewKey() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"some.other.host " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(ec256));
+		Files.write(knownHosts, initialKnownHosts);
+		List<String> initialKnownHosts2 = List
+				.of("some.other.com " + PublicKeyEntry.toString(rsa2048));
+		Files.write(knownHosts2, initialKnownHosts2);
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		List<String> expected = new ArrayList<>(initialKnownHosts);
+		expected.add("localhost,127.0.0.1 " + PublicKeyEntry.toString(ec384));
+		assertFile(knownHosts, expected);
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertTrue(database.accept("127.0.0.1:22", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testCreateNewFile() throws Exception {
+		List<String> initialKnownHosts2 = List
+				.of("some.other.com " + PublicKeyEntry.toString(ec256));
+		Files.write(knownHosts2, initialKnownHosts2);
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		assertFile(knownHosts, List
+				.of("localhost,127.0.0.1 " + PublicKeyEntry.toString(ec384)));
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertTrue(database.accept("127.0.0.1:22", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testAddNewKey2() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"some.other.host " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(ec256));
+		Files.write(knownHosts, initialKnownHosts);
+		List<String> initialKnownHosts2 = List
+				.of("some.other.com " + PublicKeyEntry.toString(rsa2048));
+		Files.write(knownHosts2, initialKnownHosts2);
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("127.0.0.1:29418", LOCAL_29418, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		List<String> expected = new ArrayList<>(initialKnownHosts);
+		expected.add("[127.0.0.1]:29418,[localhost]:29418 "
+				+ PublicKeyEntry.toString(ec384));
+		assertFile(knownHosts, expected);
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertTrue(database.accept("localhost:29418", LOCAL_29418, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testAddNewKey3() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"some.other.host " + PublicKeyEntry.toString(rsa1024),
+				"some.other.com " + PublicKeyEntry.toString(ec256));
+		Files.write(knownHosts, initialKnownHosts);
+		List<String> initialKnownHosts2 = List
+				.of("some.other.com " + PublicKeyEntry.toString(rsa2048));
+		Files.write(knownHosts2, initialKnownHosts2);
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("localhost:29418", LOCAL_29418, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		List<String> expected = new ArrayList<>(initialKnownHosts);
+		expected.add("[localhost]:29418,[127.0.0.1]:29418 "
+				+ PublicKeyEntry.toString(ec384));
+		assertFile(knownHosts, expected);
+		assertFile(knownHosts2, initialKnownHosts2);
+		assertTrue(database.accept("127.0.0.1:29418", LOCAL_29418, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	@Test
+	public void testUnknownKeyType() throws Exception {
+		List<String> initialKnownHosts = List.of(
+				"localhost,127.0.0.1 " + PublicKeyEntry.toString(ec384)
+						.replace("ecdsa", "foo"),
+				"some.other.com " + PublicKeyEntry.toString(ec384));
+		Files.write(knownHosts, initialKnownHosts);
+		TestCredentialsProvider ui = new TestCredentialsProvider(true, true);
+		assertTrue(database.accept("localhost", LOCAL, ec384,
+				new KnownHostsConfig(
+						ServerKeyDatabase.Configuration.StrictHostKeyChecking.ASK),
+				ui));
+		assertEquals(1, ui.invocations);
+		// The "modified key" dialog has two questions; whereas the "add new
+		// key" is just a simple question.
+		assertEquals(2, ui.questions);
+		List<String> expected = new ArrayList<>(initialKnownHosts);
+		expected.add("localhost,127.0.0.1 " + PublicKeyEntry.toString(ec384));
+		assertFile(knownHosts, expected);
+		assertTrue(database.accept("127.0.0.1:22", LOCAL, ec384,
+				new KnownHostsConfig(), null));
+	}
+
+	private void assertFile(Path path, List<String> lines) throws Exception {
+		assertEquals(lines, Files.readAllLines(path).stream()
+				.filter(s -> !s.isBlank()).toList());
+	}
+
+	private static class TestCredentialsProvider extends CredentialsProvider {
+
+		private final boolean[] values;
+
+		int invocations = 0;
+
+		int questions = 0;
+
+		TestCredentialsProvider(boolean accept, boolean store) {
+			values = new boolean[] { accept, store };
+		}
+
+		@Override
+		public boolean isInteractive() {
+			return true;
+		}
+
+		@Override
+		public boolean supports(CredentialItem... items) {
+			return true;
+		}
+
+		@Override
+		public boolean get(URIish uri, CredentialItem... items)
+				throws UnsupportedCredentialItem {
+			invocations++;
+			int i = 0;
+			for (CredentialItem item : items) {
+				if (item instanceof CredentialItem.YesNoType) {
+					((CredentialItem.YesNoType) item)
+							.setValue(i < values.length && values[i++]);
+					questions++;
+				}
+			}
+			return true;
+		}
+	}
+
+	private static class KnownHostsConfig implements ServerKeyDatabase.Configuration {
+
+		@NonNull
+		private final StrictHostKeyChecking check;
+
+		KnownHostsConfig() {
+			this(StrictHostKeyChecking.REQUIRE_MATCH);
+		}
+
+		KnownHostsConfig(@NonNull StrictHostKeyChecking check) {
+			this.check = check;
+		}
+
+		@Override
+		public List<String> getUserKnownHostsFiles() {
+			return List.of();
+		}
+
+		@Override
+		public List<String> getGlobalKnownHostsFiles() {
+			return List.of();
+		}
+
+		@Override
+		public StrictHostKeyChecking getStrictHostKeyChecking() {
+			return check;
+		}
+
+		@Override
+		public boolean getHashKnownHosts() {
+			return false;
+		}
+
+		@Override
+		public String getUsername() {
+			return "user";
+		}
+
+	}
+}
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
index eef0402..69bd5c5 100644
--- 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
@@ -17,7 +17,6 @@
 
 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;
@@ -52,7 +51,8 @@ protected void installConfig(String... config) {
 	@Override
 	public void setUp() throws Exception {
 		super.setUp();
-		StoredConfig config = ((Repository) db).getConfig();
+		@SuppressWarnings("restriction")
+		StoredConfig config = db.getConfig();
 		config.setInt("protocol", null, "version", 2);
 		config.save();
 	}
diff --git a/org.eclipse.jgit.ssh.apache/BUILD b/org.eclipse.jgit.ssh.apache/BUILD
index fd88a8a..83709c3 100644
--- a/org.eclipse.jgit.ssh.apache/BUILD
+++ b/org.eclipse.jgit.ssh.apache/BUILD
@@ -12,7 +12,6 @@
     resource_strip_prefix = "org.eclipse.jgit.ssh.apache/resources",
     resources = RESOURCES,
     deps = [
-        "//lib:eddsa",
         "//lib:slf4j-api",
         "//lib:sshd-osgi",
         "//lib:sshd-sftp",
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
index 02d9466..f38ea72 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
@@ -6,9 +6,10 @@
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-ActivationPolicy: lazy
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Export-Package: org.eclipse.jgit.internal.transport.sshd;version="7.0.0";x-internal:=true;
+Export-Package: org.eclipse.jgit.internal.signing.ssh;version="7.3.0";x-friends:="org.eclipse.jgit.ssh.apache.test",
+ org.eclipse.jgit.internal.transport.sshd;version="7.3.0";x-friends:="org.eclipse.jgit.ssh.apache.test";
   uses:="org.apache.sshd.client,
    org.apache.sshd.client.auth,
    org.apache.sshd.client.auth.keyboard,
@@ -23,78 +24,79 @@
    org.apache.sshd.common.signature,
    org.apache.sshd.common.util.buffer,
    org.eclipse.jgit.transport",
- org.eclipse.jgit.internal.transport.sshd.agent;version="7.0.0";x-internal:=true,
- org.eclipse.jgit.internal.transport.sshd.auth;version="7.0.0";x-internal:=true,
- org.eclipse.jgit.internal.transport.sshd.pkcs11;version="7.0.0";x-internal:=true,
- org.eclipse.jgit.internal.transport.sshd.proxy;version="7.0.0";x-friends:="org.eclipse.jgit.ssh.apache.test",
- org.eclipse.jgit.transport.sshd;version="7.0.0";
+ org.eclipse.jgit.internal.transport.sshd.agent;version="7.3.0";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.auth;version="7.3.0";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.pkcs11;version="7.3.0";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.proxy;version="7.3.0";x-friends:="org.eclipse.jgit.ssh.apache.test",
+ org.eclipse.jgit.signing.ssh;version="7.3.0";uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.transport.sshd;version="7.3.0";
   uses:="org.eclipse.jgit.transport,
    org.apache.sshd.client.config.hosts,
    org.apache.sshd.common.keyprovider,
    org.eclipse.jgit.util,
    org.apache.sshd.client.session,
    org.apache.sshd.client.keyverifier",
- org.eclipse.jgit.transport.sshd.agent;version="7.0.0",
- sun.security.x509
-Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)",
- org.apache.sshd.agent;version="[2.12.0,2.13.0)",
- org.apache.sshd.client;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.auth;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.auth.keyboard;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.auth.password;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.auth.pubkey;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.channel;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.config.hosts;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.config.keys;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.future;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.keyverifier;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.session;version="[2.12.0,2.13.0)",
- org.apache.sshd.client.session.forward;version="[2.12.0,2.13.0)",
- org.apache.sshd.common;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.auth;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.channel;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.cipher;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.compression;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.config.keys;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.config.keys.loader;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.config.keys.loader.openssh.kdf;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.config.keys.u2f;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.digest;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.forward;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.future;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.helpers;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.io;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.kex;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.kex.extension;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.kex.extension.parser;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.keyprovider;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.mac;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.random;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.session;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.session.helpers;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.signature;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.buffer;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.buffer.keys;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.closeable;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.io;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.io.der;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.io.functors;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.io.resource;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.logging;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.net;version="[2.12.0,2.13.0)",
- org.apache.sshd.common.util.security;version="[2.12.0,2.13.0)",
- org.apache.sshd.core;version="[2.12.0,2.13.0)",
- org.apache.sshd.server.auth;version="[2.12.0,2.13.0)",
- org.apache.sshd.sftp;version="[2.12.0,2.13.0)",
- org.apache.sshd.sftp.client;version="[2.12.0,2.13.0)",
- org.apache.sshd.sftp.common;version="[2.12.0,2.13.0)",
- org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.fnmatch;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.transport.sshd.agent;version="7.3.0"
+Import-Package: org.apache.sshd.agent;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.auth;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.auth.keyboard;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.auth.password;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.auth.pubkey;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.channel;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.config.hosts;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.config.keys;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.future;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.keyverifier;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.session;version="[2.15.0,2.16.0)",
+ org.apache.sshd.client.session.forward;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.auth;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.channel;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.cipher;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.compression;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.config.keys;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.config.keys.loader;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.config.keys.loader.openssh.kdf;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.config.keys.u2f;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.digest;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.forward;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.future;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.helpers;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.io;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.kex;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.kex.extension;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.kex.extension.parser;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.keyprovider;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.mac;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.random;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.session;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.session.helpers;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.signature;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.buffer;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.buffer.keys;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.closeable;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.io;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.io.der;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.io.functors;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.io.resource;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.logging;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.net;version="[2.15.0,2.16.0)",
+ org.apache.sshd.common.util.security;version="[2.15.0,2.16.0)",
+ org.apache.sshd.core;version="[2.15.0,2.16.0)",
+ org.apache.sshd.server.auth;version="[2.15.0,2.16.0)",
+ org.apache.sshd.sftp;version="[2.15.0,2.16.0)",
+ org.apache.sshd.sftp.client;version="[2.15.0,2.16.0)",
+ org.apache.sshd.sftp.common;version="[2.15.0,2.16.0)",
+ org.eclipse.jgit.annotations;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.fnmatch;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
  org.slf4j;version="[1.7.0,3.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 bb0d79b..d6225e2 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.apache;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.apache/pom.xml b/org.eclipse.jgit.ssh.apache/pom.xml
index 7cc3a55..365034a 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.apache</artifactId>
@@ -30,7 +30,6 @@
   <properties>
     <translate-qualifier/>
     <source-bundle-manifest>${project.build.directory}/META-INF/SOURCE-MANIFEST.MF</source-bundle-manifest>
-    <eddsa-version>0.3.0</eddsa-version>
   </properties>
 
   <dependencies>
@@ -63,12 +62,6 @@
     </dependency>
 
     <dependency>
-      <groupId>net.i2p.crypto</groupId>
-      <artifactId>eddsa</artifactId>
-      <version>${eddsa-version}</version>
-    </dependency>
-
-    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
diff --git a/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory
new file mode 100644
index 0000000..4a0f553
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignatureVerifierFactory
@@ -0,0 +1 @@
+org.eclipse.jgit.signing.ssh.SshSignatureVerifierFactory
\ No newline at end of file
diff --git a/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory
new file mode 100644
index 0000000..80f22c0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/resources/META-INF/services/org.eclipse.jgit.lib.SignerFactory
@@ -0,0 +1 @@
+org.eclipse.jgit.signing.ssh.SshSignerFactory
\ No newline at end of file
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 7da7181..773c4b9 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
@@ -61,6 +61,7 @@
 The {0} key actually received has the fingerprints:\n\
 {5}\n\
 {6}
+knownHostsRevokedCertificateMsg=Host ''{0}'' sent a certificate with a CA key that is marked as revoked in the known hosts file {1}.
 knownHostsRevokedKeyMsg=Host ''{0}'' sent a key that is marked as revoked in the known hosts file {1}.
 knownHostsUnknownKeyMsg=The authenticity of host ''{0}'' cannot be established.
 knownHostsUnknownKeyPrompt=Accept and store this key, and continue connecting?
@@ -125,4 +126,69 @@
 sshCommandTimeout={0} timed out after {1} seconds while opening the channel
 sshProcessStillRunning={0} is not yet completed, cannot get exit code
 sshProxySessionCloseFailed=Error while closing proxy session {0}
+signAllowedSignersCertAuthorityError=Garbage after cert-authority
+signAllowedSignersEmptyIdentity=Identities contains an empty identity; check for spurious extra commas: {0}
+signAllowedSignersEmptyNamespaces=Empty namespaces= is not allowed; to allow a key for any namespace, omit the namespaces option
+signAllowedSignersFormatError=Cannot parse allowed signers file {0}, problem at line {1}: {2}
+signAllowedSignersInvalidDate=Cannot parse valid-before or valid-after date {0}
+signAllowedSignersLineFormat=Invalid line format
+signAllowedSignersMultiple={0} is allowed only once
+signAllowedSignersNoIdentities=Line has no identity patterns
+signAllowedSignersPublicKeyParsing=Cannot parse public key {0}
+signAllowedSignersUnterminatedQuote=Unterminated double quote
+signCertAlgorithmMismatch=Certificate of type {0} with CA key {1} uses an incompatible signature algorithm {2}
+signCertAlgorithmUnknown=Certificate with CA key {0} is signed with an unknown algorithm {1}
+signCertificateExpired=Expired certificate with CA key {0}
+signCertificateInvalid=Certificate signature does not match on certificate with CA key {0}
+signCertificateNotForName=Certificate with CA key {0} does not apply for name ''{1}''
+signCertificateRevoked=Certificate with CA key {0} was revoked
+signCertificateTooEarly=Certificate with CA key {0} was not valid yet
+signCertificateWithoutPrincipals=Certificate with CA key {0} has no principals; identities from gpg.ssh.allowedSignersFile: {1}
+signDefaultKeyEmpty=git.ssh.defaultKeyCommand {0} returned no key
+signDefaultKeyFailed=git.ssh.defaultKeyCommand {0} failed with exit code {1}\n{2}
+signDefaultKeyInterrupted=git.ssh.defaultKeyCommand {0} was interrupted
+signGarbageAtEnd=SSH signature has extra bytes at the end
+signInvalidAlgorithm=SSH signature has invalid signature algorithm {0}
+signInvalidKeyDSA=SSH signatures with DSA keys or certificates are not supported; use a different signing key.
+signInvalidMagic=SSH signature does not start with "SSHSIG"
+signInvalidNamespace=Namespace of SSH signature should be ''git'' but is ''{0}''
+signInvalidSignature=SSH signature is invalid: {0}
+signInvalidVersion=Cannot verify signature with version {0}
+signKeyExpired=Expired key used for SSH signature
+signKeyRevoked=Key used for the SSH signature was revoked
+signKeyTooEarly=Key used for the SSH signature was not valid yet
+signKrlBlobLeftover=gpg.ssh.revocationFile has invalid blob section {0} with {1} leftover bytes
+signKrlBlobLengthInvalid=gpg.ssh.revocationFile has invalid blob length {1} in section {0}
+signKrlBlobLengthInvalidExpected=gpg.ssh.revocationFile has invalid blob length {1} (expected {2}) in section {0}
+signKrlCaKeyLengthInvalid=gpg.ssh.revocationFile has invalid CA key length {0} in certificates section
+signKrlCertificateLeftover=gpg.ssh.revocationFile has invalid certificates section with {0} leftover bytes
+signKrlCertificateSubsectionLeftover=gpg.ssh.revocationFile has invalid certificates subsection with {0} leftover bytes
+signKrlCertificateSubsectionLength=gpg.ssh.revocationFile has invalid certificates subsection length {0}
+signKrlEmptyRange=gpg.ssh.revocationFile has an empty range of certificate serial numbers
+signKrlInvalidBitSetLength=gpg.ssh.revocationFile has invalid certificate serial number bit set length {0}
+signKrlInvalidKeyIdLength=gpg.ssh.revocationFile has invalid certificate key ID length {0}
+signKrlInvalidMagic=gpg.ssh.revocationFile is not a binary OpenSSH key revocation list
+signKrlInvalidReservedLength=gpg.ssh.revocationFile has an invalid reserved string length {0}
+signKrlInvalidVersion=gpg.ssh.revocationFile: cannot read KRLs with FORMAT_VERSION {0}
+signKrlNoCertificateSubsection=gpg.ssh.revocationFile has certificate section without subsections
+signKrlSerialZero=gpg.ssh.revocationFile: certificate serial number zero cannot be revoked
+signKrlShortRange=gpg.ssh.revocationFile: short certificate serial number range, need at least 8 more bytes, got only {0}
+signKrlUnknownSection=gpg.ssh.revocationFile has an unknown section type {0}
+signKrlUnknownSubsection=gpg.ssh.revocationFile has an unknown certificates subsection type {0}
+signLogFailure=SSH signature verification failed
+signMismatchedSignatureAlgorithm=SSH signature made with an ''{0}'' key has incompatible signature algorithm ''{1}''
+signNoAgent=No connector for ssh-agent found; maybe include org.eclipse.jgit.ssh.apache.agent in the application.
+signNoPrincipalMatched=No principal matched in gpg.ssh.allowedSignersFile
+signNoPublicKey=No public key found with signing key {0}
+signNoSigningKey=Git config user.signingKey or gpg.ssh.defaultKeyCommand must be set for SSH signing.
+signNotUserCertificate=Certificate with CA key {0} used for the SSH signature is not a user certificate.
+signPublicKeyError=Cannot read public key {0}
+signSeeLog=SSH signature verification failed; see the log for details
+signSignatureError=Could not create the signature
+signStderr=Cannot read stderr
+signTooManyPrivateKeys=Private key file {0} must contain exactly one private key
+signTooManyPublicKeys=Public key file {0} must contain exactly one public key
+signUnknownHashAlgorithm=SSH Signature has an unknown hash algorithm {0}
+signUnknownSignatureAlgorithm=SSH Signature has an unknown signature algorithm {0}
+signWrongNamespace=Key may not be used in namespace "{0}".
 unknownProxyProtocol=Ignoring unknown proxy protocol {0}
\ No newline at end of file
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/AllowedSigners.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/AllowedSigners.java
new file mode 100644
index 0000000..80b171f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/AllowedSigners.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.io.ModifiableFileWatcher;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.signing.ssh.VerificationException;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.SystemReader;
+
+/**
+ * Encapsulates the allowed signers handling.
+ */
+final class AllowedSigners extends ModifiableFileWatcher {
+
+	private static final String CERT_AUTHORITY = "cert-authority"; //$NON-NLS-1$
+
+	private static final String NAMESPACES = "namespaces="; //$NON-NLS-1$
+
+	private static final String VALID_AFTER = "valid-after="; //$NON-NLS-1$
+
+	private static final String VALID_BEFORE = "valid-before="; //$NON-NLS-1$
+
+	private static final DateTimeFormatter SSH_DATE_FORMAT = new DateTimeFormatterBuilder()
+			.appendValue(ChronoField.YEAR, 4)
+			.appendValue(ChronoField.MONTH_OF_YEAR, 2)
+			.appendValue(ChronoField.DAY_OF_MONTH, 2)
+			.optionalStart()
+			.appendValue(ChronoField.HOUR_OF_DAY, 2)
+			.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+			.optionalStart()
+			.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
+			.toFormatter(Locale.ROOT);
+
+	private static final Predicate<AllowedEntry> CERTIFICATES = AllowedEntry::isCA;
+
+	private static final Predicate<AllowedEntry> PLAIN_KEYS = Predicate
+			.not(CERTIFICATES);
+
+	@SuppressWarnings("ArrayRecordComponent")
+	static record AllowedEntry(String[] identities, boolean isCA,
+			String[] namespaces, Instant validAfter, Instant validBefore,
+			String key) {
+		// Empty
+
+		@Override
+		public final boolean equals(Object any) {
+			if (this == any) {
+				return true;
+			}
+			if (any == null || !(any instanceof AllowedEntry)) {
+				return false;
+			}
+			AllowedEntry other = (AllowedEntry) any;
+			return isCA == other.isCA
+					&& Arrays.equals(identities, other.identities)
+					&& Arrays.equals(namespaces, other.namespaces)
+					&& Objects.equals(validAfter, other.validAfter)
+					&& Objects.equals(validBefore, other.validBefore)
+					&& Objects.equals(key, other.key);
+		}
+
+		@Override
+		public final int hashCode() {
+			int hash = Boolean.hashCode(isCA);
+			hash = hash * 31 + Arrays.hashCode(identities);
+			hash = hash * 31 + Arrays.hashCode(namespaces);
+			return hash * 31 + Objects.hash(validAfter, validBefore, key);
+		}
+	}
+
+	private static record State(Map<String, List<AllowedEntry>> entries) {
+		// Empty
+	}
+
+	private State state;
+
+	public AllowedSigners(Path path) {
+		super(path);
+		state = new State(new HashMap<>());
+	}
+
+	public String isAllowed(PublicKey key, String namespace, String name,
+			Instant time) throws IOException, VerificationException {
+		State currentState = refresh();
+		PublicKey keyToCheck = key;
+		if (key instanceof OpenSshCertificate certificate) {
+			AllowedEntry entry = find(currentState, certificate.getCaPubKey(),
+					namespace, name, time, CERTIFICATES);
+			if (entry != null) {
+				Collection<String> principals = certificate.getPrincipals();
+				if (principals.isEmpty()) {
+					// According to the OpenSSH documentation, a certificate
+					// without principals is valid for anyone.
+					//
+					// See https://man.openbsd.org/ssh-keygen.1#CERTIFICATES .
+					//
+					// However, the same documentation also says that a name
+					// must match both the entry's patterns and be listed in the
+					// certificate's principals.
+					//
+					// See https://man.openbsd.org/ssh-keygen.1#ALLOWED_SIGNERS
+					//
+					// git/OpenSSH considers signatures made by such
+					// certificates untrustworthy.
+					String identities;
+					if (!StringUtils.isEmptyOrNull(name)) {
+						// The name must have matched entry.identities.
+						identities = name;
+					} else {
+						identities = Arrays.stream(entry.identities())
+								.collect(Collectors.joining(",")); //$NON-NLS-1$
+					}
+					throw new VerificationException(false, MessageFormat.format(
+							SshdText.get().signCertificateWithoutPrincipals,
+							KeyUtils.getFingerPrint(certificate.getCaPubKey()),
+							identities));
+				}
+				if (!StringUtils.isEmptyOrNull(name)) {
+					if (!principals.contains(name)) {
+						throw new VerificationException(false,
+								MessageFormat.format(SshdText
+										.get().signCertificateNotForName,
+										KeyUtils.getFingerPrint(
+												certificate.getCaPubKey()),
+										name));
+					}
+					return name;
+				}
+				// Filter the principals listed in the certificate by
+				// the patterns defined in the file.
+				Set<String> filtered = new LinkedHashSet<>();
+				List<String> patterns = Arrays.asList(entry.identities());
+				for (String principal : principals) {
+					if (OpenSshConfigFile.patternMatch(patterns, principal)) {
+						filtered.add(principal);
+					}
+				}
+				return filtered.stream().collect(Collectors.joining(",")); //$NON-NLS-1$
+			}
+			// Certificate not found. git/OpenSSH considers this untrustworthy,
+			// even if the certified key itself might be listed.
+			return null;
+			// Alternative: go check for the certified key itself:
+			// keyToCheck = certificate.getCertPubKey();
+		}
+		AllowedEntry entry = find(currentState, keyToCheck, namespace, name,
+				time, PLAIN_KEYS);
+		if (entry != null) {
+			if (!StringUtils.isEmptyOrNull(name)) {
+				// The name must have matched entry.identities.
+				return name;
+			}
+			// No name given, but we consider the key valid: report the
+			// identities.
+			return Arrays.stream(entry.identities())
+					.collect(Collectors.joining(",")); //$NON-NLS-1$
+		}
+		return null;
+	}
+
+	private AllowedEntry find(State current, PublicKey key,
+			String namespace, String name, Instant time,
+			Predicate<AllowedEntry> filter)
+			throws VerificationException {
+		String k = PublicKeyEntry.toString(key);
+		VerificationException v = null;
+		List<AllowedEntry> candidates = current.entries().get(k);
+		if (candidates == null) {
+			return null;
+		}
+		for (AllowedEntry entry : candidates) {
+			if (!filter.test(entry)) {
+				continue;
+			}
+			if (name != null && !OpenSshConfigFile
+					.patternMatch(Arrays.asList(entry.identities()), name)) {
+				continue;
+			}
+			if (entry.namespaces() != null) {
+				if (!OpenSshConfigFile.patternMatch(
+						Arrays.asList(entry.namespaces()),
+						namespace)) {
+					if (v == null) {
+						v = new VerificationException(false,
+								MessageFormat.format(
+										SshdText.get().signWrongNamespace,
+										KeyUtils.getFingerPrint(key),
+										namespace));
+					}
+					continue;
+				}
+			}
+			if (time != null) {
+				if (entry.validAfter() != null
+						&& time.isBefore(entry.validAfter())) {
+					if (v == null) {
+						v = new VerificationException(true,
+								MessageFormat.format(
+										SshdText.get().signKeyTooEarly,
+										KeyUtils.getFingerPrint(key)));
+					}
+					continue;
+				} else if (entry.validBefore() != null
+						&& time.isAfter(entry.validBefore())) {
+					if (v == null) {
+						v = new VerificationException(true,
+								MessageFormat.format(
+										SshdText.get().signKeyTooEarly,
+										KeyUtils.getFingerPrint(key)));
+					}
+					continue;
+				}
+			}
+			return entry;
+		}
+		if (v != null) {
+			throw v;
+		}
+		return null;
+	}
+
+	private synchronized State refresh() throws IOException {
+		if (checkReloadRequired()) {
+			updateReloadAttributes();
+			try {
+				state = reload(getPath());
+			} catch (NoSuchFileException e) {
+				// File disappeared
+				resetReloadAttributes();
+				state = new State(new HashMap<>());
+			}
+		}
+		return state;
+	}
+
+	private static State reload(Path path) throws IOException {
+		Map<String, List<AllowedEntry>> entries = new HashMap<>();
+		try (BufferedReader r = Files.newBufferedReader(path,
+				StandardCharsets.UTF_8)) {
+			String line;
+			for (int lineNumber = 1;; lineNumber++) {
+				line = r.readLine();
+				if (line == null) {
+					break;
+				}
+				line = line.strip();
+				try {
+					AllowedEntry entry = parseLine(line);
+					if (entry != null) {
+						entries.computeIfAbsent(entry.key(),
+								k -> new ArrayList<>()).add(entry);
+					}
+				} catch (IOException | RuntimeException e) {
+					throw new IOException(MessageFormat.format(
+							SshdText.get().signAllowedSignersFormatError, path,
+							Integer.toString(lineNumber), line), e);
+				}
+			}
+		}
+		return new State(entries);
+	}
+
+	private static boolean matches(String src, String other, int offset) {
+		return src.regionMatches(true, offset, other, 0, other.length());
+	}
+
+	// Things below have package visibility for testing.
+
+	static AllowedEntry parseLine(String line)
+			throws IOException {
+		if (StringUtils.isEmptyOrNull(line) || line.charAt(0) == '#') {
+			return null;
+		}
+		int length = line.length();
+		if ((matches(line, CERT_AUTHORITY, 0)
+				&& CERT_AUTHORITY.length() < length
+				&& Character.isWhitespace(line.charAt(CERT_AUTHORITY.length())))
+				|| matches(line, NAMESPACES, 0)
+				|| matches(line, VALID_AFTER, 0)
+				|| matches(line, VALID_BEFORE, 0)) {
+			throw new StreamCorruptedException(
+					SshdText.get().signAllowedSignersNoIdentities);
+		}
+		int i = 0;
+		while (i < length && !Character.isWhitespace(line.charAt(i))) {
+			i++;
+		}
+		if (i >= length) {
+			throw new StreamCorruptedException(SshdText.get().signAllowedSignersLineFormat);
+		}
+		String[] identities = line.substring(0, i).split(","); //$NON-NLS-1$
+		if (Arrays.stream(identities).anyMatch(String::isEmpty)) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersEmptyIdentity,
+					line.substring(0, i)));
+		}
+		// Parse the options
+		i++;
+		boolean isCA = false;
+		List<String> namespaces = null;
+		Instant validAfter = null;
+		Instant validBefore = null;
+		while (i < length) {
+			// Skip whitespace
+			if (Character.isSpaceChar(line.charAt(i))) {
+				i++;
+				continue;
+			}
+			if (matches(line, CERT_AUTHORITY, i)) {
+				i += CERT_AUTHORITY.length();
+				isCA = true;
+				if (!Character.isWhitespace(line.charAt(i))) {
+					throw new StreamCorruptedException(SshdText.get().signAllowedSignersCertAuthorityError);
+				}
+				i++;
+			} else if (matches(line, NAMESPACES, i)) {
+				if (namespaces != null) {
+					throw new StreamCorruptedException(MessageFormat.format(
+							SshdText.get().signAllowedSignersMultiple,
+							NAMESPACES));
+				}
+				i += NAMESPACES.length();
+				Dequoted parsed = dequote(line, i);
+				i = parsed.after();
+				String ns = parsed.value();
+				String[] items = ns.split(","); //$NON-NLS-1$
+				namespaces = new ArrayList<>(items.length);
+				for (int j = 0; j < items.length; j++) {
+					String n = items[j].strip();
+					if (!n.isEmpty()) {
+						namespaces.add(n);
+					}
+				}
+				if (namespaces.isEmpty()) {
+					throw new StreamCorruptedException(
+							SshdText.get().signAllowedSignersEmptyNamespaces);
+				}
+			} else if (matches(line, VALID_AFTER, i)) {
+				if (validAfter != null) {
+					throw new StreamCorruptedException(MessageFormat.format(
+							SshdText.get().signAllowedSignersMultiple,
+							VALID_AFTER));
+				}
+				i += VALID_AFTER.length();
+				Dequoted parsed = dequote(line, i);
+				i = parsed.after();
+				validAfter = parseDate(parsed.value());
+			} else if (matches(line, VALID_BEFORE, i)) {
+				if (validBefore != null) {
+					throw new StreamCorruptedException(MessageFormat.format(
+							SshdText.get().signAllowedSignersMultiple,
+							VALID_BEFORE));
+				}
+				i += VALID_BEFORE.length();
+				Dequoted parsed = dequote(line, i);
+				i = parsed.after();
+				validBefore = parseDate(parsed.value());
+			} else {
+				break;
+			}
+		}
+		// Now we should be at the key
+		String key = parsePublicKey(line, i);
+		return new AllowedEntry(identities, isCA,
+				namespaces == null ? null : namespaces.toArray(new String[0]),
+				validAfter, validBefore, key);
+	}
+
+	static String parsePublicKey(String s, int from)
+			throws StreamCorruptedException {
+		int i = from;
+		int length = s.length();
+		while (i < length && Character.isWhitespace(s.charAt(i))) {
+			i++;
+		}
+		if (i >= length) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersPublicKeyParsing,
+					s.substring(from)));
+		}
+		int start = i;
+		while (i < length && !Character.isWhitespace(s.charAt(i))) {
+			i++;
+		}
+		if (i >= length) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersPublicKeyParsing,
+					s.substring(start)));
+		}
+		int endOfKeyType = i;
+		i = endOfKeyType + 1;
+		while (i < length && Character.isWhitespace(s.charAt(i))) {
+			i++;
+		}
+		int startOfKey = i;
+		while (i < length && !Character.isWhitespace(s.charAt(i))) {
+			i++;
+		}
+		if (i == startOfKey) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersPublicKeyParsing,
+					s.substring(start)));
+		}
+		String keyType = s.substring(start, endOfKeyType);
+		String key = s.substring(startOfKey, i);
+		if (!key.startsWith("AAAA")) { //$NON-NLS-1$
+			// base64 encoded SSH keys always start with four 'A's.
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signAllowedSignersPublicKeyParsing,
+					s.substring(start)));
+		}
+		return keyType + ' ' + s.substring(startOfKey, i);
+	}
+
+	static Instant parseDate(String input) {
+		// Allowed formats are YYYYMMDD[Z] or YYYYMMDDHHMM[SS][Z]. If 'Z', it's
+		// UTC, otherwise local time.
+		String timeSpec = input;
+		int length = input.length();
+		if (length < 8) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					SshdText.get().signAllowedSignersInvalidDate, input));
+		}
+		boolean isUTC = false;
+		if (timeSpec.charAt(length - 1) == 'Z') {
+			isUTC = true;
+			timeSpec = timeSpec.substring(0, length - 1);
+		}
+		LocalDateTime time;
+		TemporalAccessor temporalAccessor = SSH_DATE_FORMAT.parseBest(timeSpec,
+				LocalDateTime::from, LocalDate::from);
+		if (temporalAccessor instanceof LocalDateTime) {
+			time = (LocalDateTime) temporalAccessor;
+		} else {
+			time = ((LocalDate) temporalAccessor).atStartOfDay();
+		}
+		if (isUTC) {
+			return time.atOffset(ZoneOffset.UTC).toInstant();
+		}
+		ZoneId tz = SystemReader.getInstance().getTimeZoneId();
+		return time.atZone(tz).toInstant();
+	}
+
+	// OpenSSH uses the backslash *only* to quote the double-quote.
+	static Dequoted dequote(String line, int from) {
+		int length = line.length();
+		int i = from;
+		if (line.charAt(i) == '"') {
+			boolean quoted = false;
+			i++;
+			StringBuilder b = new StringBuilder();
+			while (i < length) {
+				char ch = line.charAt(i);
+				if (ch == '"') {
+					if (quoted) {
+						b.append(ch);
+						quoted = false;
+					} else {
+						break;
+					}
+				} else if (ch == '\\') {
+					quoted = true;
+				} else {
+					if (quoted) {
+						b.append('\\');
+					}
+					b.append(ch);
+					quoted = false;
+				}
+				i++;
+			}
+			if (i >= length) {
+				throw new IllegalArgumentException(
+						SshdText.get().signAllowedSignersUnterminatedQuote);
+			}
+			return new Dequoted(b.toString(), i + 1);
+		}
+		while (i < length && !Character.isWhitespace(line.charAt(i))) {
+			i++;
+		}
+		return new Dequoted(line.substring(from, i), i);
+	}
+
+	static record Dequoted(String value, int after) {
+		// Empty
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrl.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrl.java
new file mode 100644
index 0000000..6b19eb32
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshBinaryKrl.java
@@ -0,0 +1,491 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StreamCorruptedException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * An implementation of OpenSSH binary format key revocation lists (KRLs).
+ *
+ * @see <a href=
+ *      "https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.krl">PROTOCOL.krl</a>
+ */
+class OpenSshBinaryKrl {
+
+	/**
+	 * The "magic" bytes at the start of an OpenSSH binary KRL.
+	 */
+	static final byte[] MAGIC = { 'S', 'S', 'H', 'K', 'R', 'L', '\n', 0 };
+
+	private static final int FORMAT_VERSION = 1;
+
+	private static final int SECTION_CERTIFICATES = 1;
+
+	private static final int SECTION_KEY = 2;
+
+	private static final int SECTION_SHA1 = 3;
+
+	private static final int SECTION_SIGNATURE = 4; // Skipped
+
+	private static final int SECTION_SHA256 = 5;
+
+	private static final int SECTION_EXTENSION = 255; // Skipped
+
+	// Certificates
+
+	private static final int CERT_SERIAL_LIST = 0x20;
+
+	private static final int CERT_SERIAL_RANGES = 0x21;
+
+	private static final int CERT_SERIAL_BITS = 0x22;
+
+	private static final int CERT_KEY_IDS = 0x23;
+
+	private static final int CERT_EXTENSIONS = 0x39; // Skipped
+
+	private final Map<Blob, CertificateRevocation> certificates = new HashMap<>();
+
+	private static class CertificateRevocation {
+
+		final SerialRangeSet ranges = new SerialRangeSet();
+
+		final Set<String> keyIds = new HashSet<>();
+	}
+
+	// Plain keys
+
+	/**
+	 * A byte array that can be used as a key in a {@link Map} or {@link Set}.
+	 * {@link #equals(Object)} and {@link #hashCode()} are based on the content.
+	 *
+	 * @param blob
+	 *            the array to wrap
+	 */
+	@SuppressWarnings("ArrayRecordComponent")
+	private static record Blob(byte[] blob) {
+
+		@Override
+		public final boolean equals(Object any) {
+			if (this == any) {
+				return true;
+			}
+			if (any == null || !(any instanceof Blob)) {
+				return false;
+			}
+			Blob other = (Blob) any;
+			return Arrays.equals(blob, other.blob);
+		}
+
+		@Override
+		public final int hashCode() {
+			return Arrays.hashCode(blob);
+		}
+	}
+
+	private final Set<Blob> blobs = new HashSet<>();
+
+	private final Set<Blob> sha1 = new HashSet<>();
+
+	private final Set<Blob> sha256 = new HashSet<>();
+
+	private OpenSshBinaryKrl() {
+		// No public instantiation, use load(InputStream, boolean) instead.
+	}
+
+	/**
+	 * Tells whether the given key has been revoked.
+	 *
+	 * @param key
+	 *            {@link PublicKey} to check
+	 * @return {@code true} if the key was revoked, {@code false} otherwise
+	 */
+	boolean isRevoked(PublicKey key) {
+		if (key instanceof OpenSshCertificate certificate) {
+			if (certificates.isEmpty()) {
+				return false;
+			}
+			// These apply to all certificates
+			if (isRevoked(certificate, certificates.get(null))) {
+				return true;
+			}
+			if (isRevoked(certificate,
+					certificates.get(blob(certificate.getCaPubKey())))) {
+				return true;
+			}
+			// Keys themselves are checked in OpenSshKrl.
+			return false;
+		}
+		if (!blobs.isEmpty() && blobs.contains(blob(key))) {
+			return true;
+		}
+		if (!sha256.isEmpty() && sha256.contains(hash("SHA256", key))) { //$NON-NLS-1$
+			return true;
+		}
+		if (!sha1.isEmpty() && sha1.contains(hash("SHA1", key))) { //$NON-NLS-1$
+			return true;
+		}
+		return false;
+	}
+
+	private boolean isRevoked(OpenSshCertificate certificate,
+			CertificateRevocation revocations) {
+		if (revocations == null) {
+			return false;
+		}
+		String id = certificate.getId();
+		if (!StringUtils.isEmptyOrNull(id) && revocations.keyIds.contains(id)) {
+			return true;
+		}
+		long serial = certificate.getSerial();
+		if (serial != 0 && revocations.ranges.contains(serial)) {
+			return true;
+		}
+		return false;
+	}
+
+	private Blob blob(PublicKey key) {
+		ByteArrayBuffer buf = new ByteArrayBuffer();
+		buf.putRawPublicKey(key);
+		return new Blob(buf.getCompactData());
+	}
+
+	private Blob hash(String algorithm, PublicKey key) {
+		ByteArrayBuffer buf = new ByteArrayBuffer();
+		buf.putRawPublicKey(key);
+		try {
+			return new Blob(MessageDigest.getInstance(algorithm)
+					.digest(buf.getCompactData()));
+		} catch (NoSuchAlgorithmException e) {
+			throw new JGitInternalException(e.getMessage(), e);
+		}
+	}
+
+	/**
+	 * Loads a binary KRL from the given stream.
+	 *
+	 * @param in
+	 *            {@link InputStream} to read from
+	 * @param magicSkipped
+	 *            whether the {@link #MAGIC} bytes at the beginning have already
+	 *            been skipped
+	 * @return a new {@link OpenSshBinaryKrl}.
+	 * @throws IOException
+	 *             if the stream cannot be read as an OpenSSH binary KRL
+	 */
+	@NonNull
+	static OpenSshBinaryKrl load(InputStream in, boolean magicSkipped)
+			throws IOException {
+		if (!magicSkipped) {
+			byte[] magic = new byte[MAGIC.length];
+			IO.readFully(in, magic);
+			if (!Arrays.equals(magic, MAGIC)) {
+				throw new StreamCorruptedException(
+						SshdText.get().signKrlInvalidMagic);
+			}
+		}
+		skipHeader(in);
+		return load(in);
+	}
+
+	private static long getUInt(InputStream in) throws IOException {
+		byte[] buf = new byte[Integer.BYTES];
+		IO.readFully(in, buf);
+		return BufferUtils.getUInt(buf);
+	}
+
+	private static long getLong(InputStream in) throws IOException {
+		byte[] buf = new byte[Long.BYTES];
+		IO.readFully(in, buf);
+		return BufferUtils.getLong(buf, 0, Long.BYTES);
+	}
+
+	private static void skipHeader(InputStream in) throws IOException {
+		long version = getUInt(in);
+		if (version != FORMAT_VERSION) {
+			throw new StreamCorruptedException(
+					MessageFormat.format(SshdText.get().signKrlInvalidVersion,
+							Long.valueOf(version)));
+		}
+		// krl_version, generated_date, flags (none defined in version 1)
+		in.skip(24);
+		in.skip(getUInt(in)); // reserved
+		in.skip(getUInt(in)); // comment
+	}
+
+	private static OpenSshBinaryKrl load(InputStream in) throws IOException {
+		OpenSshBinaryKrl krl = new OpenSshBinaryKrl();
+		for (;;) {
+			int sectionType = in.read();
+			if (sectionType < 0) {
+				break; // EOF
+			}
+			switch (sectionType) {
+			case SECTION_CERTIFICATES:
+				readCertificates(krl.certificates, in, getUInt(in));
+				break;
+			case SECTION_KEY:
+				readBlobs("explicit_keys", krl.blobs, in, getUInt(in), 0); //$NON-NLS-1$
+				break;
+			case SECTION_SHA1:
+				readBlobs("fingerprint_sha1", krl.sha1, in, getUInt(in), 20); //$NON-NLS-1$
+				break;
+			case SECTION_SIGNATURE:
+				// Unsupported as of OpenSSH 9.4. It even refuses to load such
+				// KRLs. Just skip it.
+				in.skip(getUInt(in));
+				break;
+			case SECTION_SHA256:
+				readBlobs("fingerprint_sha256", krl.sha256, in, getUInt(in), //$NON-NLS-1$
+						32);
+				break;
+			case SECTION_EXTENSION:
+				// No extensions are defined for version 1 KRLs.
+				in.skip(getUInt(in));
+				break;
+			default:
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlUnknownSection,
+						Integer.valueOf(sectionType)));
+			}
+		}
+		return krl;
+	}
+
+	private static void readBlobs(String sectionName, Set<Blob> blobs,
+			InputStream in, long sectionLength, long expectedBlobLength)
+			throws IOException {
+		while (sectionLength >= Integer.BYTES) {
+			// Read blobs.
+			long blobLength = getUInt(in);
+			sectionLength -= Integer.BYTES;
+			if (blobLength > sectionLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlBlobLengthInvalid, sectionName,
+						Long.valueOf(blobLength)));
+			}
+			if (expectedBlobLength != 0 && blobLength != expectedBlobLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlBlobLengthInvalidExpected,
+						sectionName, Long.valueOf(blobLength),
+						Long.valueOf(expectedBlobLength)));
+			}
+			byte[] blob = new byte[(int) blobLength];
+			IO.readFully(in, blob);
+			sectionLength -= blobLength;
+			blobs.add(new Blob(blob));
+		}
+		if (sectionLength != 0) {
+			throw new StreamCorruptedException(
+					MessageFormat.format(SshdText.get().signKrlBlobLeftover,
+							sectionName, Long.valueOf(sectionLength)));
+		}
+	}
+
+	private static void readCertificates(Map<Blob, CertificateRevocation> certs,
+			InputStream in, long sectionLength) throws IOException {
+		long keyLength = getUInt(in);
+		sectionLength -= Integer.BYTES;
+		if (keyLength > sectionLength) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signKrlCaKeyLengthInvalid,
+					Long.valueOf(keyLength)));
+		}
+		Blob key = null;
+		if (keyLength > 0) {
+			byte[] blob = new byte[(int) keyLength];
+			IO.readFully(in, blob);
+			key = new Blob(blob);
+			sectionLength -= keyLength;
+		}
+		CertificateRevocation rev = certs.computeIfAbsent(key,
+				k -> new CertificateRevocation());
+		long reservedLength = getUInt(in);
+		sectionLength -= Integer.BYTES;
+		if (reservedLength > sectionLength) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signKrlCaKeyLengthInvalid,
+					Long.valueOf(reservedLength)));
+		}
+		in.skip(reservedLength);
+		sectionLength -= reservedLength;
+		if (sectionLength == 0) {
+			throw new StreamCorruptedException(
+					SshdText.get().signKrlNoCertificateSubsection);
+		}
+		while (sectionLength > 0) {
+			int subSection = in.read();
+			if (subSection < 0) {
+				throw new EOFException();
+			}
+			sectionLength--;
+			if (sectionLength < Integer.BYTES) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlCertificateLeftover,
+						Long.valueOf(sectionLength)));
+			}
+			long subLength = getUInt(in);
+			sectionLength -= Integer.BYTES;
+			if (subLength > sectionLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlCertificateSubsectionLength,
+						Long.valueOf(subLength)));
+			}
+			if (subLength > 0) {
+				switch (subSection) {
+				case CERT_SERIAL_LIST:
+					readSerials(rev.ranges, in, subLength, false);
+					break;
+				case CERT_SERIAL_RANGES:
+					readSerials(rev.ranges, in, subLength, true);
+					break;
+				case CERT_SERIAL_BITS:
+					readSerialBitSet(rev.ranges, in, subLength);
+					break;
+				case CERT_KEY_IDS:
+					readIds(rev.keyIds, in, subLength);
+					break;
+				case CERT_EXTENSIONS:
+					in.skip(subLength);
+					break;
+				default:
+					throw new StreamCorruptedException(MessageFormat.format(
+							SshdText.get().signKrlUnknownSubsection,
+							Long.valueOf(subSection)));
+				}
+			}
+			sectionLength -= subLength;
+		}
+	}
+
+	private static void readSerials(SerialRangeSet set, InputStream in,
+			long length, boolean ranges) throws IOException {
+		while (length >= Long.BYTES) {
+			long a = getLong(in);
+			length -= Long.BYTES;
+			if (a == 0) {
+				throw new StreamCorruptedException(
+						SshdText.get().signKrlSerialZero);
+			}
+			if (!ranges) {
+				set.add(a);
+				continue;
+			}
+			if (length < Long.BYTES) {
+				throw new StreamCorruptedException(
+						MessageFormat.format(SshdText.get().signKrlShortRange,
+								Long.valueOf(length)));
+			}
+			long b = getLong(in);
+			length -= Long.BYTES;
+			if (Long.compareUnsigned(a, b) > 0) {
+				throw new StreamCorruptedException(
+						SshdText.get().signKrlEmptyRange);
+			}
+			set.add(a, b);
+		}
+		if (length != 0) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signKrlCertificateSubsectionLeftover,
+					Long.valueOf(length)));
+		}
+	}
+
+	private static void readSerialBitSet(SerialRangeSet set, InputStream in,
+			long subLength) throws IOException {
+		while (subLength > 0) {
+			if (subLength < Long.BYTES) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlCertificateSubsectionLeftover,
+						Long.valueOf(subLength)));
+			}
+			long base = getLong(in);
+			subLength -= Long.BYTES;
+			if (subLength < Integer.BYTES) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlCertificateSubsectionLeftover,
+						Long.valueOf(subLength)));
+			}
+			long setLength = getUInt(in);
+			subLength -= Integer.BYTES;
+			if (setLength == 0 || setLength > subLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlInvalidBitSetLength,
+						Long.valueOf(setLength)));
+			}
+			// Now process the bits. Note that the mpint is stored MSB first.
+			//
+			// We set individual serial numbers (one for each set bit) and let
+			// the SerialRangeSet take care of coalescing for successive runs
+			// of set bits.
+			int n = (int) setLength;
+			for (int i = n - 1; i >= 0; i--) {
+				int b = in.read();
+				if (b < 0) {
+					throw new EOFException();
+				} else if (b == 0) {
+					// Stored as an mpint: may have leading zero bytes (actually
+					// at most one; if the high bit of the first byte is set).
+					continue;
+				}
+				for (int bit = 0,
+						mask = 1; bit < Byte.SIZE; bit++, mask <<= 1) {
+					if ((b & mask) != 0) {
+						set.add(base + (i * Byte.SIZE) + bit);
+					}
+				}
+			}
+			subLength -= setLength;
+		}
+	}
+
+	private static void readIds(Set<String> ids, InputStream in, long subLength)
+			throws IOException {
+		while (subLength >= Integer.BYTES) {
+			long length = getUInt(in);
+			subLength -= Integer.BYTES;
+			if (length > subLength) {
+				throw new StreamCorruptedException(MessageFormat.format(
+						SshdText.get().signKrlInvalidKeyIdLength,
+						Long.valueOf(length)));
+			}
+			byte[] bytes = new byte[(int) length];
+			IO.readFully(in, bytes);
+			ids.add(new String(bytes, StandardCharsets.UTF_8));
+			subLength -= length;
+		}
+		if (subLength != 0) {
+			throw new StreamCorruptedException(MessageFormat.format(
+					SshdText.get().signKrlCertificateSubsectionLeftover,
+					Long.valueOf(subLength)));
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshKrl.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshKrl.java
new file mode 100644
index 0000000..7993def
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshKrl.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.io.ModifiableFileWatcher;
+import org.eclipse.jgit.util.IO;
+
+/**
+ * An implementation of an OpenSSH key revocation list (KRL), either a binary
+ * KRL or a simple list of public keys.
+ */
+class OpenSshKrl extends ModifiableFileWatcher {
+
+	private static record State(Set<String> keys, OpenSshBinaryKrl krl) {
+		// Empty
+	}
+
+	private State state;
+
+	public OpenSshKrl(Path path) {
+		super(path);
+		state = new State(Set.of(), null);
+	}
+
+	public boolean isRevoked(PublicKey key) throws IOException {
+		State current = refresh();
+		return isRevoked(current, key);
+	}
+
+	private boolean isRevoked(State current, PublicKey key) {
+		if (key instanceof OpenSshCertificate cert) {
+			OpenSshBinaryKrl krl = current.krl();
+			if (krl != null && krl.isRevoked(cert)) {
+				return true;
+			}
+			if (isRevoked(current, cert.getCaPubKey())
+					|| isRevoked(current, cert.getCertPubKey())) {
+				return true;
+			}
+			return false;
+		}
+		OpenSshBinaryKrl krl = current.krl();
+		if (krl != null) {
+			return krl.isRevoked(key);
+		}
+		return current.keys().contains(PublicKeyEntry.toString(key));
+	}
+
+	private synchronized State refresh() throws IOException {
+		if (checkReloadRequired()) {
+			updateReloadAttributes();
+			try {
+				state = reload(getPath());
+			} catch (NoSuchFileException e) {
+				// File disappeared
+				resetReloadAttributes();
+				state = new State(Set.of(), null);
+			}
+		}
+		return state;
+	}
+
+	private static State reload(Path path) throws IOException {
+		try (BufferedInputStream in = new BufferedInputStream(
+				Files.newInputStream(path))) {
+			byte[] magic = new byte[OpenSshBinaryKrl.MAGIC.length];
+			in.mark(magic.length);
+			IO.readFully(in, magic);
+			if (Arrays.equals(magic, OpenSshBinaryKrl.MAGIC)) {
+				return new State(null, OpenSshBinaryKrl.load(in, true));
+			}
+			// Otherwise try reading it textually
+			in.reset();
+			return loadTextKrl(in);
+		}
+	}
+
+	private static State loadTextKrl(InputStream in) throws IOException {
+		Set<String> keys = new HashSet<>();
+		try (BufferedReader r = new BufferedReader(
+				new InputStreamReader(in, StandardCharsets.UTF_8))) {
+			String line;
+			for (;;) {
+				line = r.readLine();
+				if (line == null) {
+					break;
+				}
+				line = line.strip();
+				if (line.isEmpty() || line.charAt(0) == '#') {
+					continue;
+				}
+				keys.add(AllowedSigners.parsePublicKey(line, 0));
+			}
+		}
+		return new State(keys, null);
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshSigningKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshSigningKeyDatabase.java
new file mode 100644
index 0000000..aa26886
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/OpenSshSigningKeyDatabase.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.signing.ssh.CachingSigningKeyDatabase;
+import org.eclipse.jgit.signing.ssh.VerificationException;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * A {@link CachingSigningKeyDatabase} using the OpenSSH allowed signers file
+ * and the OpenSSH key revocation list.
+ */
+public class OpenSshSigningKeyDatabase implements CachingSigningKeyDatabase {
+
+	// Keep caches of allowed signers and KRLs. Cache by canonical path.
+
+	private static final int DEFAULT_CACHE_SIZE = 5;
+
+	private AtomicInteger cacheSize = new AtomicInteger(DEFAULT_CACHE_SIZE);
+
+	private class LRU<K, V> extends LinkedHashMap<K, V> {
+
+		private static final long serialVersionUID = 1L;
+
+		LRU() {
+			super(DEFAULT_CACHE_SIZE, 0.75f, true);
+		}
+
+		@Override
+		protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
+			return size() > cacheSize.get();
+		}
+	}
+
+	private final HashMap<Path, AllowedSigners> allowedSigners = new LRU<>();
+
+	private final HashMap<Path, OpenSshKrl> revocations = new LRU<>();
+
+	@Override
+	public boolean isRevoked(Repository repository, GpgConfig config,
+			PublicKey key) throws IOException {
+		String fileName = config.getSshRevocationFile();
+		if (StringUtils.isEmptyOrNull(fileName)) {
+			return false;
+		}
+		File file = getFile(repository, fileName);
+		OpenSshKrl revocationList;
+		synchronized (revocations) {
+			revocationList = revocations.computeIfAbsent(file.toPath(),
+					OpenSshKrl::new);
+		}
+		return revocationList.isRevoked(key);
+	}
+
+	@Override
+	public String isAllowed(Repository repository, GpgConfig config,
+			PublicKey key, String namespace, PersonIdent ident)
+			throws IOException, VerificationException {
+		String fileName = config.getSshAllowedSignersFile();
+		if (StringUtils.isEmptyOrNull(fileName)) {
+			// No file configured. Git would error out.
+			return null;
+		}
+		File file = getFile(repository, fileName);
+		AllowedSigners allowed;
+		synchronized (allowedSigners) {
+			allowed = allowedSigners.computeIfAbsent(file.toPath(),
+					AllowedSigners::new);
+		}
+		Instant gitTime = null;
+		if (ident != null) {
+			gitTime = ident.getWhenAsInstant();
+		}
+		return allowed.isAllowed(key, namespace, null, gitTime);
+	}
+
+	private File getFile(@NonNull Repository repository, String fileName)
+			throws IOException {
+		File file;
+		if (fileName.startsWith("~/") //$NON-NLS-1$
+				|| fileName.startsWith('~' + File.separator)) {
+			file = FS.DETECTED.resolve(FS.DETECTED.userHome(),
+					fileName.substring(2));
+		} else {
+			file = new File(fileName);
+			if (!file.isAbsolute()) {
+				file = new File(repository.getWorkTree(), fileName);
+			}
+		}
+		return file.getCanonicalFile();
+	}
+
+	@Override
+	public int getCacheSize() {
+		return cacheSize.get();
+	}
+
+	@Override
+	public void setCacheSize(int size) {
+		if (size > 0) {
+			cacheSize.set(size);
+			pruneCache(size);
+		}
+	}
+
+	private void pruneCache(int size) {
+		prune(allowedSigners, size);
+		prune(revocations, size);
+	}
+
+	private void prune(HashMap<?, ?> map, int size) {
+		synchronized (map) {
+			if (map.size() <= size) {
+				return;
+			}
+			Iterator<?> iter = map.entrySet().iterator();
+			int i = 0;
+			while (iter.hasNext() && i < size) {
+				iter.next();
+				i++;
+			}
+			while (iter.hasNext()) {
+				iter.next();
+				iter.remove();
+			}
+		}
+	}
+
+	@Override
+	public void clearCache() {
+		synchronized (allowedSigners) {
+			allowedSigners.clear();
+		}
+		synchronized (revocations) {
+			revocations.clear();
+		}
+	}
+
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SerialRangeSet.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SerialRangeSet.java
new file mode 100644
index 0000000..f4eb884
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SerialRangeSet.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.util.TreeMap;
+
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+
+/**
+ * Encapsulates the storage for revoked certificate serial numbers.
+ */
+class SerialRangeSet {
+
+	/**
+	 * A range of certificate serial numbers [from..to], i.e., with both range
+	 * limits included.
+	 */
+	private interface SerialRange {
+
+		long from();
+
+		long to();
+	}
+
+	private static record Singleton(long from) implements SerialRange {
+
+		@Override
+		public long to() {
+			return from;
+		}
+	}
+
+	private static record Range(long from, long to) implements SerialRange {
+
+		public Range(long from, long to) {
+			if (Long.compareUnsigned(from, to) > 0) {
+				throw new IllegalArgumentException(
+						SshdText.get().signKrlEmptyRange);
+			}
+			this.from = from;
+			this.to = to;
+		}
+	}
+
+	// We use the same data structure as OpenSSH; basically a TreeSet of mutable
+	// SerialRanges. To get "mutability", the set is implemented as a TreeMap
+	// with the same elements as keys and values.
+	//
+	// get(x) will return null if none of the serial numbers in the range x is
+	// in the set, and some range (partially) overlapping with x otherwise.
+	//
+	// containsKey(x) will return true if there is any (partially) overlapping
+	// range in the TreeMap.
+	private final TreeMap<SerialRange, SerialRange> ranges = new TreeMap<>(
+			SerialRangeSet::compare);
+
+	private static int compare(SerialRange a, SerialRange b) {
+		// Return == if they overlap
+		if (Long.compareUnsigned(a.to(), b.from()) >= 0
+				&& Long.compareUnsigned(a.from(), b.to()) <= 0) {
+			return 0;
+		}
+		return Long.compareUnsigned(a.from(), b.from());
+	}
+
+	void add(long serial) {
+		add(ranges, new Singleton(serial));
+	}
+
+	void add(long from, long to) {
+		add(ranges, new Range(from, to));
+	}
+
+	boolean contains(long serial) {
+		return ranges.containsKey(new Singleton(serial));
+	}
+
+	int size() {
+		return ranges.size();
+	}
+
+	boolean isEmpty() {
+		return ranges.isEmpty();
+	}
+
+	private static void add(TreeMap<SerialRange, SerialRange> ranges,
+			SerialRange newRange) {
+		for (;;) {
+			SerialRange existing = ranges.get(newRange);
+			if (existing == null) {
+				break;
+			}
+			if (Long.compareUnsigned(existing.from(), newRange.from()) <= 0
+					&& Long.compareUnsigned(existing.to(),
+							newRange.to()) >= 0) {
+				// newRange completely contained in existing
+				return;
+			}
+			ranges.remove(existing);
+			long newFrom = newRange.from();
+			if (Long.compareUnsigned(existing.from(), newFrom) < 0) {
+				newFrom = existing.from();
+			}
+			long newTo = newRange.to();
+			if (Long.compareUnsigned(existing.to(), newTo) > 0) {
+				newTo = existing.to();
+			}
+			newRange = new Range(newFrom, newTo);
+		}
+		// No overlapping range exists: check for coalescing with the
+		// previous/next range
+		SerialRange prev = ranges.floorKey(newRange);
+		if (prev != null && newRange.from() - prev.to() == 1) {
+			ranges.remove(prev);
+			newRange = new Range(prev.from(), newRange.to());
+		}
+		SerialRange next = ranges.ceilingKey(newRange);
+		if (next != null && next.from() - newRange.to() == 1) {
+			ranges.remove(next);
+			newRange = new Range(newRange.from(), next.to());
+		}
+		ranges.put(newRange, newRange);
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SigningDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SigningDatabase.java
new file mode 100644
index 0000000..e2e1a36
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SigningDatabase.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import org.eclipse.jgit.signing.ssh.CachingSigningKeyDatabase;
+import org.eclipse.jgit.signing.ssh.SigningKeyDatabase;
+
+/**
+ * A global {@link SigningKeyDatabase} instance.
+ */
+public final class SigningDatabase {
+
+	private static SigningKeyDatabase INSTANCE = new OpenSshSigningKeyDatabase();
+
+	private SigningDatabase() {
+		// No instantiation
+	}
+
+	/**
+	 * Obtains the current instance.
+	 *
+	 * @return the global {@link SigningKeyDatabase}
+	 */
+	public static synchronized SigningKeyDatabase getInstance() {
+		return INSTANCE;
+	}
+
+	/**
+	 * Sets the global {@link SigningKeyDatabase}.
+	 *
+	 * @param database
+	 *            to set; if {@code null} a default database using the OpenSSH
+	 *            allowed signers file and the OpenSSH revocation list mechanism
+	 *            is used.
+	 * @return the previously set {@link SigningKeyDatabase}
+	 */
+	public static synchronized SigningKeyDatabase setInstance(
+			SigningKeyDatabase database) {
+		SigningKeyDatabase previous = INSTANCE;
+		if (database != INSTANCE) {
+			if (INSTANCE instanceof CachingSigningKeyDatabase caching) {
+				caching.clearCache();
+			}
+			if (database == null) {
+				INSTANCE = new OpenSshSigningKeyDatabase();
+			} else {
+				INSTANCE = database;
+			}
+		}
+		return previous;
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java
new file mode 100644
index 0000000..040c6d4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshCertificateUtils.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.time.Instant;
+
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+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.buffer.ByteArrayBuffer;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility methods for working with OpenSSH certificates.
+ */
+final class SshCertificateUtils {
+
+	private static final Logger LOG = LoggerFactory
+			.getLogger(SshCertificateUtils.class);
+
+	/**
+	 * Verifies a certificate: checks that it is a user certificate and has a
+	 * valid signature, and if a time is given, that the certificate is valid at
+	 * that time.
+	 *
+	 * @param certificate
+	 *            {@link OpenSshCertificate} to verify
+	 * @param signatureTime
+	 *            {@link Instant} to check whether the certificate is valid at
+	 *            that time; maybe {@code null}, in which case the valid-time
+	 *            check is skipped.
+	 * @return {@code null} if the certificate is valid; otherwise a descriptive
+	 *         message
+	 */
+	static String verify(OpenSshCertificate certificate,
+			Instant signatureTime) {
+		if (!OpenSshCertificate.Type.USER.equals(certificate.getType())) {
+			return MessageFormat.format(SshdText.get().signNotUserCertificate,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+		}
+		String message = verifySignature(certificate);
+		if (message == null && signatureTime != null) {
+			message = checkExpiration(certificate, signatureTime);
+		}
+		return message;
+	}
+
+	/**
+	 * Verifies the signature on a certificate.
+	 *
+	 * @param certificate
+	 *            {@link OpenSshCertificate} to verify
+	 * @return {@code null} if the signature is valid; otherwise a descriptive
+	 *         message
+	 */
+	static String verifySignature(OpenSshCertificate certificate) {
+		// Verify the signature on the certificate.
+		//
+		// Note that OpenSSH certificates do not support chaining.
+		//
+		// ssh-keygen refuses to create a certificate for a certificate, so the
+		// certified key cannot be another OpenSshCertificate. Additionally,
+		// when creating a certificate ssh-keygen loads the CA private key to
+		// make the signature and reconstructs the public key that it stores in
+		// the certificate from that, so the CA public key also cannot be an
+		// OpenSshCertificate.
+		PublicKey caKey = certificate.getCaPubKey();
+		PublicKey certifiedKey = certificate.getCertPubKey();
+		if (caKey == null
+				|| caKey instanceof OpenSshCertificate
+				|| certifiedKey == null
+				|| certifiedKey instanceof OpenSshCertificate) {
+			return SshdText.get().signCertificateInvalid;
+		}
+		// Verify that key type and algorithm match
+		String keyType = KeyUtils.getKeyType(caKey);
+		String certAlgorithm = certificate.getSignatureAlgorithm();
+		if (!KeyUtils.getCanonicalKeyType(keyType)
+				.equals(KeyUtils.getCanonicalKeyType(certAlgorithm))) {
+			return MessageFormat.format(
+					SshdText.get().signCertAlgorithmMismatch, keyType,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()),
+					certAlgorithm);
+		}
+		BuiltinSignatures factory = BuiltinSignatures
+				.fromFactoryName(certAlgorithm);
+		if (factory == null || !factory.isSupported()) {
+			return MessageFormat.format(SshdText.get().signCertAlgorithmUnknown,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()),
+					certAlgorithm);
+		}
+		Signature signer = factory.create();
+		try {
+			signer.initVerifier(null, caKey);
+			signer.update(null, getBlob(certificate));
+			if (signer.verify(null, certificate.getRawSignature())) {
+				return null;
+			}
+		} catch (Exception e) {
+			LOG.warn("{}", SshdText.get().signLogFailure, e); //$NON-NLS-1$
+			return SshdText.get().signSeeLog;
+		}
+		return MessageFormat.format(SshdText.get().signCertificateInvalid,
+				KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+	}
+
+	private static byte[] getBlob(OpenSshCertificate certificate) {
+		// Theoretically, this should be just certificate.getMessage(). But
+		// Apache MINA sshd has a bug and may return additional bytes if the
+		// certificate is not the first thing in the buffer it was read from.
+		// As a work-around, re-create the signed blob from scratch.
+		//
+		// This may be replaced by return certificate.getMessage() once the
+		// upstream bug is fixed.
+		//
+		// See https://github.com/apache/mina-sshd/issues/618
+		Buffer tmp = new ByteArrayBuffer();
+		tmp.putString(certificate.getKeyType());
+		tmp.putBytes(certificate.getNonce());
+		tmp.putRawPublicKeyBytes(certificate.getCertPubKey());
+		tmp.putLong(certificate.getSerial());
+		tmp.putInt(certificate.getType().getCode());
+		tmp.putString(certificate.getId());
+		Buffer list = new ByteArrayBuffer();
+		list.putStringList(certificate.getPrincipals(), false);
+		tmp.putBytes(list.getCompactData());
+		tmp.putLong(certificate.getValidAfter());
+		tmp.putLong(certificate.getValidBefore());
+		tmp.putCertificateOptions(certificate.getCriticalOptions());
+		tmp.putCertificateOptions(certificate.getExtensions());
+		tmp.putString(certificate.getReserved());
+		Buffer inner = new ByteArrayBuffer();
+		inner.putRawPublicKey(certificate.getCaPubKey());
+		tmp.putBytes(inner.getCompactData());
+		return tmp.getCompactData();
+	}
+
+	/**
+	 * Checks whether a certificate is valid at a given time.
+	 *
+	 * @param certificate
+	 *            {@link OpenSshCertificate} to check
+	 * @param signatureTime
+	 *            {@link Instant} to check
+	 * @return {@code null} if the certificate is valid at the given instant;
+	 *         otherwise a descriptive message
+	 */
+	static String checkExpiration(OpenSshCertificate certificate,
+			@NonNull Instant signatureTime) {
+		long instant = signatureTime.getEpochSecond();
+		if (Long.compareUnsigned(instant, certificate.getValidAfter()) < 0) {
+			return MessageFormat.format(SshdText.get().signCertificateTooEarly,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+		} else if (Long.compareUnsigned(instant,
+				certificate.getValidBefore()) > 0) {
+			return MessageFormat.format(SshdText.get().signCertificateExpired,
+					KeyUtils.getFingerPrint(certificate.getCaPubKey()));
+		}
+		return null;
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java
new file mode 100644
index 0000000..bc72196
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureConstants.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jgit.lib.Constants;
+
+/**
+ * Defines common constants for SSH signatures.
+ */
+final class SshSignatureConstants {
+
+	private static final String SIGNATURE_END = "-----END SSH SIGNATURE-----"; //$NON-NLS-1$
+
+	static final byte[] MAGIC = { 'S', 'S', 'H', 'S', 'I', 'G' };
+
+	static final int VERSION = 1;
+
+	static final String NAMESPACE = "git"; //$NON-NLS-1$
+
+	static final byte[] ARMOR_HEAD = Constants.SSH_SIGNATURE_PREFIX
+			.getBytes(StandardCharsets.US_ASCII);
+
+	static final byte[] ARMOR_END = SIGNATURE_END
+			.getBytes(StandardCharsets.US_ASCII);
+
+	private SshSignatureConstants() {
+		// No instantiation
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifier.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifier.java
new file mode 100644
index 0000000..76be340
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSignatureVerifier.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Locale;
+
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+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.buffer.ByteArrayBuffer;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.signing.ssh.CachingSigningKeyDatabase;
+import org.eclipse.jgit.signing.ssh.SigningKeyDatabase;
+import org.eclipse.jgit.signing.ssh.VerificationException;
+import org.eclipse.jgit.util.Base64;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link SignatureVerifier} for SSH signatures.
+ */
+public class SshSignatureVerifier implements SignatureVerifier {
+
+	private static final Logger LOG = LoggerFactory
+			.getLogger(SshSignatureVerifier.class);
+
+	private static final byte[] OBJECT = { 'o', 'b', 'j', 'e', 'c', 't', ' ' };
+
+	private static final byte[] TREE = { 't', 'r', 'e', 'e', ' ' };
+
+	private static final byte[] TYPE = { 't', 'y', 'p', 'e', ' ' };
+
+	@Override
+	public String getName() {
+		return "ssh"; //$NON-NLS-1$
+	}
+
+	@Override
+	public SignatureVerification verify(Repository repository, GpgConfig config,
+			byte[] data, byte[] signatureData) throws IOException {
+		// This is a bit stupid. SSH signatures do not store a signer, nor a
+		// time the signature was created. So we must use the committer's or
+		// tagger's PersonIdent, but here we have neither. But... if we see
+		// that the data is a commit or tag, then we can parse the PersonIdent
+		// from the data.
+		//
+		// Note: we cannot assume that absent a principal recorded in the
+		// allowedSignersFile or on a certificate that the key used to sign the
+		// commit belonged to the committer.
+		PersonIdent gitIdentity = getGitIdentity(data);
+		Date signatureDate = null;
+		Instant signatureInstant = null;
+		if (gitIdentity != null) {
+			signatureDate = gitIdentity.getWhen();
+			signatureInstant = gitIdentity.getWhenAsInstant();
+		}
+
+		TrustLevel trust = TrustLevel.NEVER;
+		byte[] decodedSignature;
+		try {
+			decodedSignature = dearmor(signatureData);
+		} catch (IllegalArgumentException e) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					null, null, false, false, trust,
+					MessageFormat.format(SshdText.get().signInvalidSignature,
+							e.getLocalizedMessage()));
+		}
+		int start = RawParseUtils.match(decodedSignature, 0,
+				SshSignatureConstants.MAGIC);
+		if (start < 0) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					null, null, false, false, trust,
+					SshdText.get().signInvalidMagic);
+		}
+		ByteArrayBuffer signature = new ByteArrayBuffer(decodedSignature, start,
+				decodedSignature.length - start);
+
+		long version = signature.getUInt();
+		if (version != SshSignatureConstants.VERSION) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					null, null, false, false, trust,
+					MessageFormat.format(SshdText.get().signInvalidVersion,
+							Long.toString(version)));
+		}
+
+		PublicKey key = signature.getPublicKey();
+		String fingerprint;
+		if (key instanceof OpenSshCertificate cert) {
+			fingerprint = KeyUtils.getFingerPrint(cert.getCertPubKey());
+			String message = SshCertificateUtils.verify(cert, signatureInstant);
+			if (message != null) {
+				return new SignatureVerification(getName(), signatureDate, null,
+						fingerprint, null, false, false, trust, message);
+			}
+		} else {
+			fingerprint = KeyUtils.getFingerPrint(key);
+		}
+
+		String namespace = signature.getString();
+		if (!SshSignatureConstants.NAMESPACE.equals(namespace)) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(SshdText.get().signInvalidNamespace,
+							namespace));
+		}
+
+		signature.getString(); // Skip the reserved field
+		String hashAlgorithm = signature.getString();
+		byte[] hash;
+		try {
+			hash = MessageDigest
+					.getInstance(hashAlgorithm.toUpperCase(Locale.ROOT))
+					.digest(data);
+		} catch (NoSuchAlgorithmException e) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(
+							SshdText.get().signUnknownHashAlgorithm,
+							hashAlgorithm));
+		}
+		ByteArrayBuffer rawSignature = new ByteArrayBuffer(
+				signature.getBytes());
+		if (signature.available() > 0) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					SshdText.get().signGarbageAtEnd);
+		}
+
+		String signatureAlgorithm = rawSignature.getString();
+		switch (signatureAlgorithm) {
+		case KeyPairProvider.SSH_DSS:
+		case KeyPairProvider.SSH_DSS_CERT:
+		case KeyPairProvider.SSH_RSA:
+		case KeyPairProvider.SSH_RSA_CERT:
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(SshdText.get().signInvalidAlgorithm,
+							signatureAlgorithm));
+		}
+
+		String keyType = KeyUtils
+				.getSignatureAlgorithm(KeyUtils.getKeyType(key), key);
+		if (!KeyUtils.getCanonicalKeyType(keyType)
+				.equals(KeyUtils.getCanonicalKeyType(signatureAlgorithm))) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(
+							SshdText.get().signMismatchedSignatureAlgorithm,
+							keyType, signatureAlgorithm));
+		}
+
+		BuiltinSignatures factory = BuiltinSignatures
+				.fromFactoryName(signatureAlgorithm);
+		if (factory == null || !factory.isSupported()) {
+			return new SignatureVerification(getName(), signatureDate, null,
+					fingerprint, null, false, false, trust,
+					MessageFormat.format(
+							SshdText.get().signUnknownSignatureAlgorithm,
+							signatureAlgorithm));
+		}
+
+		boolean valid;
+		String message = null;
+		try {
+			Signature verifier = factory.create();
+			verifier.initVerifier(null,
+					key instanceof OpenSshCertificate cert
+							? cert.getCertPubKey()
+							: key);
+			// Feed it the data
+			Buffer toSign = new ByteArrayBuffer();
+			toSign.putRawBytes(SshSignatureConstants.MAGIC);
+			toSign.putString(SshSignatureConstants.NAMESPACE);
+			toSign.putUInt(0); // reserved: zero-length string
+			toSign.putString(hashAlgorithm);
+			toSign.putBytes(hash);
+			verifier.update(null, toSign.getCompactData());
+			valid = verifier.verify(null, rawSignature.getBytes());
+		} catch (Exception e) {
+			LOG.warn("{}", SshdText.get().signLogFailure, e); //$NON-NLS-1$
+			valid = false;
+			message = SshdText.get().signSeeLog;
+		}
+		boolean expired = false;
+		String principal = null;
+		if (valid) {
+			if (rawSignature.available() > 0) {
+				valid = false;
+				message = SshdText.get().signGarbageAtEnd;
+			} else {
+				SigningKeyDatabase database = SigningKeyDatabase.getInstance();
+				if (database.isRevoked(repository, config, key)) {
+					valid = false;
+					if (key instanceof OpenSshCertificate certificate) {
+						message = MessageFormat.format(
+								SshdText.get().signCertificateRevoked,
+								KeyUtils.getFingerPrint(
+										certificate.getCaPubKey()));
+					} else {
+						message = SshdText.get().signKeyRevoked;
+					}
+				} else {
+					// This may turn a positive verification into a failed one.
+					try {
+						principal = database.isAllowed(repository, config, key,
+								SshSignatureConstants.NAMESPACE, gitIdentity);
+						if (!StringUtils.isEmptyOrNull(principal)) {
+							trust = TrustLevel.FULL;
+						} else {
+							valid = false;
+							message = SshdText.get().signNoPrincipalMatched;
+							trust = TrustLevel.UNKNOWN;
+						}
+					} catch (VerificationException e) {
+						valid = false;
+						message = e.getMessage();
+						expired = e.isExpired();
+					} catch (IOException e) {
+						LOG.warn("{}", SshdText.get().signLogFailure, e); //$NON-NLS-1$
+						valid = false;
+						message = SshdText.get().signSeeLog;
+					}
+				}
+			}
+		}
+		return new SignatureVerification(getName(), signatureDate, null,
+				fingerprint, principal, valid, expired, trust, message);
+	}
+
+	private static PersonIdent getGitIdentity(byte[] rawObject) {
+		// Data from a commit will start with "tree ID\n".
+		int i = RawParseUtils.match(rawObject, 0, TREE);
+		if (i > 0) {
+			i = RawParseUtils.committer(rawObject, 0);
+			if (i < 0) {
+				return null;
+			}
+			return RawParseUtils.parsePersonIdent(rawObject, i);
+		}
+		// Data from a tag will start with "object ID\ntype ".
+		i = RawParseUtils.match(rawObject, 0, OBJECT);
+		if (i > 0) {
+			i = RawParseUtils.nextLF(rawObject, i);
+			i = RawParseUtils.match(rawObject, i, TYPE);
+			if (i > 0) {
+				i = RawParseUtils.tagger(rawObject, 0);
+				if (i < 0) {
+					return null;
+				}
+				return RawParseUtils.parsePersonIdent(rawObject, i);
+			}
+		}
+		return null;
+	}
+
+	private static byte[] dearmor(byte[] data) {
+		int start = RawParseUtils.match(data, 0,
+				SshSignatureConstants.ARMOR_HEAD);
+		if (start > 0) {
+			if (data[start] == '\r') {
+				start++;
+			}
+			if (data[start] == '\n') {
+				start++;
+			}
+		}
+		int end = data.length;
+		if (end > start + 1 && data[end - 1] == '\n') {
+			end--;
+			if (end > start + 1 && data[end - 1] == '\r') {
+				end--;
+			}
+		}
+		end = end - SshSignatureConstants.ARMOR_END.length;
+		if (end >= 0 && end >= start
+				&& RawParseUtils.match(data, end,
+						SshSignatureConstants.ARMOR_END) >= 0) {
+			// end is fine: on the first the character of the end marker
+		} else {
+			// No end marker.
+			end = data.length;
+		}
+		if (start < 0) {
+			start = 0;
+		}
+		return Base64.decode(data, start, end - start);
+	}
+
+	@Override
+	public void clear() {
+		SigningKeyDatabase database = SigningKeyDatabase.getInstance();
+		if (database instanceof CachingSigningKeyDatabase caching) {
+			caching.clearCache();
+		}
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java
new file mode 100644
index 0000000..8cfe5f4
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/signing/ssh/SshSigner.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StreamCorruptedException;
+import java.io.StringReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.OpenSshCertificate;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.session.SessionContext;
+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.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
+import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.internal.transport.sshd.agent.SshAgentClient;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignature;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.sshd.KeyPasswordProviderFactory;
+import org.eclipse.jgit.transport.sshd.agent.Connector;
+import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory;
+import org.eclipse.jgit.util.Base64;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.StringUtils;
+import org.eclipse.jgit.util.SystemReader;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link Signer} to create SSH signatures.
+ *
+ * @see <a href=
+ *      "https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig">PROTOCOL.sshsig</a>
+ */
+public class SshSigner implements Signer {
+
+	private static final Logger LOG = LoggerFactory.getLogger(SshSigner.class);
+
+	private static final String GIT_KEY_PREFIX = "key::"; //$NON-NLS-1$
+
+	// Base64 encoded lines should not be longer than 75 characters, plus the
+	// newline.
+	private static final int LINE_LENGTH = 75;
+
+	@Override
+	public GpgSignature sign(Repository repository, GpgConfig config,
+			byte[] data, PersonIdent committer, String signingKey,
+			CredentialsProvider credentialsProvider) throws CanceledException,
+			IOException, UnsupportedSigningFormatException {
+		byte[] hash;
+		try {
+			hash = MessageDigest.getInstance("SHA512").digest(data); //$NON-NLS-1$
+		} catch (NoSuchAlgorithmException e) {
+			throw new UnsupportedSigningFormatException(
+					MessageFormat.format(
+							SshdText.get().signUnknownHashAlgorithm, "SHA512"), //$NON-NLS-1$
+					e);
+		}
+		Buffer toSign = new ByteArrayBuffer();
+		toSign.putRawBytes(SshSignatureConstants.MAGIC);
+		toSign.putString(SshSignatureConstants.NAMESPACE);
+		toSign.putUInt(0); // reserved: zero-length string
+		toSign.putString("sha512"); //$NON-NLS-1$
+		toSign.putBytes(hash);
+		String key = signingKey;
+		if (StringUtils.isEmptyOrNull(key)) {
+			key = config.getSigningKey();
+		}
+		if (StringUtils.isEmptyOrNull(key)) {
+			key = defaultKeyCommand(repository, config);
+			// According to documentation, this is supposed to return a
+			// valid SSH public key prefixed with "key::". We don't enforce
+			// this: there might be older command implementations (like just
+			// calling "ssh-add -L") that return keys without prefix.
+		}
+		PublicKeyIdentity identity;
+		try {
+			identity = getIdentity(key, committer, credentialsProvider);
+		} catch (GeneralSecurityException e) {
+			throw new UnsupportedSigningFormatException(MessageFormat
+					.format(SshdText.get().signPublicKeyError, key), e);
+		}
+		String algorithm = KeyUtils
+				.getKeyType(identity.getKeyIdentity().getPublic());
+		switch (algorithm) {
+		case KeyPairProvider.SSH_DSS:
+		case KeyPairProvider.SSH_DSS_CERT:
+			throw new UnsupportedSigningFormatException(
+					SshdText.get().signInvalidKeyDSA);
+		case KeyPairProvider.SSH_RSA:
+			algorithm = KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS;
+			break;
+		case KeyPairProvider.SSH_RSA_CERT:
+			algorithm = KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS;
+			break;
+		default:
+			break;
+		}
+
+		Map.Entry<String, byte[]> rawSignature;
+		try {
+			rawSignature = identity.sign(null, algorithm,
+					toSign.getCompactData());
+		} catch (Exception e) {
+			throw new UnsupportedSigningFormatException(
+					SshdText.get().signSignatureError, e);
+		}
+		algorithm = rawSignature.getKey();
+		Buffer signature = new ByteArrayBuffer();
+		signature.putRawBytes(SshSignatureConstants.MAGIC);
+		signature.putUInt(SshSignatureConstants.VERSION);
+		signature.putPublicKey(identity.getKeyIdentity().getPublic());
+		signature.putString(SshSignatureConstants.NAMESPACE);
+		signature.putUInt(0); // reserved: zero-length string
+		signature.putString("sha512"); //$NON-NLS-1$
+		Buffer sig = new ByteArrayBuffer();
+		sig.putString(KeyUtils.getSignatureAlgorithm(algorithm,
+				identity.getKeyIdentity().getPublic()));
+		sig.putBytes(rawSignature.getValue());
+		signature.putBytes(sig.getCompactData());
+		return armor(signature.getCompactData());
+	}
+
+	private static String defaultKeyCommand(@NonNull Repository repository,
+			@NonNull GpgConfig config) throws IOException {
+		String command = config.getSshDefaultKeyCommand();
+		if (StringUtils.isEmptyOrNull(command)) {
+			return null;
+		}
+		FS fileSystem = repository.getFS();
+		if (fileSystem == null) {
+			fileSystem = FS.DETECTED;
+		}
+		ProcessBuilder builder = fileSystem.runInShell(command,
+				new String[] {});
+		ExecutionResult result = null;
+		try {
+			result = fileSystem.execute(builder, null);
+			int exitCode = result.getRc();
+			if (exitCode == 0) {
+				// The command is supposed to return a public key in its first
+				// line on stdout.
+				try (BufferedReader r = new BufferedReader(
+						new InputStreamReader(
+								result.getStdout().openInputStream(),
+								SystemReader.getInstance()
+										.getDefaultCharset()))) {
+					String line = r.readLine();
+					if (line != null) {
+						line = line.strip();
+					}
+					if (StringUtils.isEmptyOrNull(line)) {
+						throw new IOException(MessageFormat.format(
+								SshdText.get().signDefaultKeyEmpty, command));
+					}
+					return line;
+				}
+			}
+			TemporaryBuffer stderr = result.getStderr();
+			throw new IOException(MessageFormat.format(
+					SshdText.get().signDefaultKeyFailed, command,
+					Integer.toString(exitCode), toString(stderr)));
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+			throw new IOException(
+					MessageFormat.format(
+							SshdText.get().signDefaultKeyInterrupted, command),
+					e);
+		} finally {
+			if (result != null) {
+				if (result.getStderr() != null) {
+					result.getStderr().destroy();
+				}
+				if (result.getStdout() != null) {
+					result.getStdout().destroy();
+				}
+			}
+		}
+	}
+
+	private static String toString(TemporaryBuffer b) {
+		if (b != null) {
+			try {
+				return new String(b.toByteArray(4000),
+						SystemReader.getInstance().getDefaultCharset());
+			} catch (IOException e) {
+				LOG.warn("{}", SshdText.get().signStderr, e); //$NON-NLS-1$
+			}
+		}
+		return ""; //$NON-NLS-1$
+	}
+
+	private static PublicKeyIdentity getIdentity(String signingKey,
+			PersonIdent committer, CredentialsProvider credentials)
+			throws CanceledException, GeneralSecurityException, IOException {
+		if (StringUtils.isEmptyOrNull(signingKey)) {
+			throw new IllegalArgumentException(SshdText.get().signNoSigningKey);
+		}
+		PublicKey publicKey = null;
+		PrivateKey privateKey = null;
+		File keyFile = null;
+		if (signingKey.startsWith(GIT_KEY_PREFIX)) {
+			try (StringReader r = new StringReader(
+					signingKey.substring(GIT_KEY_PREFIX.length()))) {
+				publicKey = fromEntry(
+						AuthorizedKeyEntry.readAuthorizedKeys(r, true));
+			}
+		} else if (signingKey.startsWith("~/") //$NON-NLS-1$
+				|| signingKey.startsWith('~' + File.separator)) {
+			keyFile = new File(FS.DETECTED.userHome(), signingKey.substring(2));
+		} else {
+			try (StringReader r = new StringReader(signingKey)) {
+				publicKey = fromEntry(
+						AuthorizedKeyEntry.readAuthorizedKeys(r, true));
+			} catch (IOException e) {
+				// Ignore and try to read as a file
+				keyFile = new File(signingKey);
+			}
+		}
+		if (keyFile != null && keyFile.isFile()) {
+			try {
+				publicKey = fromEntry(AuthorizedKeyEntry
+						.readAuthorizedKeys(keyFile.toPath()));
+				if (publicKey == null) {
+					throw new IOException(MessageFormat.format(
+							SshdText.get().signTooManyPublicKeys, keyFile));
+				}
+				// Try to find the private key so we don't go looking for
+				// the agent (or PKCS#11) in vain.
+				keyFile = getPrivateKeyFile(keyFile.getParentFile(),
+						keyFile.getName());
+				if (keyFile != null) {
+					try {
+						KeyPair pair = loadPrivateKey(keyFile.toPath(),
+								credentials);
+						if (pair != null) {
+							PublicKey pk = pair.getPublic();
+							if (pk == null) {
+								privateKey = pair.getPrivate();
+							} else {
+								PublicKey original = publicKey;
+								if (publicKey instanceof OpenSshCertificate cert) {
+									original = cert.getCertPubKey();
+								}
+								if (KeyUtils.compareKeys(original, pk)) {
+									privateKey = pair.getPrivate();
+								}
+							}
+						}
+					} catch (IOException e) {
+						// Apparently it wasn't a private key file. Ignore.
+					}
+				}
+			} catch (StreamCorruptedException e) {
+				// File is readable, but apparently not a public key. Try to
+				// load it as a private key.
+				KeyPair pair = loadPrivateKey(keyFile.toPath(), credentials);
+				if (pair != null) {
+					publicKey = pair.getPublic();
+					privateKey = pair.getPrivate();
+				}
+			}
+		}
+		if (publicKey == null) {
+			throw new IOException(MessageFormat
+					.format(SshdText.get().signNoPublicKey, signingKey));
+		}
+		if (publicKey instanceof OpenSshCertificate cert) {
+			String message = SshCertificateUtils.verify(cert,
+					committer.getWhenAsInstant());
+			if (message != null) {
+				throw new IOException(message);
+			}
+		}
+		if (privateKey == null) {
+			// Could be in the agent, or a PKCS#11 key. The normal procedure
+			// with PKCS#11 keys is to put them in the agent and let the agent
+			// deal with it.
+			//
+			// This may or may not work well. For instance, the agent might ask
+			// for a passphrase for PKCS#11 keys... also, the OpenSSH ssh-agent
+			// had a bug with signing using PKCS#11 certificates in the agent;
+			// see https://bugzilla.mindrot.org/show_bug.cgi?id=3613 . If there
+			// are troubles, we might do the PKCS#11 dance ourselves, but we'd
+			// need additional configuration for the PKCS#11 library. (Plus
+			// some refactoring in the Pkcs11Provider.)
+			return new AgentIdentity(publicKey);
+
+		}
+		return new KeyPairIdentity(new KeyPair(publicKey, privateKey));
+	}
+
+	private static File getPrivateKeyFile(File directory,
+			String publicKeyName) {
+		if (publicKeyName.endsWith(".pub")) { //$NON-NLS-1$
+			String privateKeyName = publicKeyName.substring(0,
+					publicKeyName.length() - 4);
+			if (!privateKeyName.isEmpty()) {
+				File keyFile = new File(directory, privateKeyName);
+				if (keyFile.isFile()) {
+					return keyFile;
+				}
+				if (privateKeyName.endsWith("-cert")) { //$NON-NLS-1$
+					privateKeyName = privateKeyName.substring(0,
+							privateKeyName.length() - 5);
+					if (!privateKeyName.isEmpty()) {
+						keyFile = new File(directory, privateKeyName);
+						if (keyFile.isFile()) {
+							return keyFile;
+						}
+					}
+				}
+			}
+		}
+		return null;
+	}
+
+	private static KeyPair loadPrivateKey(Path path,
+			CredentialsProvider credentials)
+			throws CanceledException, GeneralSecurityException, IOException {
+		if (!Files.isRegularFile(path)) {
+			return null;
+		}
+		KeyPairResourceParser parser = SecurityUtils.getKeyPairResourceParser();
+		if (parser != null) {
+			PasswordProviderWrapper provider = null;
+			if (credentials != null) {
+				provider = new PasswordProviderWrapper(
+						() -> KeyPasswordProviderFactory.getInstance()
+								.apply(credentials));
+			}
+			try {
+				Collection<KeyPair> keyPairs = parser.loadKeyPairs(null, path,
+						provider);
+				if (keyPairs.size() != 1) {
+					throw new GeneralSecurityException(MessageFormat.format(
+							SshdText.get().signTooManyPrivateKeys, path));
+				}
+				return keyPairs.iterator().next();
+			} catch (AuthenticationCanceledException e) {
+				throw new CanceledException(e.getMessage());
+			}
+		}
+		return null;
+	}
+
+	private static GpgSignature armor(byte[] data) throws IOException {
+		try (ByteArrayOutputStream b = new ByteArrayOutputStream()) {
+			b.write(SshSignatureConstants.ARMOR_HEAD);
+			b.write('\n');
+			String encoded = Base64.encodeBytes(data);
+			int length = encoded.length();
+			int column = 0;
+			for (int i = 0; i < length; i++) {
+				b.write(encoded.charAt(i));
+				column++;
+				if (column == LINE_LENGTH) {
+					b.write('\n');
+					column = 0;
+				}
+			}
+			if (column > 0) {
+				b.write('\n');
+			}
+			b.write(SshSignatureConstants.ARMOR_END);
+			b.write('\n');
+			return new GpgSignature(b.toByteArray());
+		}
+	}
+
+	private static PublicKey fromEntry(List<AuthorizedKeyEntry> entries)
+			throws GeneralSecurityException, IOException {
+		if (entries == null || entries.size() != 1) {
+			return null;
+		}
+		return entries.get(0).resolvePublicKey(null,
+				PublicKeyEntryResolver.FAILING);
+	}
+
+	@Override
+	public boolean canLocateSigningKey(Repository repository, GpgConfig config,
+			PersonIdent committer, String signingKey,
+			CredentialsProvider credentialsProvider) throws CanceledException {
+		String key = signingKey;
+		if (key == null) {
+			key = config.getSigningKey();
+		}
+		return !(StringUtils.isEmptyOrNull(key)
+				&& StringUtils.isEmptyOrNull(config.getSshDefaultKeyCommand()));
+	}
+
+	private static class KeyPairIdentity implements PublicKeyIdentity {
+
+		private final @NonNull KeyPair pair;
+
+		KeyPairIdentity(@NonNull KeyPair pair) {
+			this.pair = pair;
+		}
+
+		@Override
+		public KeyPair getKeyIdentity() {
+			return pair;
+		}
+
+		@Override
+		public Entry<String, byte[]> sign(SessionContext session, String algo,
+				byte[] data) throws Exception {
+			BuiltinSignatures factory = BuiltinSignatures.fromFactoryName(algo);
+			if (factory == null || !factory.isSupported()) {
+				throw new GeneralSecurityException(MessageFormat.format(
+						SshdText.get().signUnknownSignatureAlgorithm, algo));
+			}
+			Signature signer = factory.create();
+			signer.initSigner(null, pair.getPrivate());
+			signer.update(null, data);
+			return new SimpleImmutableEntry<>(factory.getName(),
+					signer.sign(null));
+		}
+	}
+
+	private static class AgentIdentity extends KeyPairIdentity {
+
+		AgentIdentity(PublicKey publicKey) {
+			super(new KeyPair(publicKey, null));
+		}
+
+		@Override
+		public Entry<String, byte[]> sign(SessionContext session, String algo,
+				byte[] data) throws Exception {
+			ConnectorFactory factory = ConnectorFactory.getDefault();
+			Connector connector = factory == null ? null
+					: factory.create("", null); //$NON-NLS-1$
+			if (connector == null) {
+				throw new IOException(SshdText.get().signNoAgent);
+			}
+			try (SshAgentClient agent = new SshAgentClient(connector)) {
+				return agent.sign(null, getKeyIdentity().getPublic(), algo,
+						data);
+			}
+		}
+	}
+}
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
index b0b1028..6aace47 100644
--- 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
@@ -17,6 +17,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.StreamCorruptedException;
 import java.net.URISyntaxException;
 import java.nio.file.Files;
 import java.nio.file.InvalidPathException;
@@ -355,20 +356,20 @@ private PublicKey readPublicKey(String keyFile, boolean isDerived) {
 				// only warn about non-existing files in case the key file is
 				// not derived
 				if (!isDerived) {
-					log.warn("{}", //$NON-NLS-1$
+					log.warn(LOG_FORMAT,
 						format(SshdText.get().cannotReadPublicKey, keyFile));
 				}
-			} catch (InvalidPathException | IOException e) {
-				log.warn("{}", //$NON-NLS-1$
-						format(SshdText.get().cannotReadPublicKey, keyFile), e);
-			} catch (GeneralSecurityException e) {
+			} catch (GeneralSecurityException | StreamCorruptedException e) {
 				// ignore in case this is not a derived key path, as in most
 				// cases this specifies a private key
 				if (isDerived) {
-					log.warn("{}", //$NON-NLS-1$
+					log.warn(LOG_FORMAT,
 							format(SshdText.get().cannotReadPublicKey, keyFile),
 							e);
 				}
+			} catch (InvalidPathException | IOException e) {
+				log.warn(LOG_FORMAT,
+						format(SshdText.get().cannotReadPublicKey, keyFile), e);
 			}
 			return null;
 		}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java
index 96829b7..6b2345d 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/KnownHostEntryReader.java
@@ -29,6 +29,7 @@
 import org.apache.sshd.client.config.hosts.KnownHostEntry;
 import org.apache.sshd.client.config.hosts.KnownHostHashValue;
 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -97,7 +98,7 @@ private static String clean(String line) {
 		return i < 0 ? line.trim() : line.substring(0, i).trim();
 	}
 
-	private static KnownHostEntry parseHostEntry(String line) {
+	static KnownHostEntry parseHostEntry(String line) {
 		KnownHostEntry entry = new KnownHostEntry();
 		entry.setConfigLine(line);
 		String tmp = line;
@@ -135,8 +136,8 @@ private static KnownHostEntry parseHostEntry(String line) {
 			entry.setPatterns(patterns);
 		}
 		tmp = tmp.substring(i + 1).trim();
-		AuthorizedKeyEntry key = AuthorizedKeyEntry
-				.parseAuthorizedKeyEntry(tmp);
+		AuthorizedKeyEntry key = PublicKeyEntry
+				.parsePublicKeyEntry(new AuthorizedKeyEntry(), tmp);
 		if (key == null) {
 			return null;
 		}
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 2b4f7e5..acb77c5 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2025 Thomas Wolf <twolf@apache.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,9 +31,11 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Supplier;
@@ -45,10 +47,13 @@
 import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedFactory;
+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.OpenSshCertificate;
 import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.config.keys.UnsupportedSshPublicKey;
 import org.apache.sshd.common.digest.BuiltinDigests;
 import org.apache.sshd.common.mac.Mac;
 import org.apache.sshd.common.util.io.ModifiableFileWatcher;
@@ -126,6 +131,9 @@ public class OpenSshServerKeyDatabase
 	/** Can be used to mark revoked known host lines. */
 	private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
 
+	/** Marks CA keys used for SSH certificates. */
+	private static final String MARKER_CA = "cert-authority"; //$NON-NLS-1$
+
 	private final boolean askAboutNewFile;
 
 	private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>();
@@ -178,7 +186,10 @@ public List<PublicKey> lookup(@NonNull String connectAddress,
 		for (HostKeyFile file : filesToUse) {
 			for (HostEntryPair current : file.get()) {
 				KnownHostEntry entry = current.getHostEntry();
-				if (!isRevoked(entry)) {
+				if (current.getServerKey() instanceof UnsupportedSshPublicKey) {
+					continue;
+				}
+				if (!isRevoked(entry) && !isCertificateAuthority(entry)) {
 					for (SshdSocketAddress host : candidates) {
 						if (entry.isHostMatch(host.getHostName(),
 								host.getPort())) {
@@ -204,6 +215,7 @@ public boolean accept(@NonNull String connectAddress,
 		Collection<SshdSocketAddress> candidates = getCandidates(connectAddress,
 				remoteAddress);
 		for (HostKeyFile file : filesToUse) {
+			HostEntryPair lastModified = modified[0];
 			try {
 				if (find(candidates, serverKey, file.get(), modified)) {
 					return true;
@@ -212,24 +224,35 @@ public boolean accept(@NonNull String connectAddress,
 				ask.revokedKey(remoteAddress, serverKey, file.getPath());
 				return false;
 			}
-			if (path == null && modified[0] != null) {
+			if (modified[0] != lastModified) {
 				// Remember the file in which we might need to update the
 				// entry
 				path = file.getPath();
 			}
 		}
+		if (serverKey instanceof OpenSshCertificate) {
+			return false;
+		}
 		if (modified[0] != null) {
-			// We found an entry, but with a different key
+			// We found an entry, but with a different key.
 			AskUser.ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
 					remoteAddress, modified[0].getServerKey(),
 					serverKey, path);
 			if (toDo == AskUser.ModifiedKeyHandling.ALLOW_AND_STORE) {
-				try {
-					updateModifiedServerKey(serverKey, modified[0], path);
-					knownHostsFiles.get(path).resetReloadAttributes();
-				} catch (IOException e) {
-					LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
-							path));
+				if (modified[0]
+						.getServerKey() instanceof UnsupportedSshPublicKey) {
+					// Never update a line containing an unknown key type,
+					// always add.
+					addKeyToFile(filesToUse.get(0), candidates, serverKey, ask,
+							config);
+				} else {
+					try {
+						updateModifiedServerKey(serverKey, modified[0], path);
+						knownHostsFiles.get(path).resetReloadAttributes();
+					} catch (IOException e) {
+						LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
+								path));
+					}
 				}
 			}
 			if (toDo == AskUser.ModifiedKeyHandling.DENY) {
@@ -242,19 +265,8 @@ public boolean accept(@NonNull String connectAddress,
 			return true;
 		} else if (ask.acceptUnknownKey(remoteAddress, serverKey)) {
 			if (!filesToUse.isEmpty()) {
-				HostKeyFile toUpdate = filesToUse.get(0);
-				path = toUpdate.getPath();
-				try {
-					if (Files.exists(path) || !askAboutNewFile
-							|| ask.createNewFile(path)) {
-						updateKnownHostsFile(candidates, serverKey, path,
-								config);
-						toUpdate.resetReloadAttributes();
-					}
-				} catch (Exception e) {
-					LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
-							path), e);
-				}
+				addKeyToFile(filesToUse.get(0), candidates, serverKey, ask,
+						config);
 			}
 			return true;
 		}
@@ -265,39 +277,90 @@ private static class RevokedKeyException extends Exception {
 		private static final long serialVersionUID = 1L;
 	}
 
-	private boolean isRevoked(KnownHostEntry entry) {
+	private static boolean isRevoked(KnownHostEntry entry) {
 		return MARKER_REVOKED.equals(entry.getMarker());
 	}
 
+	private static boolean isCertificateAuthority(KnownHostEntry entry) {
+		return MARKER_CA.equals(entry.getMarker());
+	}
+
 	private boolean find(Collection<SshdSocketAddress> candidates,
 			PublicKey serverKey, List<HostEntryPair> entries,
 			HostEntryPair[] modified) throws RevokedKeyException {
+		PublicKey keyToCheck = serverKey;
+		boolean isCert = false;
+		String keyType = KeyUtils.getKeyType(keyToCheck);
+		String modifiedKeyType = null;
+		if (modified[0] != null) {
+			modifiedKeyType = modified[0].getHostEntry().getKeyEntry()
+					.getKeyType();
+		}
+		if (serverKey instanceof OpenSshCertificate) {
+			keyToCheck = ((OpenSshCertificate) serverKey).getCaPubKey();
+			isCert = true;
+		}
 		for (HostEntryPair current : entries) {
 			KnownHostEntry entry = current.getHostEntry();
-			for (SshdSocketAddress host : candidates) {
-				if (entry.isHostMatch(host.getHostName(), host.getPort())) {
-					boolean revoked = isRevoked(entry);
-					if (KeyUtils.compareKeys(serverKey,
-							current.getServerKey())) {
-						// Exact match
-						if (revoked) {
-							throw new RevokedKeyException();
-						}
+			if (candidates.stream().anyMatch(host -> entry
+					.isHostMatch(host.getHostName(), host.getPort()))) {
+				boolean revoked = isRevoked(entry);
+				boolean haveCert = isCertificateAuthority(entry);
+				if (KeyUtils.compareKeys(keyToCheck, current.getServerKey())) {
+					// Exact match
+					if (revoked) {
+						throw new RevokedKeyException();
+					}
+					if (haveCert == isCert) {
 						modified[0] = null;
 						return true;
-					} else if (!revoked) {
-						// Server sent a different key
-						modified[0] = current;
-						// Keep going -- maybe there's another entry for this
-						// host
 					}
-					break;
+				}
+				if (haveCert == isCert && !haveCert && !revoked) {
+					// Server sent a different key.
+					if (modifiedKeyType == null) {
+						modified[0] = current;
+						modifiedKeyType = entry.getKeyEntry().getKeyType();
+					} else if (!keyType.equals(modifiedKeyType)) {
+						String thisKeyType = entry.getKeyEntry().getKeyType();
+						if (isBetterMatch(keyType, thisKeyType,
+								modifiedKeyType)) {
+							// Since we may replace the modified[0] key,
+							// prefer to report a key of the same key type
+							// as having been modified.
+							modified[0] = current;
+							modifiedKeyType = keyType;
+						}
+					}
+					// Keep going -- maybe there's another entry for this
+					// host
 				}
 			}
 		}
 		return false;
 	}
 
+	private static boolean isBetterMatch(String keyType, String thisType,
+			String modifiedType) {
+		if (keyType.equals(thisType)) {
+			return true;
+		}
+		// EC keys are a bit special because they encode the curve in the key
+		// type. If we have no exactly matching EC key type in known_hosts, we
+		// still prefer to update an existing EC key type over some other key
+		// type.
+		if (!keyType.startsWith("ecdsa") || !thisType.startsWith("ecdsa")) { //$NON-NLS-1$ //$NON-NLS-2$
+			return false;
+		}
+		if (!modifiedType.startsWith("ecdsa")) { //$NON-NLS-1$
+			return true;
+		}
+		// All three are EC keys. thisType doesn't match the size of keyType
+		// (otherwise the two would have compared equal above already), so it is
+		// not better than modifiedType.
+		return false;
+	}
+
 	private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
 		if (fileNames == null || fileNames.isEmpty()) {
 			return Collections.emptyList();
@@ -317,6 +380,21 @@ private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
 		return userFiles;
 	}
 
+	private void addKeyToFile(HostKeyFile file,
+			Collection<SshdSocketAddress> candidates, PublicKey serverKey,
+			AskUser ask, Configuration config) {
+		Path path = file.getPath();
+		try {
+			if (Files.exists(path) || !askAboutNewFile
+					|| ask.createNewFile(path)) {
+				updateKnownHostsFile(candidates, serverKey, path, config);
+				file.resetReloadAttributes();
+			}
+		} catch (Exception e) {
+			LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate, path), e);
+		}
+	}
+
 	private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates,
 			PublicKey serverKey, Path path, Configuration config)
 			throws Exception {
@@ -453,15 +531,22 @@ public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey,
 				return;
 			}
 			InetSocketAddress remote = (InetSocketAddress) remoteAddress;
+			boolean isCert = serverKey instanceof OpenSshCertificate;
+			PublicKey keyToReport = isCert
+					? ((OpenSshCertificate) serverKey).getCaPubKey()
+					: serverKey;
 			URIish uri = JGitUserInteraction.toURI(config.getUsername(),
 					remote);
 			String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
-					serverKey);
-			String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
-			String keyAlgorithm = serverKey.getAlgorithm();
+					keyToReport);
+			String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5,
+					keyToReport);
+			String keyAlgorithm = keyToReport.getAlgorithm();
+			String msg = isCert
+					? SshdText.get().knownHostsRevokedCertificateMsg
+					: SshdText.get().knownHostsRevokedKeyMsg;
 			askUser(provider, uri, null, //
-					format(SshdText.get().knownHostsRevokedKeyMsg,
-							remote.getHostString(), path),
+					format(msg, remote.getHostString(), path),
 					format(SshdText.get().knownHostsKeyFingerprints,
 							keyAlgorithm),
 					md5, sha256);
@@ -594,7 +679,7 @@ private List<HostEntryPair> reload(Path path) throws IOException {
 					}
 					try {
 						PublicKey serverKey = keyPart.resolvePublicKey(null,
-								PublicKeyEntryResolver.IGNORING);
+								PublicKeyEntryResolver.UNSUPPORTED);
 						if (serverKey == null) {
 							LOG.warn(format(
 									SshdText.get().knownHostsUnknownKeyType,
@@ -625,7 +710,7 @@ private int parsePort(String s) {
 
 	private SshdSocketAddress toSshdSocketAddress(@NonNull String address) {
 		String host = null;
-		int port = 0;
+		int port = SshConstants.DEFAULT_PORT;
 		if (HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == address
 				.charAt(0)) {
 			int end = address.indexOf(
@@ -665,12 +750,23 @@ private Collection<SshdSocketAddress> getCandidates(
 		if (address != null) {
 			candidates.add(address);
 		}
-		return candidates;
+		List<SshdSocketAddress> result = new ArrayList<>();
+		result.addAll(candidates);
+		if (!remoteAddress.isUnresolved()) {
+			SshdSocketAddress ip = new SshdSocketAddress(
+					remoteAddress.getAddress().getHostAddress(),
+					remoteAddress.getPort());
+			if (candidates.add(ip)) {
+				result.add(ip);
+			}
+		}
+		return result;
 	}
 
 	private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
 			PublicKey key, Configuration config) throws Exception {
 		StringBuilder result = new StringBuilder();
+		Set<String> knownNames = new HashSet<>();
 		if (config.getHashKnownHosts()) {
 			// SHA1 is the only algorithm for host name hashing known to OpenSSH
 			// or to Apache MINA sshd.
@@ -680,10 +776,10 @@ private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
 				prng = new SecureRandom();
 			}
 			byte[] salt = new byte[mac.getDefaultBlockSize()];
-			for (SshdSocketAddress address : patterns) {
-				if (result.length() > 0) {
-					result.append(',');
-				}
+			// For hashed hostnames, only one hashed pattern is allowed per
+			// https://man.openbsd.org/sshd.8#SSH_KNOWN_HOSTS_FILE_FORMAT
+			if (!patterns.isEmpty()) {
+				SshdSocketAddress address = patterns.iterator().next();
 				prng.nextBytes(salt);
 				KnownHostHashValue.append(result, digester, salt,
 						KnownHostHashValue.calculateHashValue(
@@ -692,6 +788,10 @@ private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
 			}
 		} else {
 			for (SshdSocketAddress address : patterns) {
+				String tgt = address.getHostName() + ':' + address.getPort();
+				if (!knownNames.add(tgt)) {
+					continue;
+				}
 				if (result.length() > 0) {
 					result.append(',');
 				}
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 2cd0669..900c9fb 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2024 Thomas Wolf <twolf@apache.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
@@ -47,6 +47,8 @@ private static class PerSessionState {
 
 	private final Supplier<KeyPasswordProvider> factory;
 
+	private PerSessionState noSessionState;
+
 	/**
 	 * Creates a new {@link PasswordProviderWrapper}.
 	 *
@@ -59,13 +61,18 @@ public PasswordProviderWrapper(
 	}
 
 	private PerSessionState getState(SessionContext context) {
-		PerSessionState state = context.getAttribute(STATE);
+		PerSessionState state = context != null ? context.getAttribute(STATE)
+				: noSessionState;
 		if (state == null) {
 			state = new PerSessionState();
 			state.delegate = factory.get();
 			state.delegate.setAttempts(
 					PASSWORD_PROMPTS.getRequiredDefault().intValue());
-			context.setAttribute(STATE, state);
+			if (context != null) {
+				context.setAttribute(STATE, state);
+			} else {
+				noSessionState = 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 05f04ac..e401378 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2024 Thomas Wolf <twolf@apache.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
@@ -83,6 +83,7 @@ public static SshdText get() {
 	/***/ public String knownHostsModifiedKeyDenyMsg;
 	/***/ public String knownHostsModifiedKeyStorePrompt;
 	/***/ public String knownHostsModifiedKeyWarning;
+	/***/ public String knownHostsRevokedCertificateMsg;
 	/***/ public String knownHostsRevokedKeyMsg;
 	/***/ public String knownHostsUnknownKeyMsg;
 	/***/ public String knownHostsUnknownKeyPrompt;
@@ -147,6 +148,71 @@ public static SshdText get() {
 	/***/ public String sshCommandTimeout;
 	/***/ public String sshProcessStillRunning;
 	/***/ public String sshProxySessionCloseFailed;
+	/***/ public String signAllowedSignersCertAuthorityError;
+	/***/ public String signAllowedSignersEmptyIdentity;
+	/***/ public String signAllowedSignersEmptyNamespaces;
+	/***/ public String signAllowedSignersFormatError;
+	/***/ public String signAllowedSignersInvalidDate;
+	/***/ public String signAllowedSignersLineFormat;
+	/***/ public String signAllowedSignersMultiple;
+	/***/ public String signAllowedSignersNoIdentities;
+	/***/ public String signAllowedSignersPublicKeyParsing;
+	/***/ public String signAllowedSignersUnterminatedQuote;
+	/***/ public String signCertAlgorithmMismatch;
+	/***/ public String signCertAlgorithmUnknown;
+	/***/ public String signCertificateExpired;
+	/***/ public String signCertificateInvalid;
+	/***/ public String signCertificateNotForName;
+	/***/ public String signCertificateRevoked;
+	/***/ public String signCertificateTooEarly;
+	/***/ public String signCertificateWithoutPrincipals;
+	/***/ public String signDefaultKeyEmpty;
+	/***/ public String signDefaultKeyFailed;
+	/***/ public String signDefaultKeyInterrupted;
+	/***/ public String signGarbageAtEnd;
+	/***/ public String signInvalidAlgorithm;
+	/***/ public String signInvalidKeyDSA;
+	/***/ public String signInvalidMagic;
+	/***/ public String signInvalidNamespace;
+	/***/ public String signInvalidSignature;
+	/***/ public String signInvalidVersion;
+	/***/ public String signKeyExpired;
+	/***/ public String signKeyRevoked;
+	/***/ public String signKeyTooEarly;
+	/***/ public String signKrlBlobLeftover;
+	/***/ public String signKrlBlobLengthInvalid;
+	/***/ public String signKrlBlobLengthInvalidExpected;
+	/***/ public String signKrlCaKeyLengthInvalid;
+	/***/ public String signKrlCertificateLeftover;
+	/***/ public String signKrlCertificateSubsectionLeftover;
+	/***/ public String signKrlCertificateSubsectionLength;
+	/***/ public String signKrlEmptyRange;
+	/***/ public String signKrlInvalidBitSetLength;
+	/***/ public String signKrlInvalidKeyIdLength;
+	/***/ public String signKrlInvalidMagic;
+	/***/ public String signKrlInvalidReservedLength;
+	/***/ public String signKrlInvalidVersion;
+	/***/ public String signKrlNoCertificateSubsection;
+	/***/ public String signKrlSerialZero;
+	/***/ public String signKrlShortRange;
+	/***/ public String signKrlUnknownSection;
+	/***/ public String signKrlUnknownSubsection;
+	/***/ public String signLogFailure;
+	/***/ public String signMismatchedSignatureAlgorithm;
+	/***/ public String signNoAgent;
+	/***/ public String signNoPrincipalMatched;
+	/***/ public String signNoPublicKey;
+	/***/ public String signNoSigningKey;
+	/***/ public String signNotUserCertificate;
+	/***/ public String signPublicKeyError;
+	/***/ public String signSeeLog;
+	/***/ public String signSignatureError;
+	/***/ public String signStderr;
+	/***/ public String signTooManyPrivateKeys;
+	/***/ public String signTooManyPublicKeys;
+	/***/ public String signUnknownHashAlgorithm;
+	/***/ public String signUnknownSignatureAlgorithm;
+	/***/ public String signWrongNamespace;
 	/***/ public String unknownProxyProtocol;
 
 }
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/BasicAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/BasicAuthentication.java
index 8866976..3e1fab3 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/BasicAuthentication.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/BasicAuthentication.java
@@ -17,8 +17,6 @@
 import java.net.PasswordAuthentication;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.util.Arrays;
 import java.util.concurrent.CancellationException;
 
@@ -113,13 +111,12 @@ public void process() throws Exception {
 	 */
 	protected void askCredentials() {
 		clearPassword();
-		PasswordAuthentication auth = AccessController.doPrivileged(
-				(PrivilegedAction<PasswordAuthentication>) () -> Authenticator
-						.requestPasswordAuthentication(proxy.getHostString(),
-								proxy.getAddress(), proxy.getPort(),
-								SshConstants.SSH_SCHEME,
-								SshdText.get().proxyPasswordPrompt, "Basic", //$NON-NLS-1$
-								null, RequestorType.PROXY));
+		PasswordAuthentication auth = Authenticator
+				.requestPasswordAuthentication(proxy.getHostString(),
+						proxy.getAddress(), proxy.getPort(),
+						SshConstants.SSH_SCHEME,
+						SshdText.get().proxyPasswordPrompt, "Basic", //$NON-NLS-1$
+						null, RequestorType.PROXY);
 		if (auth == null) {
 			user = ""; //$NON-NLS-1$
 			throw new CancellationException(
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/CachingSigningKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/CachingSigningKeyDatabase.java
new file mode 100644
index 0000000..4d2d8b6
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/CachingSigningKeyDatabase.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+/**
+ * A {@link SigningKeyDatabase} that caches data.
+ * <p>
+ * A signing key database may be used to check keys frequently; it may thus need
+ * to cache some data and it may need to cache data per repository. If an
+ * implementation does cache data, it is responsible itself for refreshing that
+ * cache at appropriate times. Clients can control the cache size somewhat via
+ * {@link #setCacheSize(int)}, although the meaning of the cache size (i.e., its
+ * unit) is left undefined here.
+ * </p>
+ *
+ * @since 7.1
+ */
+public interface CachingSigningKeyDatabase extends SigningKeyDatabase {
+
+	/**
+	 * Retrieves the current cache size.
+	 *
+	 * @return the cache size, or -1 if this database has no cache.
+	 */
+	int getCacheSize();
+
+	/**
+	 * Sets the cache size to use.
+	 *
+	 * @param size
+	 *            the cache size, ignored if this database does not have a
+	 *            cache.
+	 * @throws IllegalArgumentException
+	 *             if {@code size < 0}
+	 */
+	void setCacheSize(int size);
+
+	/**
+	 * Discards any cached data. A no-op if the database has no cache.
+	 */
+	void clearCache();
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SigningKeyDatabase.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SigningKeyDatabase.java
new file mode 100644
index 0000000..eec64c3
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SigningKeyDatabase.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import java.io.IOException;
+import java.security.PublicKey;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.signing.ssh.SigningDatabase;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * A database storing meta-information about signing keys and certificates.
+ *
+ * @since 7.1
+ */
+public interface SigningKeyDatabase {
+
+	/**
+	 * Obtains the current global instance.
+	 *
+	 * @return the global {@link SigningKeyDatabase}
+	 */
+	static SigningKeyDatabase getInstance() {
+		return SigningDatabase.getInstance();
+	}
+
+	/**
+	 * Sets the global {@link SigningKeyDatabase}.
+	 *
+	 * @param database
+	 *            to set; if {@code null} a default database using the OpenSSH
+	 *            allowed signers file and the OpenSSH revocation list mechanism
+	 *            is used.
+	 * @return the previously set {@link SigningKeyDatabase}
+	 */
+	static SigningKeyDatabase setInstance(SigningKeyDatabase database) {
+		return SigningDatabase.setInstance(database);
+	}
+
+	/**
+	 * Determines whether the gives key has been revoked.
+	 *
+	 * @param repository
+	 *            {@link Repository} the key is being used in
+	 * @param config
+	 *            {@link GpgConfig} to use
+	 * @param key
+	 *            {@link PublicKey} to check
+	 * @return {@code true} if the key has been revoked, {@code false} otherwise
+	 * @throws IOException
+	 *             if an I/O problem occurred
+	 */
+	boolean isRevoked(@NonNull Repository repository, @NonNull GpgConfig config,
+			@NonNull PublicKey key) throws IOException;
+
+	/**
+	 * Checks whether the given key is allowed to be used for signing, and if
+	 * allowed returns the principal.
+	 *
+	 * @param repository
+	 *            {@link Repository} the key is being used in
+	 * @param config
+	 *            {@link GpgConfig} to use
+	 * @param key
+	 *            {@link PublicKey} to check
+	 * @param namespace
+	 *            of the signature
+	 * @param ident
+	 *            optional {@link PersonIdent} giving a signer's e-mail address
+	 *            and a signature time
+	 * @return {@code null} if the database does not contain any information
+	 *         about the given key; the principal if it does and all checks
+	 *         passed
+	 * @throws IOException
+	 *             if an I/O problem occurred
+	 * @throws VerificationException
+	 *             if the database contains information about the key and the
+	 *             checks determined that the key is not allowed to be used for
+	 *             signing
+	 */
+	String isAllowed(@NonNull Repository repository, @NonNull GpgConfig config,
+			@NonNull PublicKey key, @NonNull String namespace,
+			PersonIdent ident) throws IOException, VerificationException;
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignatureVerifierFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignatureVerifierFactory.java
new file mode 100644
index 0000000..c315428
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignatureVerifierFactory.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.SignatureVerifier;
+import org.eclipse.jgit.internal.signing.ssh.SshSignatureVerifier;
+import org.eclipse.jgit.lib.SignatureVerifierFactory;
+
+/**
+ * Factory creating {@link SshSignatureVerifier}s.
+ *
+ * @since 7.1
+ */
+public final class SshSignatureVerifierFactory
+		implements SignatureVerifierFactory {
+
+	@Override
+	public GpgFormat getType() {
+		return GpgFormat.SSH;
+	}
+
+	@Override
+	public SignatureVerifier create() {
+		return new SshSignatureVerifier();
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java
new file mode 100644
index 0000000..5459b53
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/SshSignerFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.internal.signing.ssh.SshSigner;
+import org.eclipse.jgit.lib.SignerFactory;
+
+/**
+ * Factory creating {@link SshSigner}s.
+ *
+ * @since 7.1
+ */
+public final class SshSignerFactory implements SignerFactory {
+
+	@Override
+	public GpgFormat getType() {
+		return GpgFormat.SSH;
+	}
+
+	@Override
+	public Signer create() {
+		return new SshSigner();
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/VerificationException.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/VerificationException.java
new file mode 100644
index 0000000..cd77111
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/signing/ssh/VerificationException.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.signing.ssh;
+
+/**
+ * An exception giving details about a failed
+ * {@link SigningKeyDatabase#isAllowed(org.eclipse.jgit.lib.Repository, org.eclipse.jgit.lib.GpgConfig, java.security.PublicKey, String, org.eclipse.jgit.lib.PersonIdent)}
+ * validation.
+ *
+ * @since 7.1
+ */
+public class VerificationException extends Exception {
+
+	private static final long serialVersionUID = 313760495170326160L;
+
+	private final boolean expired;
+
+	private final String reason;
+
+	/**
+	 * Creates a new instance.
+	 *
+	 * @param expired
+	 *            whether the checked public key or certificate was expired
+	 * @param reason
+	 *            describing the check failure
+	 */
+	public VerificationException(boolean expired, String reason) {
+		this.expired = expired;
+		this.reason = reason;
+	}
+
+	@Override
+	public String getMessage() {
+		return reason;
+	}
+
+	/**
+	 * Tells whether the check failed because the public key was expired.
+	 *
+	 * @return {@code true} if the check failed because the public key was
+	 *         expired, {@code false} otherwise
+	 */
+	public boolean isExpired() {
+		return expired;
+	}
+
+	/**
+	 * Retrieves the check failure reason.
+	 *
+	 * @return the reason description
+	 */
+	public String getReason() {
+		return reason;
+	}
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProviderFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProviderFactory.java
new file mode 100644
index 0000000..0537300
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProviderFactory.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.transport.sshd;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.transport.CredentialsProvider;
+
+/**
+ * Maintains a static singleton instance of a factory to create a
+ * {@link KeyPasswordProvider} from a {@link CredentialsProvider}.
+ *
+ * @since 7.1
+ */
+public final class KeyPasswordProviderFactory {
+
+	/**
+	 * Creates a {@link KeyPasswordProvider} from a {@link CredentialsProvider}.
+	 */
+	@FunctionalInterface
+	public interface KeyPasswordProviderCreator
+			extends Function<CredentialsProvider, KeyPasswordProvider> {
+		// Nothing
+	}
+
+	private static final KeyPasswordProviderCreator DEFAULT = IdentityPasswordProvider::new;
+
+	private static AtomicReference<KeyPasswordProviderCreator> INSTANCE = new AtomicReference<>(
+			DEFAULT);
+
+	private KeyPasswordProviderFactory() {
+		// No instantiation
+	}
+
+	/**
+	 * Retrieves the currently set {@link KeyPasswordProviderCreator}.
+	 *
+	 * @return the {@link KeyPasswordProviderCreator}
+	 */
+	@NonNull
+	public static KeyPasswordProviderCreator getInstance() {
+		return INSTANCE.get();
+	}
+
+	/**
+	 * Sets a new {@link KeyPasswordProviderCreator}.
+	 *
+	 * @param provider
+	 *            to set; if {@code null}, sets a default provider.
+	 * @return the previously set {@link KeyPasswordProviderCreator}
+	 */
+	@NonNull
+	public static KeyPasswordProviderCreator setInstance(
+			KeyPasswordProviderCreator provider) {
+		if (provider == null) {
+			return INSTANCE.getAndSet(DEFAULT);
+		}
+		return INSTANCE.getAndSet(provider);
+	}
+}
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 2c3cbe5..4a2eb9c 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2024 Thomas Wolf <twolf@apache.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
@@ -210,7 +210,7 @@ public SshdSession getSession(URIish uri,
 						home, sshDir);
 				KeyIdentityProvider defaultKeysProvider = toKeyIdentityProvider(
 						getDefaultKeys(sshDir));
-				Supplier<KeyPasswordProvider> keyPasswordProvider = () -> createKeyPasswordProvider(
+				Supplier<KeyPasswordProvider> keyPasswordProvider = newKeyPasswordProvider(
 						credentialsProvider);
 				SshClient client = ClientBuilder.builder()
 						.factory(JGitSshClient::new)
@@ -574,12 +574,24 @@ protected final KeyCache getKeyCache() {
 	 * @param provider
 	 *            the {@link CredentialsProvider} to delegate to for user
 	 *            interactions
-	 * @return a new {@link KeyPasswordProvider}
+	 * @return a new {@link KeyPasswordProvider}, or {@code null} to use the
+	 *         global {@link KeyPasswordProviderFactory}
 	 */
-	@NonNull
 	protected KeyPasswordProvider createKeyPasswordProvider(
 			CredentialsProvider provider) {
-		return new IdentityPasswordProvider(provider);
+		return null;
+	}
+
+	private Supplier<KeyPasswordProvider> newKeyPasswordProvider(
+			CredentialsProvider credentials) {
+		return () -> {
+			KeyPasswordProvider provider = createKeyPasswordProvider(
+					credentials);
+			if (provider != null) {
+				return provider;
+			}
+			return KeyPasswordProviderFactory.getInstance().apply(credentials);
+		};
 	}
 
 	/**
diff --git a/org.eclipse.jgit.ssh.apache/src/sun/security/x509/README.md b/org.eclipse.jgit.ssh.apache/src/sun/security/x509/README.md
deleted file mode 100644
index a84ee37..0000000
--- a/org.eclipse.jgit.ssh.apache/src/sun/security/x509/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-This dummy package is used to fix the error
-"Missing requirement: net.i2p.crypto.eddsa 0.3.0 requires 'java.package; sun.security.x509 0.0.0'"
-raised since eddsa falsely requires this import
\ No newline at end of file
diff --git a/org.eclipse.jgit.ssh.jsch.test/BUILD b/org.eclipse.jgit.ssh.jsch.test/BUILD
index 4a8b925..d4e6875 100644
--- a/org.eclipse.jgit.ssh.jsch.test/BUILD
+++ b/org.eclipse.jgit.ssh.jsch.test/BUILD
@@ -8,7 +8,9 @@
     srcs = glob(["tst/**/*.java"]),
     tags = ["jsch"],
     deps = [
-        "//lib:eddsa",
+        "//lib:bcpkix",
+        "//lib:bcprov",
+        "//lib:bcutil",
         "//lib:jsch",
         "//lib:junit",
         "//org.eclipse.jgit:jgit",
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 3bf4edd..34f8e0a 100644
--- a/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch.test/META-INF/MANIFEST.MF
@@ -3,19 +3,20 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.jsch.test
 Bundle-SymbolicName: org.eclipse.jgit.ssh.jsch.test
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Require-Bundle: org.hamcrest.core;bundle-version="[1.3.0,2.0.0)"
 Import-Package: com.jcraft.jsch;version="[0.1.54,0.2.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit.ssh;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.ssh.jsch;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit.ssh;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.ssh.jsch;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.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.jsch.test/pom.xml b/org.eclipse.jgit.ssh.jsch.test/pom.xml
index 761b6c4..5f094a7 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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/ssh/jsch/JSchSshProtocol2Test.java b/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/ssh/jsch/JSchSshProtocol2Test.java
index 611d4e8..8aa33e3 100644
--- a/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/ssh/jsch/JSchSshProtocol2Test.java
+++ b/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/ssh/jsch/JSchSshProtocol2Test.java
@@ -22,7 +22,6 @@
 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.CredentialsProvider;
 import org.eclipse.jgit.transport.RemoteSession;
@@ -89,7 +88,8 @@ private OpenSshConfig createConfig(String... content) throws IOException {
 	@Override
 	public void setUp() throws Exception {
 		super.setUp();
-		StoredConfig config = ((Repository) db).getConfig();
+		@SuppressWarnings("restriction")
+		StoredConfig config = db.getConfig();
 		config.setInt("protocol", null, "version", 2);
 		config.save();
 	}
diff --git a/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
index 9da7452..6b0130c 100644
--- a/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.jsch/META-INF/MANIFEST.MF
@@ -3,19 +3,19 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit.ssh.jsch
 Bundle-SymbolicName: org.eclipse.jgit.ssh.jsch;singleton:=true
-Fragment-Host: org.eclipse.jgit;bundle-version="[7.0.0,7.1.0)"
+Fragment-Host: org.eclipse.jgit;bundle-version="[7.3.0,7.4.0)"
 Bundle-Vendor: %Bundle-Vendor
 Bundle-Localization: OSGI-INF/l10n/jsch
 Bundle-ActivationPolicy: lazy
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Export-Package: org.eclipse.jgit.transport.ssh.jsch;version="7.0.0"
+Export-Package: org.eclipse.jgit.transport.ssh.jsch;version="7.3.0"
 Import-Package: com.jcraft.jsch;version="[0.1.37,0.2.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util.io;version="[7.3.0,7.4.0)",
  org.slf4j;version="[1.7.0,3.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 1dc0e01..06d0ce3 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ssh.jsch;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ssh.jsch;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.ssh.jsch/pom.xml b/org.eclipse.jgit.ssh.jsch/pom.xml
index a3db63c..03ae29d 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
diff --git a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschSession.java b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschSession.java
index 5f36dad..ad58ae1 100644
--- a/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschSession.java
+++ b/org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/ssh/jsch/JschSession.java
@@ -34,7 +34,6 @@
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.util.io.IsolatedOutputStream;
 
-import com.jcraft.jsch.Channel;
 import com.jcraft.jsch.ChannelExec;
 import com.jcraft.jsch.ChannelSftp;
 import com.jcraft.jsch.JSchException;
@@ -86,22 +85,6 @@ public void disconnect() {
 	}
 
 	/**
-	 * A kludge to allow {@link org.eclipse.jgit.transport.TransportSftp} to get
-	 * an Sftp channel from Jsch. Ideally, this method would be generic, which
-	 * would require implementing generic Sftp channel operations in the
-	 * RemoteSession class.
-	 *
-	 * @return a channel suitable for Sftp operations.
-	 * @throws com.jcraft.jsch.JSchException
-	 *             on problems getting the channel.
-	 * @deprecated since 5.2; use {@link #getFtpChannel()} instead
-	 */
-	@Deprecated
-	public Channel getSftpChannel() throws JSchException {
-		return sock.openChannel("sftp"); //$NON-NLS-1$
-	}
-
-	/**
 	 * {@inheritDoc}
 	 *
 	 * @since 5.2
diff --git a/org.eclipse.jgit.test/BUILD b/org.eclipse.jgit.test/BUILD
index 29f5b36..7755df0 100644
--- a/org.eclipse.jgit.test/BUILD
+++ b/org.eclipse.jgit.test/BUILD
@@ -53,6 +53,11 @@
     exclude = HELPERS + DATA + EXCLUDED,
 ))
 
+
+tests(tests = glob(["exttst/**/*.java"]),
+    srcprefix = "exttst/",
+    extra_tags = ["ext"])
+
 # Non abstract base classes used for tests by other test classes
 BASE = [
     PKG + "internal/storage/file/FileRepositoryBuilderTest.java",
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index ab70d68..7ac93c2 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: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-RequiredExecutionEnvironment: JavaSE-17
@@ -20,65 +20,68 @@
  org.apache.commons.compress.compressors.xz;version="[1.15.0,2.0)",
  org.apache.commons.io;version="[2.15.0,3.0.0)",
  org.apache.commons.io.output;version="[2.15.0,3.0.0)",
+ org.apache.commons.lang3;version="[3.17.0,4.0.0)",
  org.assertj.core.api;version="[3.14.0,4.0.0)",
- org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.archive;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.attributes;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.awtui;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.blame;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.diff;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.dircache;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.events;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.fnmatch;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.gitrepo;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.hooks;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.ignore;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.ignore.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.diff;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.diffmergetool;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.fsck;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.commitgraph;version="7.0.0",
- org.eclipse.jgit.internal.storage.dfs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.io;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.memory;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.pack;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.storage.reftable;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.transport.connectivity;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.transport.http;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.transport.parser;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.internal.transport.ssh;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.junit.time;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lfs;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.logging;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.merge;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.notes;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.patch;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.pgm;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.pgm.internal;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revplot;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk.filter;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.file;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.storage.pack;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.submodule;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.http;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport.resolver;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util.io;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util.sha1;version="[7.0.0,7.1.0)",
+ org.eclipse.jgit.annotations;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.api.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.archive;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.attributes;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.awtui;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.blame;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.blame.cache;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.diff;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.dircache;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.events;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.fnmatch;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.gitrepo;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.hooks;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.ignore;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.ignore.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.diff;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.diffmergetool;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.fsck;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.commitgraph;version="7.3.0",
+ org.eclipse.jgit.internal.storage.dfs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.io;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.memory;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.midx;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.storage.reftable;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.transport.connectivity;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.transport.http;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.transport.parser;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.internal.transport.ssh;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.junit.time;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lfs;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.logging;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.merge;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.notes;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.patch;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.pgm;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.pgm.internal;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revplot;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk.filter;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.file;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.storage.pack;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.submodule;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.http;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport.resolver;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.treewalk.filter;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util.io;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util.sha1;version="[7.3.0,7.4.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/exttst/org/eclipse/jgit/internal/storage/midx/CgitMidxCompatibilityTest.java b/org.eclipse.jgit.test/exttst/org/eclipse/jgit/internal/storage/midx/CgitMidxCompatibilityTest.java
new file mode 100644
index 0000000..88f0806
--- /dev/null
+++ b/org.eclipse.jgit.test/exttst/org/eclipse/jgit/internal/storage/midx/CgitMidxCompatibilityTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.midx;
+
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.file.Pack;
+import org.eclipse.jgit.internal.storage.file.PackFile;
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
+import org.eclipse.jgit.util.NB;
+import org.junit.Test;
+
+public class CgitMidxCompatibilityTest extends SampleDataRepositoryTestCase {
+
+	@Test
+	public void jgitMidx_verifyByCgit()
+			throws IOException, InterruptedException {
+		byte[] jgitMidxBytes = generateJGitMidx();
+		writeMidx(jgitMidxBytes);
+		assertEquals("cgit exit code", 0, run_cgit_multipackindex_verify());
+	}
+
+	@Test
+	public void compareBasicChunkSizes()
+			throws IOException, InterruptedException {
+		// We cannot compare byte-by-byte because there are optional chunks and
+		// it is not guaranteed what cgit and jgit will generate
+		byte[] jgitMidxBytes = generateJGitMidx();
+		assertEquals("cgit exit code", 0, run_cgit_multipackindex_write());
+		byte[] cgitMidxBytes = readCgitMidx();
+
+		RawMultiPackIndex jgitMidx = new RawMultiPackIndex(jgitMidxBytes);
+		RawMultiPackIndex cgitMidx = new RawMultiPackIndex(cgitMidxBytes);
+
+		// This is a fixed sized chunk
+		assertEquals(256 * 4, cgitMidx.getChunkSize(MIDX_CHUNKID_OIDFANOUT));
+		assertArrayEquals(cgitMidx.getRawChunk(MIDX_CHUNKID_OIDFANOUT),
+				jgitMidx.getRawChunk(MIDX_CHUNKID_OIDFANOUT));
+
+		assertArrayEquals(cgitMidx.getRawChunk(MIDX_CHUNKID_OIDLOOKUP),
+				jgitMidx.getRawChunk(MIDX_CHUNKID_OIDLOOKUP));
+
+		// The spec has changed from padding packnames to a multile of four, to
+		// move the packname chunk to the end of the file.
+		// git 2.48 pads the packs names to a multiple of 4
+		// jgit puts the chunk at the end
+		byte[] cgitPacknames = trimPadding(
+				cgitMidx.getRawChunk(MIDX_CHUNKID_PACKNAMES));
+		assertArrayEquals(cgitPacknames,
+				jgitMidx.getRawChunk(MIDX_CHUNKID_PACKNAMES));
+
+		assertArrayEquals(cgitMidx.getRawChunk(MIDX_CHUNKID_OBJECTOFFSETS),
+				jgitMidx.getRawChunk(MIDX_CHUNKID_OBJECTOFFSETS));
+
+	}
+
+	private byte[] generateJGitMidx() throws IOException {
+		Map<String, PackIndex> indexes = new HashMap<>();
+		for (Pack pack : db.getObjectDatabase().getPacks()) {
+			PackFile packFile = pack.getPackFile().create(PackExt.INDEX);
+			indexes.put(packFile.getName(), pack.getIndex());
+		}
+
+		MultiPackIndexWriter writer = new MultiPackIndexWriter();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		writer.write(NullProgressMonitor.INSTANCE, out, indexes);
+		return out.toByteArray();
+	}
+
+	private int run_cgit_multipackindex_write()
+			throws IOException, InterruptedException {
+		String[] command = new String[] { "git", "multi-pack-index", "write" };
+		Process proc = Runtime.getRuntime().exec(command, new String[0],
+				db.getDirectory());
+		return proc.waitFor();
+	}
+
+	private int run_cgit_multipackindex_verify()
+			throws IOException, InterruptedException {
+		String[] command = new String[] { "git", "multi-pack-index", "verify" };
+		Process proc = Runtime.getRuntime().exec(command, new String[0],
+				db.getDirectory());
+		return proc.waitFor();
+	}
+
+	private byte[] readCgitMidx() throws IOException {
+		File midx = getMIdxStandardLocation();
+		assertTrue("cgit multi-pack-index exists", midx.exists());
+		return Files.readAllBytes(midx.toPath());
+	}
+
+	private void writeMidx(byte[] midx) throws IOException {
+		File midxFile = getMIdxStandardLocation();
+		Files.write(midxFile.toPath(), midx);
+	}
+
+	private File getMIdxStandardLocation() {
+		return new File(db.getObjectDatabase().getPackDirectory(),
+				"multi-pack-index");
+	}
+
+	private byte[] trimPadding(byte[] data) {
+		// Chunk MUST have one \0, we want to remove any extra \0
+		int newEnd = data.length - 1;
+		while (newEnd - 1 >= 0 && data[newEnd - 1] == 0) {
+			newEnd--;
+		}
+
+		if (newEnd == data.length - 1) {
+			return data;
+		}
+		return Arrays.copyOfRange(data, 0, newEnd + 1);
+	}
+
+	private static class RawMultiPackIndex {
+		private final List<ChunkSegment> chunks;
+
+		private final byte[] midx;
+
+		private RawMultiPackIndex(byte[] midx) {
+			this.chunks = readChunks(midx);
+			this.midx = midx;
+		}
+
+		long getChunkSize(int chunkId) {
+			int chunkPos = findChunkPosition(chunks, chunkId);
+			return chunks.get(chunkPos + 1).offset
+					- chunks.get(chunkPos).offset;
+		}
+
+		long getOffset(int chunkId) {
+			return chunks.get(findChunkPosition(chunks, chunkId)).offset;
+		}
+
+		private long getNextOffset(int chunkId) {
+			return chunks.get(findChunkPosition(chunks, chunkId) + 1).offset;
+		}
+
+		byte[] getRawChunk(int chunkId) {
+			int start = (int) getOffset(chunkId);
+			int end = (int) getNextOffset(chunkId);
+			return Arrays.copyOfRange(midx, start, end);
+		}
+
+		private static int findChunkPosition(List<ChunkSegment> chunks,
+				int id) {
+			int chunkPos = -1;
+			for (int i = 0; i < chunks.size(); i++) {
+				if (chunks.get(i).id() == id) {
+					chunkPos = i;
+					break;
+				}
+			}
+			if (chunkPos == -1) {
+				throw new IllegalStateException("Chunk doesn't exist");
+			}
+			return chunkPos;
+		}
+
+		private List<ChunkSegment> readChunks(byte[] midx) {
+			// Read the number of "chunkOffsets" (1 byte)
+			int chunkCount = midx[6];
+			byte[] lookupBuffer = new byte[CHUNK_LOOKUP_WIDTH
+					* (chunkCount + 1)];
+			System.arraycopy(midx, 12, lookupBuffer, 0, lookupBuffer.length);
+
+			List<ChunkSegment> chunks = new ArrayList<>(chunkCount + 1);
+			for (int i = 0; i <= chunkCount; i++) {
+				// chunks[chunkCount] is just a marker, in order to record the
+				// length of the last chunk.
+				int id = NB.decodeInt32(lookupBuffer, i * 12);
+				long offset = NB.decodeInt64(lookupBuffer, i * 12 + 4);
+				chunks.add(new ChunkSegment(id, offset));
+			}
+			return chunks;
+		}
+	}
+
+	private record ChunkSegment(int id, long offset) {
+	}
+}
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index 05f7c08..9cf21fd 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.test</artifactId>
diff --git a/org.eclipse.jgit.test/tests.bzl b/org.eclipse.jgit.test/tests.bzl
index 170bf0c..41f76d0 100644
--- a/org.eclipse.jgit.test/tests.bzl
+++ b/org.eclipse.jgit.test/tests.bzl
@@ -1,11 +1,29 @@
+'''
+Expose each test as a bazel target
+'''
 load(
     "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
     "junit_tests",
 )
 
-def tests(tests):
+def tests(tests, srcprefix="tst/", extra_tags=[]):
+    '''
+    Create a target each of the tests
+
+    Each target is the full push (removing srcprefix) replacing directory
+    separators with underscores.
+
+    e.g. a test under tst/a/b/c/A.test will become the target
+    //org.eclipse.jgit.tests:a_b_c_A
+
+    Args:
+      tests: a glob of tests files
+      srcprefix: prefix between org.eclipse.jgit.tests and the package
+        start
+      extra_tags: additional tags to add to the generated targets
+    '''
     for src in tests:
-        name = src[len("tst/"):len(src) - len(".java")].replace("/", "_")
+        name = src[len(srcprefix):len(src) - len(".java")].replace("/", "_")
         labels = []
         timeout = "moderate"
         if name.startswith("org_eclipse_jgit_"):
@@ -20,6 +38,8 @@
         if "lib" not in labels:
             labels.append("lib")
 
+        labels.extend(extra_tags)
+
         # TODO(http://eclip.se/534285): Make this test pass reliably
         # and remove the flaky attribute.
         flaky = src.endswith("CrissCrossMergeTest.java")
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ConflictOutOfBounds.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ConflictOutOfBounds.patch
new file mode 100644
index 0000000..6e7448b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ConflictOutOfBounds.patch
@@ -0,0 +1,10 @@
+diff --git a/ConflictOutOfBounds b/ConflictOutOfBounds
+index 0000000..de98044
+--- a/ConflictOutOfBounds
++++ b/ConflictOutOfBounds
+@@ -25,4 +25,4 @@
+ line3
+-lineA
++lineB
+ line5
+ line6
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ConflictOutOfBounds_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ConflictOutOfBounds_PostImage
new file mode 100644
index 0000000..4e5d5b2
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ConflictOutOfBounds_PostImage
@@ -0,0 +1,15 @@
+line1
+line2
+line3
+line4
+line5
+line6
+line7
+line8
+<<<<<<< HEAD
+=======
+line3
+lineB
+line5
+line6
+>>>>>>> PATCH
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ConflictOutOfBounds_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ConflictOutOfBounds_PreImage
new file mode 100644
index 0000000..f62562a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ConflictOutOfBounds_PreImage
@@ -0,0 +1,8 @@
+line1
+line2
+line3
+line4
+line5
+line6
+line7
+line8
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict.patch
new file mode 100644
index 0000000..a99e636
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict.patch
@@ -0,0 +1,10 @@
+diff --git a/allowconflict b/allowconflict
+index 0000000..de98044
+--- a/allowconflict
++++ b/allowconflict
+@@ -3,4 +3,4 @@
+ line3
+-lineA
++lineB
+ line5
+ line6
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict_PostImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict_PostImage
new file mode 100644
index 0000000..a963b40
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict_PostImage
@@ -0,0 +1,15 @@
+line1
+line2
+<<<<<<< HEAD
+line3
+line4
+line5
+line6
+=======
+line3
+lineB
+line5
+line6
+>>>>>>> PATCH
+line7
+line8
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict_PreImage b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict_PreImage
new file mode 100644
index 0000000..f62562a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict_PreImage
@@ -0,0 +1,8 @@
+line1
+line2
+line3
+line4
+line5
+line6
+line7
+line8
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict_file_deleted.patch b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict_file_deleted.patch
new file mode 100644
index 0000000..c9655a5
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/allowconflict_file_deleted.patch
@@ -0,0 +1,10 @@
+diff --git a/allowconflict_file_deleted b/allowconflict_file_deleted
+index 0000000..de98044
+--- a/allowconflict_file_deleted
++++ b/allowconflict_file_deleted
+@@ -3,4 +3,4 @@
+ line3
+-lineA
++lineB
+ line5
+ line6
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.c b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.c
new file mode 100644
index 0000000..3661160
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.c
@@ -0,0 +1,43 @@
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+
+void getGreeting(char *result, const char *name) {
+    sprintf(result, "Hello, %s!", name);
+}
+
+void getFarewell(char *result, const char *name) {
+    sprintf(result, "Goodbye, %s. Have a great day!", name);
+}
+
+void toLower(char *str) {
+    for (int i = 0; str[i]; i++) {
+        str[i] = tolower(str[i]);
+    }
+}
+
+void getPersonalizedGreeting(char *result, const char *name, const char *timeOfDay) {
+    char timeOfDayLower[50];
+    strcpy(timeOfDayLower, timeOfDay);
+    toLower(timeOfDayLower);
+    if (strcmp(timeOfDayLower, "morning") == 0) {
+        sprintf(result, "Good morning, %s", name);
+    } else if (strcmp(timeOfDayLower, "afternoon") == 0) {
+        sprintf(result, "Good afternoon, %s", name);
+    } else if (strcmp(timeOfDayLower, "evening") == 0) {
+        sprintf(result, "Good evening, %s", name);
+    } else {
+        sprintf(result, "Good day, %s", name);
+    }
+}
+
+int main() {
+    char result[100];
+    getGreeting(result, "foo");
+    printf("%s\\n", result);
+    getFarewell(result, "bar");
+    printf("%s\\n", result);
+    getPersonalizedGreeting(result, "baz", "morning");
+    printf("%s\\n", result);
+    return 0;
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.javasource b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.javasource
new file mode 100644
index 0000000..9659685
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.javasource
@@ -0,0 +1,37 @@
+public class Greeting {
+    public String getGreeting(String name) {
+        String msg = "Hello, " + name + "!";
+        return msg;
+    }
+
+    public String getFarewell(String name) {
+        String msg = "Goodbye, " + name + ". Have a great day!";
+        return msg;
+    }
+
+    public String getPersonalizedGreeting(String name, String timeOfDay) {
+        String msg;
+        switch (timeOfDay.toLowerCase()) {
+        case "morning":
+            msg = "Good morning, " + name;
+            break;
+        case "afternoon":
+            msg = "Good afternoon, " + name;
+            break;
+        case "evening":
+            msg = "Good evening, " + name;
+            break;
+        default:
+            msg = "Good day, " + name;
+            break;
+        }
+        return msg;
+    }
+
+    public static void main(String[] args) {
+        Greeting greeting = new Greeting();
+        System.out.println(greeting.getGreeting("foo"));
+        System.out.println(greeting.getFarewell("bar"));
+        System.out.println(greeting.getPersonalizedGreeting("baz", "morning"));
+    }
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.py b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.py
new file mode 100644
index 0000000..9eda6cd
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.py
@@ -0,0 +1,26 @@
+class Greeting:
+    def get_greeting(self, name):
+        greeting_message = f"Hello, {name}!"
+        return greeting_message
+
+    def get_farewell(self, name):
+        farewell_message = f"Goodbye, {name}. Have a great day!"
+        return farewell_message
+
+    def get_personalized_greeting(self, name, time_of_day):
+        time_of_day = time_of_day.lower()
+        if time_of_day == "morning":
+            personalized_message = f"Good morning, {name}"
+        elif time_of_day == "afternoon":
+            personalized_message = f"Good afternoon, {name}"
+        elif time_of_day == "evening":
+            personalized_message = f"Good evening, {name}"
+        else:
+            personalized_message = f"Good day, {name}"
+        return personalized_message
+
+if __name__ == "__main__":
+    greeting = Greeting()
+    print(greeting.get_greeting("foo"))
+    print(greeting.get_farewell("bar"))
+    print(greeting.get_personalized_greeting("baz", "morning"))
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.rs b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.rs
new file mode 100644
index 0000000..a3aa5cb
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/greeting.rs
@@ -0,0 +1,27 @@
+struct Greeting;
+
+impl Greeting {
+    fn get_greeting(&self, name: &str) -> String {
+        format!("Hello, {}!", name)
+    }
+
+    fn get_farewell(&self, name: &str) -> String {
+        format!("Goodbye, {}. Have a great day!", name)
+    }
+
+    fn get_personalized_greeting(&self, name: &str, time_of_day: &str) -> String {
+        match time_of_day.to_lowercase().as_str() {
+            "morning" => format!("Good morning, {}", name),
+            "afternoon" => format!("Good afternoon, {}", name),
+            "evening" => format!("Good evening, {}", name),
+            _ => format!("Good day, {}", name),
+        }
+    }
+}
+
+fn main() {
+    let greeting = Greeting;
+    println!("{}", greeting.get_greeting("foo"));
+    println!("{}", greeting.get_farewell("bar"));
+    println!("{}", greeting.get_personalized_greeting("baz", "morning"));
+}
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/sample.dtsi b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/sample.dtsi
new file mode 100644
index 0000000..6aa4ecd
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/sample.dtsi
@@ -0,0 +1,25 @@
+/dts-v1/;
+
+/ {
+	model = "Example Board";
+	compatible = "example,board";
+	cpus {
+		cpu@0 {
+			device_type = "cpu";
+			compatible = "arm,cortex-a9";
+			reg = <0>;
+		};
+	};
+
+	memory {
+		device_type = "memory";
+		reg = <0x80000000 0x20000000>;
+	};
+
+	uart0: uart@101f1000 {
+		compatible = "ns16550a";
+		reg = <0x101f1000 0x1000>;
+		interrupts = <5>;
+		clock-frequency = <24000000>;
+	};
+};
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
index 1c2e995..2266772 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com>
- * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2010, 2025 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
@@ -665,11 +665,13 @@ public void testAddRemovedFile() throws Exception {
 			FileUtils.delete(file);
 
 			// is supposed to do nothing
-			dc = git.add().addFilepattern("a.txt").call();
+			dc = git.add().addFilepattern("a.txt").setAll(false).call();
 			assertEquals(oid, dc.getEntry(0).getObjectId());
 			assertEquals(
 					"[a.txt, mode:100644, content:content]",
 					indexState(CONTENT));
+			git.add().addFilepattern("a.txt").call();
+			assertEquals("", indexState(CONTENT));
 		}
 	}
 
@@ -690,11 +692,13 @@ public void testAddRemovedCommittedFile() throws Exception {
 			FileUtils.delete(file);
 
 			// is supposed to do nothing
-			dc = git.add().addFilepattern("a.txt").call();
+			dc = git.add().addFilepattern("a.txt").setAll(false).call();
 			assertEquals(oid, dc.getEntry(0).getObjectId());
 			assertEquals(
 					"[a.txt, mode:100644, content:content]",
 					indexState(CONTENT));
+			git.add().addFilepattern("a.txt").call();
+			assertEquals("", indexState(CONTENT));
 		}
 	}
 
@@ -964,7 +968,7 @@ public void testAddWithoutParameterUpdate() throws Exception {
 			// file sub/b.txt is deleted
 			FileUtils.delete(file2);
 
-			git.add().addFilepattern("sub").call();
+			git.add().addFilepattern("sub").setAll(false).call();
 			// change in sub/a.txt is staged
 			// deletion of sub/b.txt is not staged
 			// sub/c.txt is staged
@@ -973,6 +977,12 @@ public void testAddWithoutParameterUpdate() throws Exception {
 					"[sub/b.txt, mode:100644, content:content b]" +
 					"[sub/c.txt, mode:100644, content:content c]",
 					indexState(CONTENT));
+			git.add().addFilepattern("sub").call();
+			// deletion of sub/b.txt is staged
+			assertEquals(
+					"[sub/a.txt, mode:100644, content:modified content]"
+							+ "[sub/c.txt, mode:100644, content:content c]",
+					indexState(CONTENT));
 		}
 	}
 
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 3a4ea8e..9c2b16a 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
@@ -267,7 +267,10 @@ private void archiveHeadAllFilesWithCompression(String fmt) throws Exception {
 			archive(git, archive, fmt, Map.of("compression-level", 9));
 			int sizeCompression9 = getNumBytes(archive);
 
-			assertTrue(sizeCompression1 > sizeCompression9);
+			assertTrue(
+					"Expected sizeCompression1 = " + sizeCompression1
+							+ " > sizeCompression9 = " + sizeCompression9,
+					sizeCompression1 > sizeCompression9);
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
index be3b33a..3f5c5da 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
@@ -34,6 +34,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.merge.ContentMergeStrategy;
@@ -529,10 +530,11 @@ private void doCherryPickAndCheckResult(final Git git,
 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
 
 		if (reason == null) {
-			ReflogReader reader = db.getReflogReader(Constants.HEAD);
+			RefDatabase refDb = db.getRefDatabase();
+			ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("cherry-pick: "));
-			reader = db.getReflogReader(db.getBranch());
+			reader = refDb.getReflogReader(db.getFullBranch());
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("cherry-pick: "));
 		}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
index 63ab809..661878f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
@@ -182,7 +182,8 @@ private static boolean isLocalHead(Ref ref) {
 
 	private static boolean hasRefLog(Repository repo, Ref ref) {
 		try {
-			return repo.getReflogReader(ref.getName()).getLastEntry() != null;
+			return repo.getRefDatabase().getReflogReader(ref)
+					.getLastEntry() != null;
 		} catch (IOException ioe) {
 			throw new IllegalStateException(ioe);
 		}
@@ -647,7 +648,8 @@ public void testCloneRepositoryWithSubmodules() throws Exception {
 					new File(git.getRepository().getWorkTree(), walk.getPath()),
 					subRepo.getWorkTree());
 			assertEquals(new File(new File(git.getRepository().getDirectory(),
-					"modules"), walk.getPath()), subRepo.getDirectory());
+					"modules"), walk.getPath()).getCanonicalPath(),
+					subRepo.getDirectory().getCanonicalPath());
 		}
 
 		File directory = createTempDirectory("testCloneRepositoryWithSubmodules");
@@ -681,8 +683,8 @@ public void testCloneRepositoryWithSubmodules() throws Exception {
 					walk.getPath()), clonedSub1.getWorkTree());
 			assertEquals(
 					new File(new File(git2.getRepository().getDirectory(),
-							"modules"), walk.getPath()),
-					clonedSub1.getDirectory());
+							"modules"), walk.getPath()).getCanonicalPath(),
+					clonedSub1.getDirectory().getCanonicalPath());
 		}
 	}
 
@@ -770,8 +772,8 @@ public void testCloneRepositoryWithNestedSubmodules() throws Exception {
 						walk.getPath()), clonedSub1.getWorkTree());
 				assertEquals(
 						new File(new File(git2.getRepository().getDirectory(),
-								"modules"), walk.getPath()),
-						clonedSub1.getDirectory());
+								"modules"), walk.getPath()).getCanonicalPath(),
+						clonedSub1.getDirectory().getCanonicalPath());
 				status = new SubmoduleStatusCommand(clonedSub1);
 				statuses = status.call();
 			}
@@ -795,7 +797,7 @@ public void testCloneWithAutoSetupRebase() throws Exception {
 		assertNull(git2.getRepository().getConfig().getEnum(
 				BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, "test",
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 
 		StoredConfig userConfig = SystemReader.getInstance()
 				.getUserConfig();
@@ -811,7 +813,6 @@ public void testCloneWithAutoSetupRebase() throws Exception {
 		addRepoToClose(git2.getRepository());
 		assertEquals(BranchRebaseMode.REBASE,
 				git2.getRepository().getConfig().getEnum(
-						BranchRebaseMode.values(),
 						ConfigConstants.CONFIG_BRANCH_SECTION, "test",
 						ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
@@ -828,7 +829,6 @@ public void testCloneWithAutoSetupRebase() throws Exception {
 		addRepoToClose(git2.getRepository());
 		assertEquals(BranchRebaseMode.REBASE,
 				git2.getRepository().getConfig().getEnum(
-						BranchRebaseMode.values(),
 						ConfigConstants.CONFIG_BRANCH_SECTION, "test",
 						ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java
index 57e5d49..4e5f44e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java
@@ -26,6 +26,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -69,10 +70,11 @@ public void testSomeCommits() throws Exception {
 				l--;
 			}
 			assertEquals(l, -1);
-			ReflogReader reader = db.getReflogReader(Constants.HEAD);
+			RefDatabase refDb = db.getRefDatabase();
+			ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
 			assertTrue(
 					reader.getLastEntry().getComment().startsWith("commit:"));
-			reader = db.getReflogReader(db.getBranch());
+			reader = refDb.getReflogReader(db.getFullBranch());
 			assertTrue(
 					reader.getLastEntry().getComment().startsWith("commit:"));
 		}
@@ -248,10 +250,11 @@ public void testCommitAmend() throws Exception {
 				c++;
 			}
 			assertEquals(1, c);
-			ReflogReader reader = db.getReflogReader(Constants.HEAD);
+			RefDatabase refDb = db.getRefDatabase();
+			ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("commit (amend):"));
-			reader = db.getReflogReader(db.getBranch());
+			reader = refDb.getReflogReader(db.getFullBranch());
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("commit (amend):"));
 		}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
index 35de73e..21cfcc4 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
@@ -19,14 +19,15 @@
 import static org.junit.Assume.assumeTrue;
 
 import java.io.File;
-import java.util.Date;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.List;
-import java.util.TimeZone;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus;
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.api.errors.EmptyCommitException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.dircache.DirCache;
@@ -34,19 +35,23 @@
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.junit.time.TimeUtil;
-import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.GpgSigner;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.GpgSignature;
+import org.eclipse.jgit.lib.ObjectBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.lib.Signers;
 import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.submodule.SubmoduleWalk;
@@ -430,10 +435,12 @@ public void commitAfterSquashMerge() throws Exception {
 
 			assertEquals(1, squashedCommit.getParentCount());
 			assertNull(db.readSquashCommitMsg());
-			assertEquals("commit: Squashed commit of the following:", db
-					.getReflogReader(Constants.HEAD).getLastEntry().getComment());
-			assertEquals("commit: Squashed commit of the following:", db
-					.getReflogReader(db.getBranch()).getLastEntry().getComment());
+			assertEquals("commit: Squashed commit of the following:",
+					db.getRefDatabase().getReflogReader(Constants.HEAD)
+							.getLastEntry().getComment());
+			assertEquals("commit: Squashed commit of the following:",
+					db.getRefDatabase().getReflogReader(db.getFullBranch())
+							.getLastEntry().getComment());
 		}
 	}
 
@@ -450,12 +457,15 @@ public void testReflogs() throws Exception {
 			git.commit().setMessage("c3").setAll(true)
 					.setReflogComment("testRl").call();
 
-			db.getReflogReader(Constants.HEAD).getReverseEntries();
+			db.getRefDatabase().getReflogReader(Constants.HEAD)
+					.getReverseEntries();
 
 			assertEquals("testRl;commit (initial): c1;", reflogComments(
-					db.getReflogReader(Constants.HEAD).getReverseEntries()));
+					db.getRefDatabase().getReflogReader(Constants.HEAD)
+							.getReverseEntries()));
 			assertEquals("testRl;commit (initial): c1;", reflogComments(
-					db.getReflogReader(db.getBranch()).getReverseEntries()));
+					db.getRefDatabase().getReflogReader(db.getFullBranch())
+							.getReverseEntries()));
 		}
 	}
 
@@ -481,11 +491,11 @@ public void commitAmendWithoutAuthorShouldSetOriginalAuthorAndAuthorTime()
 			writeTrashFile("file1", "file1");
 			git.add().addFilepattern("file1").call();
 
-			final String authorName = "First Author";
-			final String authorEmail = "author@example.org";
-			final Date authorDate = new Date(1349621117000L);
+			String authorName = "First Author";
+			String authorEmail = "author@example.org";
+			Instant authorDate = Instant.ofEpochSecond(1349621117L);
 			PersonIdent firstAuthor = new PersonIdent(authorName, authorEmail,
-					authorDate, TimeZone.getTimeZone("UTC"));
+					authorDate, ZoneOffset.UTC);
 			git.commit().setMessage("initial commit").setAuthor(firstAuthor).call();
 
 			RevCommit amended = git.commit().setAmend(true)
@@ -494,7 +504,8 @@ public void commitAmendWithoutAuthorShouldSetOriginalAuthorAndAuthorTime()
 			PersonIdent amendedAuthor = amended.getAuthorIdent();
 			assertEquals(authorName, amendedAuthor.getName());
 			assertEquals(authorEmail, amendedAuthor.getEmailAddress());
-			assertEquals(authorDate.getTime(), amendedAuthor.getWhen().getTime());
+			assertEquals(authorDate.getEpochSecond(),
+					amendedAuthor.getWhenAsInstant().getEpochSecond());
 		}
 	}
 
@@ -839,21 +850,39 @@ public void callSignerWithProperSigningKey() throws Exception {
 			String[] signingKey = new String[1];
 			PersonIdent[] signingCommitters = new PersonIdent[1];
 			AtomicInteger callCount = new AtomicInteger();
-			GpgSigner.setDefault(new GpgSigner() {
+			// Since GpgFormat defaults to OpenPGP just set a new signer for
+			// that.
+			Signers.set(GpgFormat.OPENPGP, new Signer() {
+
 				@Override
-				public void sign(CommitBuilder commit, String gpgSigningKey,
-						PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
-					signingKey[0] = gpgSigningKey;
+				public void signObject(Repository repo, GpgConfig config,
+						ObjectBuilder builder, PersonIdent signingCommitter,
+						String signingKeySpec,
+						CredentialsProvider credentialsProvider)
+						throws CanceledException,
+						UnsupportedSigningFormatException {
+					signingKey[0] = signingKeySpec;
 					signingCommitters[0] = signingCommitter;
 					callCount.incrementAndGet();
 				}
 
 				@Override
-				public boolean canLocateSigningKey(String gpgSigningKey,
-						PersonIdent signingCommitter,
+				public GpgSignature sign(Repository repo, GpgConfig config,
+						byte[] data, PersonIdent signingCommitter,
+						String signingKeySpec,
+						CredentialsProvider credentialsProvider)
+						throws CanceledException,
+						UnsupportedSigningFormatException {
+					throw new CanceledException("Unexpected call");
+				}
+
+				@Override
+				public boolean canLocateSigningKey(Repository repo,
+						GpgConfig config, PersonIdent signingCommitter,
+						String signingKeySpec,
 						CredentialsProvider credentialsProvider)
 						throws CanceledException {
-					return false;
+					throw new CanceledException("Unexpected call");
 				}
 			});
 
@@ -904,19 +933,37 @@ public void callSignerOnlyWhenSigning() throws Exception {
 			git.add().addFilepattern("file1").call();
 
 			AtomicInteger callCount = new AtomicInteger();
-			GpgSigner.setDefault(new GpgSigner() {
+			// Since GpgFormat defaults to OpenPGP just set a new signer for
+			// that.
+			Signers.set(GpgFormat.OPENPGP, new Signer() {
+
 				@Override
-				public void sign(CommitBuilder commit, String gpgSigningKey,
-						PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
+				public void signObject(Repository repo, GpgConfig config,
+						ObjectBuilder builder, PersonIdent signingCommitter,
+						String signingKeySpec,
+						CredentialsProvider credentialsProvider)
+						throws CanceledException,
+						UnsupportedSigningFormatException {
 					callCount.incrementAndGet();
 				}
 
 				@Override
-				public boolean canLocateSigningKey(String gpgSigningKey,
-						PersonIdent signingCommitter,
+				public GpgSignature sign(Repository repo, GpgConfig config,
+						byte[] data, PersonIdent signingCommitter,
+						String signingKeySpec,
+						CredentialsProvider credentialsProvider)
+						throws CanceledException,
+						UnsupportedSigningFormatException {
+					throw new CanceledException("Unexpected call");
+				}
+
+				@Override
+				public boolean canLocateSigningKey(Repository repo,
+						GpgConfig config, PersonIdent signingCommitter,
+						String signingKeySpec,
 						CredentialsProvider credentialsProvider)
 						throws CanceledException {
-					return false;
+					throw new CanceledException("Unexpected call");
 				}
 			});
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
index ab87fa9..060e6d3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
@@ -12,6 +12,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -87,6 +88,9 @@ public void testDescribe() throws Exception {
 			assertEquals("alice-t1", describe(c2, "alice*"));
 			assertEquals("alice-t1", describe(c2, "a*", "b*", "c*"));
 
+			assertNotEquals("alice-t1", describeExcluding(c2, "alice*"));
+			assertNotEquals("alice-t1", describeCommand(c2).setMatch("*").setExclude("alice*").call());
+
 			assertEquals("bob-t2", describe(c3));
 			assertEquals("bob-t2-0-g44579eb", describe(c3, true, false));
 			assertEquals("alice-t1-1-g44579eb", describe(c3, "alice*"));
@@ -95,6 +99,15 @@ public void testDescribe() throws Exception {
 			assertEquals("bob-t2", describe(c3, "?ob*"));
 			assertEquals("bob-t2", describe(c3, "a*", "b*", "c*"));
 
+			assertNotEquals("alice-t1-1-g44579eb", describeExcluding(c3, "alice*"));
+			assertNotEquals("alice-t1-1-g44579eb", describeCommand(c3).setMatch("*").setExclude("alice*").call());
+			assertNotEquals("alice-t1-1-g44579eb", describeExcluding(c3, "a??c?-t*"));
+			assertNotEquals("alice-t1-1-g44579eb", describeCommand(c3).setMatch("bob*").setExclude("a??c?-t*").call());
+			assertNotEquals("bob-t2", describeExcluding(c3, "bob*"));
+			assertNotEquals("bob-t2", describeCommand(c3).setMatch("alice*").setExclude("bob*"));
+			assertNotEquals("bob-t2", describeExcluding(c3, "?ob*"));
+			assertNotEquals("bob-t2", describeCommand(c3).setMatch("a??c?-t*").setExclude("?ob*"));
+
 			// the value verified with git-describe(1)
 			assertEquals("bob-t2-1-g3e563c5", describe(c4));
 			assertEquals("bob-t2-1-g3e563c5", describe(c4, true, false));
@@ -518,6 +531,15 @@ private String describe(ObjectId c1, String... patterns) throws Exception {
 				.setMatch(patterns).call();
 	}
 
+	private String describeExcluding(ObjectId c1, String... patterns) throws Exception {
+		return git.describe().setTarget(c1).setTags(describeUseAllTags)
+				.setExclude(patterns).call();
+	}
+
+	private DescribeCommand describeCommand(ObjectId c1) throws Exception {
+		return git.describe().setTarget(c1).setTags(describeUseAllTags);
+	}
+
 	private static void assertNameStartsWith(ObjectId c4, String prefix) {
 		assertTrue(c4.name(), c4.name().startsWith(prefix));
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java
index b937b1f..4c971ff 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java
@@ -559,7 +559,7 @@ private void setupGitAndDoHardReset(AutoCRLF autoCRLF, EOL eol,
 
 		}
 		if (infoAttributesContent != null) {
-			File f = new File(db.getDirectory(), Constants.INFO_ATTRIBUTES);
+			File f = new File(db.getCommonDirectory(), Constants.INFO_ATTRIBUTES);
 			write(f, infoAttributesContent);
 		}
 		config.save();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java
index 3ec454c..3731347 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java
@@ -92,8 +92,8 @@ public void testFetchHasRefLogForRemoteRef() throws Exception {
 
 		assertTrue(remoteRef.getName().startsWith(Constants.R_REMOTES));
 		assertEquals(defaultBranchSha1, remoteRef.getObjectId());
-		assertNotNull(git.getRepository().getReflogReader(remoteRef.getName())
-				.getLastEntry());
+		assertNotNull(git.getRepository().getRefDatabase()
+				.getReflogReader(remoteRef.getName()).getLastEntry());
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java
index f98db34..6090d5e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java
@@ -11,12 +11,11 @@
 
 import static org.junit.Assert.assertTrue;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.Properties;
 
 import org.eclipse.jgit.junit.RepositoryTestCase;
-import org.eclipse.jgit.util.GitDateParser;
-import org.eclipse.jgit.util.SystemReader;
+import org.eclipse.jgit.util.GitTimeParser;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -36,9 +35,8 @@ public void setUp() throws Exception {
 
 	@Test
 	public void testGConeCommit() throws Exception {
-		Date expire = GitDateParser.parse("now", null, SystemReader
-				.getInstance().getLocale());
-		Properties res = git.gc().setExpire(expire).call();
+		Instant expireNow = GitTimeParser.parseInstant("now");
+		Properties res = git.gc().setExpire(expireNow).call();
 		assertTrue(res.size() == 8);
 	}
 
@@ -52,11 +50,8 @@ public void testGCmoreCommits() throws Exception {
 		writeTrashFile("b.txt", "a couple of words for gc to pack more 2");
 		writeTrashFile("c.txt", "a couple of words for gc to pack more 3");
 		git.commit().setAll(true).setMessage("commit3").call();
-		Properties res = git
-				.gc()
-				.setExpire(
-						GitDateParser.parse("now", null, SystemReader
-								.getInstance().getLocale())).call();
+		Instant expireNow = GitTimeParser.parseInstant("now");
+		Properties res = git.gc().setExpire(expireNow).call();
 		assertTrue(res.size() == 8);
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java
index 7693434..e847e72 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java
@@ -14,6 +14,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.time.Instant;
 
 import org.eclipse.jgit.api.ListBranchCommand.ListMode;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -100,7 +101,7 @@ public void testClose() throws IOException, JGitInternalException,
 			GitAPIException {
 		File workTree = db.getWorkTree();
 		Git git = Git.open(workTree);
-		git.gc().setExpire(null).call();
+		git.gc().setExpire((Instant) null).call();
 		git.checkout().setName(git.getRepository().resolve("HEAD^").getName())
 				.call();
 		try {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java
new file mode 100644
index 0000000..3b60e1b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2024, Broadcom 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 static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ReflogEntry;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.junit.Test;
+
+public class LinkedWorktreeTest extends RepositoryTestCase {
+
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+
+		try (Git git = new Git(db)) {
+			git.commit().setMessage("Initial commit").call();
+		}
+	}
+
+	@Test
+	public void testWeCanReadFromLinkedWorktreeFromBare() throws Exception {
+		FS fs = db.getFS();
+		File directory = trash.getParentFile();
+		String dbDirName = db.getWorkTree().getName();
+		cloneBare(fs, directory, dbDirName, "bare");
+		File bareDirectory = new File(directory, "bare");
+		worktreeAddExisting(fs, bareDirectory, "master");
+
+		File worktreesDir = new File(bareDirectory, "worktrees");
+		File masterWorktreesDir = new File(worktreesDir, "master");
+
+		FileRepository repository = new FileRepository(masterWorktreesDir);
+		try (Git git = new Git(repository)) {
+			ObjectId objectId = repository.resolve(HEAD);
+			assertNotNull(objectId);
+
+			Iterator<RevCommit> log = git.log().all().call().iterator();
+			assertTrue(log.hasNext());
+			assertTrue("Initial commit".equals(log.next().getShortMessage()));
+
+			// we have reflog entry
+			// depending on git version we either have one or
+			// two entries where extra is zeroid entry with
+			// same message or no message
+			Collection<ReflogEntry> reflog = git.reflog().call();
+			assertNotNull(reflog);
+			assertTrue(reflog.size() > 0);
+			ReflogEntry[] reflogs = reflog.toArray(new ReflogEntry[0]);
+			assertEquals(reflogs[reflogs.length - 1].getComment(),
+					"reset: moving to HEAD");
+
+			// index works with file changes
+			File masterDir = new File(directory, "master");
+			File testFile = new File(masterDir, "test");
+
+			Status status = git.status().call();
+			assertTrue(status.getUncommittedChanges().size() == 0);
+			assertTrue(status.getUntracked().size() == 0);
+
+			JGitTestUtil.write(testFile, "test");
+			status = git.status().call();
+			assertTrue(status.getUncommittedChanges().size() == 0);
+			assertTrue(status.getUntracked().size() == 1);
+
+			git.add().addFilepattern("test").call();
+			status = git.status().call();
+			assertTrue(status.getUncommittedChanges().size() == 1);
+			assertTrue(status.getUntracked().size() == 0);
+		}
+	}
+
+	@Test
+	public void testWeCanReadFromLinkedWorktreeFromNonBare() throws Exception {
+		FS fs = db.getFS();
+		worktreeAddNew(fs, db.getWorkTree(), "wt");
+
+		File worktreesDir = new File(db.getDirectory(), "worktrees");
+		File masterWorktreesDir = new File(worktreesDir, "wt");
+
+		FileRepository repository = new FileRepository(masterWorktreesDir);
+		try (Git git = new Git(repository)) {
+			ObjectId objectId = repository.resolve(HEAD);
+			assertNotNull(objectId);
+
+			Iterator<RevCommit> log = git.log().all().call().iterator();
+			assertTrue(log.hasNext());
+			assertTrue("Initial commit".equals(log.next().getShortMessage()));
+
+			// we have reflog entry
+			Collection<ReflogEntry> reflog = git.reflog().call();
+			assertNotNull(reflog);
+			assertTrue(reflog.size() > 0);
+			ReflogEntry[] reflogs = reflog.toArray(new ReflogEntry[0]);
+			assertEquals(reflogs[reflogs.length - 1].getComment(),
+					"reset: moving to HEAD");
+
+			// index works with file changes
+			File directory = trash.getParentFile();
+			File wtDir = new File(directory, "wt");
+			File testFile = new File(wtDir, "test");
+
+			Status status = git.status().call();
+			assertTrue(status.getUncommittedChanges().size() == 0);
+			assertTrue(status.getUntracked().size() == 0);
+
+			JGitTestUtil.write(testFile, "test");
+			status = git.status().call();
+			assertTrue(status.getUncommittedChanges().size() == 0);
+			assertTrue(status.getUntracked().size() == 1);
+
+			git.add().addFilepattern("test").call();
+			status = git.status().call();
+			assertTrue(status.getUncommittedChanges().size() == 1);
+			assertTrue(status.getUntracked().size() == 0);
+		}
+
+	}
+
+	private static void cloneBare(FS fs, File directory, String from, String to) throws IOException, InterruptedException {
+		ProcessBuilder builder = fs.runInShell("git",
+				new String[] { "clone", "--bare", from, to });
+		builder.directory(directory);
+		builder.environment().put("HOME", fs.userHome().getAbsolutePath());
+		StringBuilder input = new StringBuilder();
+		ExecutionResult result = fs.execute(builder, new ByteArrayInputStream(
+				input.toString().getBytes(StandardCharsets.UTF_8)));
+		String stdOut = toString(result.getStdout());
+		String errorOut = toString(result.getStderr());
+		assertNotNull(stdOut);
+		assertNotNull(errorOut);
+	}
+
+	private static void worktreeAddExisting(FS fs, File directory, String name) throws IOException, InterruptedException {
+		ProcessBuilder builder = fs.runInShell("git",
+				new String[] { "worktree", "add", "../" + name, name });
+		builder.directory(directory);
+		builder.environment().put("HOME", fs.userHome().getAbsolutePath());
+		StringBuilder input = new StringBuilder();
+		ExecutionResult result = fs.execute(builder, new ByteArrayInputStream(
+				input.toString().getBytes(StandardCharsets.UTF_8)));
+		String stdOut = toString(result.getStdout());
+		String errorOut = toString(result.getStderr());
+		assertNotNull(stdOut);
+		assertNotNull(errorOut);
+	}
+
+	private static void worktreeAddNew(FS fs, File directory, String name) throws IOException, InterruptedException {
+		ProcessBuilder builder = fs.runInShell("git",
+				new String[] { "worktree", "add", "-b", name, "../" + name, "master"});
+		builder.directory(directory);
+		builder.environment().put("HOME", fs.userHome().getAbsolutePath());
+		StringBuilder input = new StringBuilder();
+		ExecutionResult result = fs.execute(builder, new ByteArrayInputStream(
+				input.toString().getBytes(StandardCharsets.UTF_8)));
+		String stdOut = toString(result.getStdout());
+		String errorOut = toString(result.getStderr());
+		assertNotNull(stdOut);
+		assertNotNull(errorOut);
+	}
+
+	private static String toString(TemporaryBuffer b) throws IOException {
+		return RawParseUtils.decode(b.toByteArray());
+	}
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
index 917b6c3..1ec5067 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
@@ -21,6 +21,9 @@
 import static org.junit.Assume.assumeTrue;
 
 import java.io.File;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Iterator;
 import java.util.regex.Pattern;
 
@@ -33,6 +36,7 @@
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.lib.Sets;
@@ -45,6 +49,7 @@
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.GitDateFormatter;
 import org.eclipse.jgit.util.GitDateFormatter.Format;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.experimental.theories.DataPoints;
@@ -76,12 +81,12 @@ public void testMergeInItself() throws Exception {
 			assertEquals(MergeResult.MergeStatus.ALREADY_UP_TO_DATE, result.getMergeStatus());
 		}
 		// no reflog entry written by merge
-		assertEquals("commit (initial): initial commit",
-				db
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals("commit (initial): initial commit", refDb
 				.getReflogReader(Constants.HEAD).getLastEntry().getComment());
-		assertEquals("commit (initial): initial commit",
-				db
-				.getReflogReader(db.getBranch()).getLastEntry().getComment());
+		assertEquals("commit (initial): initial commit", refDb
+				.getReflogReader(db.getFullBranch()).getLastEntry()
+				.getComment());
 	}
 
 	@Test
@@ -96,10 +101,11 @@ public void testAlreadyUpToDate() throws Exception {
 			assertEquals(second, result.getNewHead());
 		}
 		// no reflog entry written by merge
-		assertEquals("commit: second commit", db
+		assertEquals("commit: second commit", db.getRefDatabase()
 				.getReflogReader(Constants.HEAD).getLastEntry().getComment());
-		assertEquals("commit: second commit", db
-				.getReflogReader(db.getBranch()).getLastEntry().getComment());
+		assertEquals("commit: second commit", db.getRefDatabase()
+				.getReflogReader(db.getFullBranch()).getLastEntry()
+				.getComment());
 	}
 
 	@Test
@@ -117,10 +123,13 @@ public void testFastForward() throws Exception {
 			assertEquals(MergeResult.MergeStatus.FAST_FORWARD, result.getMergeStatus());
 			assertEquals(second, result.getNewHead());
 		}
+		RefDatabase refDb = db.getRefDatabase();
 		assertEquals("merge refs/heads/master: Fast-forward",
-				db.getReflogReader(Constants.HEAD).getLastEntry().getComment());
+				refDb.getReflogReader(Constants.HEAD)
+						.getLastEntry().getComment());
 		assertEquals("merge refs/heads/master: Fast-forward",
-				db.getReflogReader(db.getBranch()).getLastEntry().getComment());
+				refDb.getReflogReader(db.getFullBranch())
+						.getLastEntry().getComment());
 	}
 
 	@Test
@@ -140,10 +149,12 @@ public void testFastForwardNoCommit() throws Exception {
 					result.getMergeStatus());
 			assertEquals(second, result.getNewHead());
 		}
-		assertEquals("merge refs/heads/master: Fast-forward", db
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals("merge refs/heads/master: Fast-forward", refDb
 				.getReflogReader(Constants.HEAD).getLastEntry().getComment());
-		assertEquals("merge refs/heads/master: Fast-forward", db
-				.getReflogReader(db.getBranch()).getLastEntry().getComment());
+		assertEquals("merge refs/heads/master: Fast-forward", refDb
+				.getReflogReader(db.getFullBranch()).getLastEntry()
+				.getComment());
 	}
 
 	@Test
@@ -171,10 +182,12 @@ public void testFastForwardWithFiles() throws Exception {
 			assertEquals(MergeResult.MergeStatus.FAST_FORWARD, result.getMergeStatus());
 			assertEquals(second, result.getNewHead());
 		}
-		assertEquals("merge refs/heads/master: Fast-forward",
-				db.getReflogReader(Constants.HEAD).getLastEntry().getComment());
-		assertEquals("merge refs/heads/master: Fast-forward",
-				db.getReflogReader(db.getBranch()).getLastEntry().getComment());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals("merge refs/heads/master: Fast-forward", refDb
+				.getReflogReader(Constants.HEAD).getLastEntry().getComment());
+		assertEquals("merge refs/heads/master: Fast-forward", refDb
+				.getReflogReader(db.getFullBranch()).getLastEntry()
+				.getComment());
 	}
 
 	@Test
@@ -229,14 +242,17 @@ public void testMergeSuccessAllStrategies(MergeStrategy mergeStrategy)
 					.include(db.exactRef(R_HEADS + MASTER)).call();
 			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
 		}
+		RefDatabase refDb = db.getRefDatabase();
 		assertEquals(
 				"merge refs/heads/master: Merge made by "
 						+ mergeStrategy.getName() + ".",
-				db.getReflogReader(Constants.HEAD).getLastEntry().getComment());
+				refDb.getReflogReader(Constants.HEAD).getLastEntry()
+						.getComment());
 		assertEquals(
 				"merge refs/heads/master: Merge made by "
 						+ mergeStrategy.getName() + ".",
-				db.getReflogReader(db.getBranch()).getLastEntry().getComment());
+				refDb.getReflogReader(db.getFullBranch()).getLastEntry()
+						.getComment());
 	}
 
 	@Theory
@@ -662,14 +678,17 @@ public void testMultipleCreationsSameContent() throws Exception {
 					.setStrategy(MergeStrategy.RESOLVE).call();
 			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
 			assertEquals("1\nb(1)\n3\n", read(new File(db.getWorkTree(), "b")));
-			assertEquals("merge " + secondCommit.getId().getName()
-					+ ": Merge made by resolve.", db
-					.getReflogReader(Constants.HEAD)
-					.getLastEntry().getComment());
-			assertEquals("merge " + secondCommit.getId().getName()
-					+ ": Merge made by resolve.", db
-					.getReflogReader(db.getBranch())
-					.getLastEntry().getComment());
+			RefDatabase refDb = db.getRefDatabase();
+			assertEquals(
+					"merge " + secondCommit.getId().getName()
+							+ ": Merge made by resolve.",
+					refDb.getReflogReader(Constants.HEAD).getLastEntry()
+							.getComment());
+			assertEquals(
+					"merge " + secondCommit.getId().getName()
+							+ ": Merge made by resolve.",
+					refDb.getReflogReader(db.getFullBranch()).getLastEntry()
+							.getComment());
 		}
 	}
 
@@ -2086,6 +2105,94 @@ public void testMergeConflictWithMessageAndCommentCharAuto()
 		}
 	}
 
+	@Test
+	public void testMergeCaseInsensitiveRename() throws Exception {
+		Assume.assumeTrue(
+				"Test makes only sense on a case-insensitive file system",
+				db.isWorkTreeCaseInsensitive());
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "aaa");
+			git.add().addFilepattern("a").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+			// "Rename" "a" to "A"
+			git.rm().addFilepattern("a").call();
+			writeTrashFile("A", "aaa");
+			git.add().addFilepattern("A").call();
+			RevCommit master = git.commit().setMessage("rename to A").call();
+
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+
+			writeTrashFile("b", "bbb");
+			git.add().addFilepattern("b").call();
+			git.commit().setMessage("side").call();
+
+			// Merge master into side
+			MergeResult result = git.merge().include(master)
+					.setStrategy(MergeStrategy.RECURSIVE).call();
+			assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+			assertTrue(new File(db.getWorkTree(), "A").isFile());
+			// Double check
+			boolean found = true;
+			try (DirectoryStream<Path> dir = Files
+					.newDirectoryStream(db.getWorkTree().toPath())) {
+				for (Path p : dir) {
+					found = "A".equals(p.getFileName().toString());
+					if (found) {
+						break;
+					}
+				}
+			}
+			assertTrue(found);
+		}
+	}
+
+	@Test
+	public void testMergeCaseInsensitiveRenameConflict() throws Exception {
+		Assume.assumeTrue(
+				"Test makes only sense on a case-insensitive file system",
+				db.isWorkTreeCaseInsensitive());
+		try (Git git = new Git(db)) {
+			writeTrashFile("a", "aaa");
+			git.add().addFilepattern("a").call();
+			RevCommit initialCommit = git.commit().setMessage("initial").call();
+			// "Rename" "a" to "A" and change it
+			git.rm().addFilepattern("a").call();
+			writeTrashFile("A", "yyy");
+			git.add().addFilepattern("A").call();
+			RevCommit master = git.commit().setMessage("rename to A").call();
+
+			createBranch(initialCommit, "refs/heads/side");
+			checkoutBranch("refs/heads/side");
+
+			writeTrashFile("a", "xxx");
+			git.add().addFilepattern("a").call();
+			git.commit().setMessage("side").call();
+
+			// Merge master into side
+			MergeResult result = git.merge().include(master)
+					.setStrategy(MergeStrategy.RECURSIVE).call();
+			assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+			File a = new File(db.getWorkTree(), "A");
+			assertTrue(a.isFile());
+			// Double check
+			boolean found = true;
+			try (DirectoryStream<Path> dir = Files
+					.newDirectoryStream(db.getWorkTree().toPath())) {
+				for (Path p : dir) {
+					found = "A".equals(p.getFileName().toString());
+					if (found) {
+						break;
+					}
+				}
+			}
+			assertTrue(found);
+			assertEquals(1, result.getConflicts().size());
+			assertTrue(result.getConflicts().containsKey("a"));
+			checkFile(a, "yyy");
+		}
+	}
+
 	private static void setExecutable(Git git, String path, boolean executable) {
 		FS.DETECTED.setExecute(
 				new File(git.getRepository().getWorkTree(), path), executable);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
index 12300b3..6d5e45c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
@@ -21,6 +21,7 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.util.Map;
 import java.util.concurrent.Callable;
 
 import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
@@ -29,6 +30,7 @@
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.IndexDiff.StageState;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
@@ -117,6 +119,7 @@ public void testPullMerge() throws Exception {
 					+ db.getWorkTree().getAbsolutePath();
 			assertEquals(message, mergeCommit.getShortMessage());
 		}
+		assertTrue(target.status().call().isClean());
 	}
 
 	@Test
@@ -153,6 +156,10 @@ public void testPullConflict() throws Exception {
 		assertFileContentsEqual(targetFile, result);
 		assertEquals(RepositoryState.MERGING, target.getRepository()
 				.getRepositoryState());
+		Status status = target.status().call();
+		Map<String, StageState> conflicting = status.getConflictingStageState();
+		assertEquals(1, conflicting.size());
+		assertEquals(StageState.BOTH_MODIFIED, conflicting.get("SomeFile.txt"));
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
index 70e990d..d1696d6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
@@ -22,6 +22,7 @@
 import java.io.PrintStream;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
 import java.util.Properties;
 
 import org.eclipse.jgit.api.errors.DetachedHeadException;
@@ -1146,7 +1147,7 @@ public void testPushAfterGC() throws Exception {
 			RevCommit commit2 = git2.commit().setMessage("adding a").call();
 
 			// run a gc to ensure we have a bitmap index
-			Properties res = git1.gc().setExpire(null).call();
+			Properties res = git1.gc().setExpire((Instant) null).call();
 			assertEquals(8, res.size());
 
 			// create another commit so we have something else to push
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
index 02e3a2e..4c8cf06 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
@@ -24,6 +24,8 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -55,6 +57,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RebaseTodoLine;
 import org.eclipse.jgit.lib.RebaseTodoLine.Action;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.RepositoryState;
@@ -131,11 +134,12 @@ public void testFastForwardWithNewFile() throws Exception {
 		checkFile(file2, "file2");
 		assertEquals(Status.FAST_FORWARD, res.getStatus());
 
-		List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
+		RefDatabase refDb = db.getRefDatabase();
+		List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD)
 				.getReverseEntries();
-		List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic")
+		List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic")
 				.getReverseEntries();
-		List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master")
+		List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master")
 				.getReverseEntries();
 		assertEquals("rebase finished: returning to refs/heads/topic", headLog
 				.get(0).getComment());
@@ -177,11 +181,12 @@ public void testFastForwardWithMultipleCommits() throws Exception {
 		checkFile(file2, "file2 new content");
 		assertEquals(Status.FAST_FORWARD, res.getStatus());
 
-		List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
+		RefDatabase refDb = db.getRefDatabase();
+		List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD)
 				.getReverseEntries();
-		List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic")
+		List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic")
 				.getReverseEntries();
-		List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master")
+		List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master")
 				.getReverseEntries();
 		assertEquals("rebase finished: returning to refs/heads/topic", headLog
 				.get(0).getComment());
@@ -445,13 +450,14 @@ public void testRebaseShouldIgnoreMergeCommits()
 			assertEquals(a, rw.next());
 		}
 
-		List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
+		RefDatabase refDb = db.getRefDatabase();
+		List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD)
 				.getReverseEntries();
-		List<ReflogEntry> sideLog = db.getReflogReader("refs/heads/side")
+		List<ReflogEntry> sideLog = refDb.getReflogReader("refs/heads/side")
 				.getReverseEntries();
-		List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic")
+		List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic")
 				.getReverseEntries();
-		List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master")
+		List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master")
 				.getReverseEntries();
 		assertEquals("rebase finished: returning to refs/heads/topic", headLog
 				.get(0).getComment());
@@ -766,9 +772,10 @@ public void testRebaseParentOntoHeadShouldBeUptoDate() throws Exception {
 		RebaseResult result = git.rebase().setUpstream(parent).call();
 		assertEquals(Status.UP_TO_DATE, result.getStatus());
 
-		assertEquals(2, db.getReflogReader(Constants.HEAD).getReverseEntries()
-				.size());
-		assertEquals(2, db.getReflogReader("refs/heads/master")
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(2, refDb.getReflogReader(Constants.HEAD)
+				.getReverseEntries().size());
+		assertEquals(2, refDb.getReflogReader("refs/heads/master")
 				.getReverseEntries().size());
 	}
 
@@ -784,9 +791,10 @@ public void testUpToDate() throws Exception {
 		RebaseResult res = git.rebase().setUpstream(first).call();
 		assertEquals(Status.UP_TO_DATE, res.getStatus());
 
-		assertEquals(1, db.getReflogReader(Constants.HEAD).getReverseEntries()
-				.size());
-		assertEquals(1, db.getReflogReader("refs/heads/master")
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(1, refDb.getReflogReader(Constants.HEAD)
+				.getReverseEntries().size());
+		assertEquals(1, refDb.getReflogReader("refs/heads/master")
 				.getReverseEntries().size());
 	}
 
@@ -844,11 +852,12 @@ public void testConflictFreeWithSingleFile() throws Exception {
 					db.resolve(Constants.HEAD)).getParent(0));
 		}
 		assertEquals(origHead, db.readOrigHead());
-		List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
+		RefDatabase refDb = db.getRefDatabase();
+		List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD)
 				.getReverseEntries();
-		List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic")
+		List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic")
 				.getReverseEntries();
-		List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master")
+		List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master")
 				.getReverseEntries();
 		assertEquals(2, masterLog.size());
 		assertEquals(3, topicLog.size());
@@ -896,8 +905,8 @@ public void testDetachedHead() throws Exception {
 					db.resolve(Constants.HEAD)).getParent(0));
 		}
 
-		List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
-				.getReverseEntries();
+		List<ReflogEntry> headLog = db.getRefDatabase()
+				.getReflogReader(Constants.HEAD).getReverseEntries();
 		assertEquals(8, headLog.size());
 		assertEquals("rebase: change file1 in topic", headLog.get(0)
 				.getComment());
@@ -1603,7 +1612,7 @@ public void testStopOnConflictFileCreationAndDeletion() throws Exception {
 	public void testAuthorScriptConverter() throws Exception {
 		// -1 h timezone offset
 		PersonIdent ident = new PersonIdent("Author name", "a.mail@some.com",
-				123456789123L, -60);
+				Instant.ofEpochMilli(123456789123L), ZoneOffset.ofHours(-1));
 		String convertedAuthor = git.rebase().toAuthorScript(ident);
 		String[] lines = convertedAuthor.split("\n");
 		assertEquals("GIT_AUTHOR_NAME='Author name'", lines[0]);
@@ -1615,12 +1624,14 @@ public void testAuthorScriptConverter() throws Exception {
 		assertEquals(ident.getName(), parsedIdent.getName());
 		assertEquals(ident.getEmailAddress(), parsedIdent.getEmailAddress());
 		// this is rounded to the last second
-		assertEquals(123456789000L, parsedIdent.getWhen().getTime());
-		assertEquals(ident.getTimeZoneOffset(), parsedIdent.getTimeZoneOffset());
+		assertEquals(123456789000L,
+				parsedIdent.getWhenAsInstant().toEpochMilli());
+		assertEquals(ident.getZoneId(), parsedIdent.getZoneId());
 
 		// + 9.5h timezone offset
 		ident = new PersonIdent("Author name", "a.mail@some.com",
-				123456789123L, +570);
+				Instant.ofEpochMilli(123456789123L),
+				ZoneOffset.ofHoursMinutes(9, 30));
 		convertedAuthor = git.rebase().toAuthorScript(ident);
 		lines = convertedAuthor.split("\n");
 		assertEquals("GIT_AUTHOR_NAME='Author name'", lines[0]);
@@ -1631,8 +1642,9 @@ public void testAuthorScriptConverter() throws Exception {
 				convertedAuthor.getBytes(UTF_8));
 		assertEquals(ident.getName(), parsedIdent.getName());
 		assertEquals(ident.getEmailAddress(), parsedIdent.getEmailAddress());
-		assertEquals(123456789000L, parsedIdent.getWhen().getTime());
-		assertEquals(ident.getTimeZoneOffset(), parsedIdent.getTimeZoneOffset());
+		assertEquals(123456789000L,
+				parsedIdent.getWhenAsInstant().toEpochMilli());
+		assertEquals(ident.getZoneId(), parsedIdent.getZoneId());
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java
index 534ebd9..add5886 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java
@@ -118,23 +118,21 @@ public void renameBranchSingleConfigValue() throws Exception {
 		String branch = "b1";
 
 		assertEquals(BranchRebaseMode.REBASE,
-				config.getEnum(BranchRebaseMode.values(),
-						ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
-						ConfigConstants.CONFIG_KEY_REBASE,
+				config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION,
+						Constants.MASTER, ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
 		assertNull(config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, branch,
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 
 		assertNotNull(git.branchRename().setNewName(branch).call());
 
 		config = git.getRepository().getConfig();
 		assertNull(config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 		assertEquals(BranchRebaseMode.REBASE,
-				config.getEnum(BranchRebaseMode.values(),
-						ConfigConstants.CONFIG_BRANCH_SECTION, branch,
+				config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
 						ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
 	}
@@ -170,13 +168,12 @@ public void renameBranchMultipleConfigValues() throws Exception {
 		String branch = "b1";
 
 		assertEquals(BranchRebaseMode.REBASE,
-				config.getEnum(BranchRebaseMode.values(),
-						ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
-						ConfigConstants.CONFIG_KEY_REBASE,
+				config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION,
+						Constants.MASTER, ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
 		assertNull(config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, branch,
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 		assertTrue(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
 				Constants.MASTER, ConfigConstants.CONFIG_KEY_MERGE, true));
 		assertFalse(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
@@ -187,10 +184,9 @@ public void renameBranchMultipleConfigValues() throws Exception {
 		config = git.getRepository().getConfig();
 		assertNull(config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
-				ConfigConstants.CONFIG_KEY_REBASE, null));
+				ConfigConstants.CONFIG_KEY_REBASE));
 		assertEquals(BranchRebaseMode.REBASE,
-				config.getEnum(BranchRebaseMode.values(),
-						ConfigConstants.CONFIG_BRANCH_SECTION, branch,
+				config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
 						ConfigConstants.CONFIG_KEY_REBASE,
 						BranchRebaseMode.NONE));
 		assertFalse(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
index 8a479a0..99873e1 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
@@ -36,11 +36,13 @@
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.FileUtils;
 import org.junit.Assert;
+import org.junit.Assume;
 import org.junit.Test;
 
 public class ResetCommandTest extends RepositoryTestCase {
@@ -554,46 +556,73 @@ public void testHardResetOnUnbornBranch() throws Exception {
 		assertNull(db.resolve(Constants.HEAD));
 	}
 
+	@Test
+	public void testHardResetFileMode() throws Exception {
+		Assume.assumeTrue("Test must be able to set executable bit",
+				db.getFS().supportsExecute());
+		git = new Git(db);
+		File a = writeTrashFile("a.txt", "aaa");
+		File b = writeTrashFile("b.txt", "bbb");
+		db.getFS().setExecute(b, true);
+		assertFalse(db.getFS().canExecute(a));
+		assertTrue(db.getFS().canExecute(b));
+		git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
+		RevCommit commit = git.commit().setMessage("files created").call();
+		db.getFS().setExecute(a, true);
+		db.getFS().setExecute(b, false);
+		assertTrue(db.getFS().canExecute(a));
+		assertFalse(db.getFS().canExecute(b));
+		git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
+		git.commit().setMessage("change exe bits").call();
+		Ref ref = git.reset().setRef(commit.getName()).setMode(HARD).call();
+		assertSameAsHead(ref);
+		assertEquals(commit.getId(), ref.getObjectId());
+		assertFalse(db.getFS().canExecute(a));
+		assertTrue(db.getFS().canExecute(b));
+	}
+
 	private void assertReflog(ObjectId prevHead, ObjectId head)
 			throws IOException {
 		// Check the reflog for HEAD
-		String actualHeadMessage = db.getReflogReader(Constants.HEAD)
+		RefDatabase refDb = db.getRefDatabase();
+		String actualHeadMessage = refDb.getReflogReader(Constants.HEAD)
 				.getLastEntry().getComment();
 		String expectedHeadMessage = head.getName() + ": updating HEAD";
 		assertEquals(expectedHeadMessage, actualHeadMessage);
-		assertEquals(head.getName(), db.getReflogReader(Constants.HEAD)
+		assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD)
 				.getLastEntry().getNewId().getName());
-		assertEquals(prevHead.getName(), db.getReflogReader(Constants.HEAD)
+		assertEquals(prevHead.getName(), refDb.getReflogReader(Constants.HEAD)
 				.getLastEntry().getOldId().getName());
 
 		// The reflog for master contains the same as the one for HEAD
-		String actualMasterMessage = db.getReflogReader("refs/heads/master")
+		String actualMasterMessage = refDb.getReflogReader("refs/heads/master")
 				.getLastEntry().getComment();
 		String expectedMasterMessage = head.getName() + ": updating HEAD"; // yes!
 		assertEquals(expectedMasterMessage, actualMasterMessage);
-		assertEquals(head.getName(), db.getReflogReader(Constants.HEAD)
+		assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD)
 				.getLastEntry().getNewId().getName());
-		assertEquals(prevHead.getName(), db
-				.getReflogReader("refs/heads/master").getLastEntry().getOldId()
-				.getName());
+		assertEquals(prevHead.getName(),
+				refDb.getReflogReader("refs/heads/master").getLastEntry()
+						.getOldId().getName());
 	}
 
 	private void assertReflogDisabled(ObjectId head)
 			throws IOException {
+		RefDatabase refDb = db.getRefDatabase();
 		// Check the reflog for HEAD
-		String actualHeadMessage = db.getReflogReader(Constants.HEAD)
+		String actualHeadMessage = refDb.getReflogReader(Constants.HEAD)
 				.getLastEntry().getComment();
 		String expectedHeadMessage = "commit: adding a.txt and dir/b.txt";
 		assertEquals(expectedHeadMessage, actualHeadMessage);
-		assertEquals(head.getName(), db.getReflogReader(Constants.HEAD)
+		assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD)
 				.getLastEntry().getOldId().getName());
 
 		// The reflog for master contains the same as the one for HEAD
-		String actualMasterMessage = db.getReflogReader("refs/heads/master")
+		String actualMasterMessage = refDb.getReflogReader("refs/heads/master")
 				.getLastEntry().getComment();
 		String expectedMasterMessage = "commit: adding a.txt and dir/b.txt";
 		assertEquals(expectedMasterMessage, actualMasterMessage);
-		assertEquals(head.getName(), db.getReflogReader(Constants.HEAD)
+		assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD)
 				.getLastEntry().getOldId().getName());
 	}
 	/**
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java
index 4ebe994..89fdb32 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, Robin Rosenberg and others
+ * Copyright (C) 2011, 2024 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
@@ -29,6 +29,7 @@
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
@@ -59,7 +60,9 @@ public void testRevert() throws IOException, JGitInternalException,
 			writeTrashFile("a",
 					"first line\nsecond line\nthird line\nfourth line\n");
 			git.add().addFilepattern("a").call();
-			RevCommit fixingA = git.commit().setMessage("fixed a").call();
+			// Commit message with a non-empty second line on purpose
+			RevCommit fixingA = git.commit().setMessage("fixed a\nsecond line")
+					.call();
 
 			writeTrashFile("b", "first line\n");
 			git.add().addFilepattern("b").call();
@@ -78,16 +81,18 @@ public void testRevert() throws IOException, JGitInternalException,
 					+ "This reverts commit " + fixingA.getId().getName() + ".\n";
 			assertEquals(expectedMessage, revertCommit.getFullMessage());
 			assertEquals("fixed b", history.next().getFullMessage());
-			assertEquals("fixed a", history.next().getFullMessage());
+			assertEquals("fixed a\nsecond line",
+					history.next().getFullMessage());
 			assertEquals("enlarged a", history.next().getFullMessage());
 			assertEquals("create b", history.next().getFullMessage());
 			assertEquals("create a", history.next().getFullMessage());
 			assertFalse(history.hasNext());
 
-			ReflogReader reader = db.getReflogReader(Constants.HEAD);
+			RefDatabase refDb = db.getRefDatabase();
+			ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("revert: Revert \""));
-			reader = db.getReflogReader(db.getBranch());
+			reader = refDb.getReflogReader(db.getFullBranch());
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("revert: Revert \""));
 		}
@@ -167,10 +172,11 @@ public void testRevertMultiple() throws IOException, JGitInternalException,
 			assertEquals("add first", history.next().getFullMessage());
 			assertFalse(history.hasNext());
 
-			ReflogReader reader = db.getReflogReader(Constants.HEAD);
+			RefDatabase refDb = db.getRefDatabase();
+			ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("revert: Revert \""));
-			reader = db.getReflogReader(db.getBranch());
+			reader = refDb.getReflogReader(db.getFullBranch());
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("revert: Revert \""));
 		}
@@ -220,10 +226,11 @@ public void testRevertMultipleWithFail() throws IOException,
 			assertEquals("add first", history.next().getFullMessage());
 			assertFalse(history.hasNext());
 
-			ReflogReader reader = db.getReflogReader(Constants.HEAD);
+			RefDatabase refDb = db.getRefDatabase();
+			ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("revert: Revert \""));
-			reader = db.getReflogReader(db.getBranch());
+			reader = refDb.getReflogReader(db.getFullBranch());
 			assertTrue(reader.getLastEntry().getComment()
 					.startsWith("revert: Revert \""));
 		}
@@ -428,12 +435,13 @@ private void doRevertAndCheckResult(final Git git,
 		assertEquals(RepositoryState.SAFE, db.getRepositoryState());
 
 		if (reason == null) {
-			ReflogReader reader = db.getReflogReader(Constants.HEAD);
-			assertTrue(reader.getLastEntry().getComment()
-					.startsWith("revert: "));
-			reader = db.getReflogReader(db.getBranch());
-			assertTrue(reader.getLastEntry().getComment()
-					.startsWith("revert: "));
+			RefDatabase refDb = db.getRefDatabase();
+			ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
+			assertTrue(
+					reader.getLastEntry().getComment().startsWith("revert: "));
+			reader = refDb.getReflogReader(db.getFullBranch());
+			assertTrue(
+					reader.getLastEntry().getComment().startsWith("revert: "));
 		}
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java
deleted file mode 100644
index d0fbdbd..0000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (c) 2019 Alex Jitianu <alex_jitianu@sync.ro> 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 static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.security.Policy;
-import java.util.Collections;
-
-import org.eclipse.jgit.junit.RepositoryTestCase;
-import org.eclipse.jgit.util.FileUtils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Tests that using a SecurityManager does not result in errors logged.
- */
-public class SecurityManagerMissingPermissionsTest extends RepositoryTestCase {
-
-	/**
-	 * Collects all logging sent to the logging system.
-	 */
-	private final ByteArrayOutputStream errorOutput = new ByteArrayOutputStream();
-
-	private SecurityManager originalSecurityManager;
-
-	private PrintStream defaultErrorOutput;
-
-	@Override
-	@Before
-	public void setUp() throws Exception {
-		originalSecurityManager = System.getSecurityManager();
-
-		// slf4j-simple logs to System.err, redirect it to enable asserting
-		// logged errors
-		defaultErrorOutput = System.err;
-		System.setErr(new PrintStream(errorOutput));
-
-		refreshPolicyAllPermission(Policy.getPolicy());
-		System.setSecurityManager(new SecurityManager());
-		super.setUp();
-	}
-
-	/**
-	 * If a SecurityManager is active a lot of {@link java.io.FilePermission}
-	 * errors are thrown and logged while initializing a repository.
-	 *
-	 * @throws Exception
-	 */
-	@Test
-	public void testCreateNewRepos_MissingPermissions() throws Exception {
-		File wcTree = new File(getTemporaryDirectory(),
-				"CreateNewRepositoryTest_testCreateNewRepos");
-
-		File marker = new File(getTemporaryDirectory(), "marker");
-		Files.write(marker.toPath(), Collections.singletonList("Can write"));
-		assertTrue("Can write in test directory", marker.isFile());
-		FileUtils.delete(marker);
-		assertFalse("Can delete in test direcory", marker.exists());
-
-		Git git = Git.init().setBare(false)
-				.setDirectory(new File(wcTree.getAbsolutePath())).call();
-
-		addRepoToClose(git.getRepository());
-
-		assertEquals("", errorOutput.toString());
-	}
-
-	@Override
-	@After
-	public void tearDown() throws Exception {
-		System.setSecurityManager(originalSecurityManager);
-		System.setErr(defaultErrorOutput);
-		super.tearDown();
-	}
-
-	/**
-	 * Refresh the Java Security Policy.
-	 *
-	 * @param policy
-	 *            the policy object
-	 *
-	 * @throws IOException
-	 *             if the temporary file that contains the policy could not be
-	 *             created
-	 */
-	private static void refreshPolicyAllPermission(Policy policy)
-			throws IOException {
-		// Starting with an all permissions policy.
-		String policyString = "grant { permission java.security.AllPermission; };";
-
-		// Do not use TemporaryFilesFactory, it will create a dependency cycle
-		Path policyFile = Files.createTempFile("testpolicy", ".txt");
-
-		try {
-			Files.write(policyFile, Collections.singletonList(policyString));
-			System.setProperty("java.security.policy",
-					policyFile.toUri().toURL().toString());
-			policy.refresh();
-		} finally {
-			try {
-				Files.delete(policyFile);
-			} catch (IOException e) {
-				// Do not log; the test tests for no logging having occurred
-				e.printStackTrace();
-			}
-		}
-	}
-
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java
deleted file mode 100644
index 2b930a1..0000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2019 Nail Samatov <sanail@yandex.ru> 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 static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.io.FilePermission;
-import java.io.IOException;
-import java.lang.reflect.ReflectPermission;
-import java.nio.file.Files;
-import java.security.Permission;
-import java.security.SecurityPermission;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.PropertyPermission;
-import java.util.logging.LoggingPermission;
-
-import javax.security.auth.AuthPermission;
-
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.junit.JGitTestUtil;
-import org.eclipse.jgit.junit.MockSystemReader;
-import org.eclipse.jgit.junit.SeparateClassloaderTestRunner;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.FileUtils;
-import org.eclipse.jgit.util.SystemReader;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * <p>
- * Tests if jgit works if SecurityManager is enabled.
- * </p>
- *
- * <p>
- * Note: JGit's classes shouldn't be used before SecurityManager is configured.
- * If you use some JGit's class before SecurityManager is replaced then part of
- * the code can be invoked outside of our custom SecurityManager and this test
- * becomes useless.
- * </p>
- *
- * <p>
- * For example the class {@link org.eclipse.jgit.util.FS} is used widely in jgit
- * sources. It contains DETECTED static field. At the first usage of the class
- * FS the field DETECTED is initialized and during initialization many system
- * operations that SecurityManager can forbid are invoked.
- * </p>
- *
- * <p>
- * For this reason this test doesn't extend LocalDiskRepositoryTestCase (it uses
- * JGit's classes in setUp() method) and other JGit's utility classes. It's done
- * to affect SecurityManager as less as possible.
- * </p>
- *
- * <p>
- * We use SeparateClassloaderTestRunner to isolate FS.DETECTED field
- * initialization between different tests run.
- * </p>
- */
-@RunWith(SeparateClassloaderTestRunner.class)
-public class SecurityManagerTest {
-	private File root;
-
-	private SecurityManager originalSecurityManager;
-
-	private List<Permission> permissions = new ArrayList<>();
-
-	@Before
-	public void setUp() throws Exception {
-		// Create working directory
-		SystemReader.setInstance(new MockSystemReader());
-		root = Files.createTempDirectory("jgit-security").toFile();
-
-		// Add system permissions
-		permissions.add(new RuntimePermission("*"));
-		permissions.add(new SecurityPermission("*"));
-		permissions.add(new AuthPermission("*"));
-		permissions.add(new ReflectPermission("*"));
-		permissions.add(new PropertyPermission("*", "read,write"));
-		permissions.add(new LoggingPermission("control", null));
-
-		permissions.add(new FilePermission(
-				System.getProperty("java.home") + "/-", "read"));
-
-		String tempDir = System.getProperty("java.io.tmpdir");
-		permissions.add(new FilePermission(tempDir, "read,write,delete"));
-		permissions
-				.add(new FilePermission(tempDir + "/-", "read,write,delete"));
-
-		// Add permissions to dependent jar files.
-		String classPath = System.getProperty("java.class.path");
-		if (classPath != null) {
-			for (String path : classPath.split(File.pathSeparator)) {
-				permissions.add(new FilePermission(path, "read"));
-			}
-		}
-		// Add permissions to jgit class files.
-		String jgitSourcesRoot = new File(System.getProperty("user.dir"))
-				.getParent();
-		permissions.add(new FilePermission(jgitSourcesRoot + "/-", "read"));
-
-		// Add permissions to working dir for jgit. Our git repositories will be
-		// initialized and cloned here.
-		permissions.add(new FilePermission(root.getPath() + "/-",
-				"read,write,delete,execute"));
-
-		// Replace Security Manager
-		originalSecurityManager = System.getSecurityManager();
-		System.setSecurityManager(new SecurityManager() {
-
-			@Override
-			public void checkPermission(Permission requested) {
-				for (Permission permission : permissions) {
-					if (permission.implies(requested)) {
-						return;
-					}
-				}
-
-				super.checkPermission(requested);
-			}
-		});
-	}
-
-	@After
-	public void tearDown() throws Exception {
-		System.setSecurityManager(originalSecurityManager);
-
-		// Note: don't use this method before security manager is replaced in
-		// setUp() method. The method uses FS.DETECTED internally and can affect
-		// the test.
-		FileUtils.delete(root, FileUtils.RECURSIVE | FileUtils.RETRY);
-	}
-
-	@Test
-	public void testInitAndClone() throws IOException, GitAPIException {
-		File remote = new File(root, "remote");
-		File local = new File(root, "local");
-
-		try (Git git = Git.init().setDirectory(remote).call()) {
-			JGitTestUtil.write(new File(remote, "hello.txt"), "Hello world!");
-			git.add().addFilepattern(".").call();
-			git.commit().setMessage("Initial commit").call();
-		}
-
-		try (Git git = Git.cloneRepository().setURI(remote.toURI().toString())
-				.setDirectory(local).call()) {
-			assertTrue(new File(local, ".git").exists());
-
-			JGitTestUtil.write(new File(local, "hi.txt"), "Hi!");
-			git.add().addFilepattern(".").call();
-			RevCommit commit1 = git.commit().setMessage("Commit on local repo")
-					.call();
-			assertEquals("Commit on local repo", commit1.getFullMessage());
-			assertNotNull(TreeWalk.forPath(git.getRepository(), "hello.txt",
-					commit1.getTree()));
-		}
-
-	}
-
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java
index 5d0ab05..18cd21a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java
@@ -409,8 +409,8 @@ public void refLogIncludesCommitMessage() throws Exception {
 		assertEquals("content", read(committedFile));
 		validateStashedCommit(stashed);
 
-		ReflogReader reader = git.getRepository().getReflogReader(
-				Constants.R_STASH);
+		ReflogReader reader = git.getRepository().getRefDatabase()
+				.getReflogReader(Constants.R_STASH);
 		ReflogEntry entry = reader.getLastEntry();
 		assertNotNull(entry);
 		assertEquals(ObjectId.zeroId(), entry.getOldId());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java
index c81731d..d937579 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java
@@ -92,8 +92,8 @@ public void dropSingleStashedCommit() throws Exception {
 		stashRef = git.getRepository().exactRef(Constants.R_STASH);
 		assertNull(stashRef);
 
-		ReflogReader reader = git.getRepository().getReflogReader(
-				Constants.R_STASH);
+		ReflogReader reader = git.getRepository().getRefDatabase()
+				.getReflogReader(Constants.R_STASH);
 		assertNull(reader);
 	}
 
@@ -120,8 +120,8 @@ public void dropAll() throws Exception {
 		assertNull(git.stashDrop().setAll(true).call());
 		assertNull(git.getRepository().exactRef(Constants.R_STASH));
 
-		ReflogReader reader = git.getRepository().getReflogReader(
-				Constants.R_STASH);
+		ReflogReader reader = git.getRepository().getRefDatabase()
+				.getReflogReader(Constants.R_STASH);
 		assertNull(reader);
 	}
 
@@ -150,8 +150,8 @@ public void dropFirstStashedCommit() throws Exception {
 		assertNotNull(stashRef);
 		assertEquals(firstStash, stashRef.getObjectId());
 
-		ReflogReader reader = git.getRepository().getReflogReader(
-				Constants.R_STASH);
+		ReflogReader reader = git.getRepository().getRefDatabase()
+				.getReflogReader(Constants.R_STASH);
 		List<ReflogEntry> entries = reader.getReverseEntries();
 		assertEquals(1, entries.size());
 		assertEquals(ObjectId.zeroId(), entries.get(0).getOldId());
@@ -192,8 +192,8 @@ public void dropMiddleStashCommit() throws Exception {
 		assertNotNull(stashRef);
 		assertEquals(thirdStash, stashRef.getObjectId());
 
-		ReflogReader reader = git.getRepository().getReflogReader(
-				Constants.R_STASH);
+		ReflogReader reader = git.getRepository().getRefDatabase()
+				.getReflogReader(Constants.R_STASH);
 		List<ReflogEntry> entries = reader.getReverseEntries();
 		assertEquals(2, entries.size());
 		assertEquals(ObjectId.zeroId(), entries.get(1).getOldId());
@@ -250,8 +250,8 @@ public void dropBoundaryStashedCommits() throws Exception {
 		assertNotNull(stashRef);
 		assertEquals(thirdStash, stashRef.getObjectId());
 
-		ReflogReader reader = git.getRepository().getReflogReader(
-				Constants.R_STASH);
+		ReflogReader reader = git.getRepository().getRefDatabase()
+				.getReflogReader(Constants.R_STASH);
 		List<ReflogEntry> entries = reader.getReverseEntries();
 		assertEquals(2, entries.size());
 		assertEquals(ObjectId.zeroId(), entries.get(1).getOldId());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
index f47f447..c2c06b2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
@@ -23,20 +23,22 @@
 
 /** Unit tests of {@link BlameGenerator}. */
 public class BlameGeneratorTest extends RepositoryTestCase {
+	private static final String FILE = "file.txt";
+
 	@Test
 	public void testBoundLineDelete() throws Exception {
 		try (Git git = new Git(db)) {
 			String[] content1 = new String[] { "first", "second" };
-			writeTrashFile("file.txt", join(content1));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content1));
+			git.add().addFilepattern(FILE).call();
 			RevCommit c1 = git.commit().setMessage("create file").call();
 
 			String[] content2 = new String[] { "third", "first", "second" };
-			writeTrashFile("file.txt", join(content2));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content2));
+			git.add().addFilepattern(FILE).call();
 			RevCommit c2 = git.commit().setMessage("create file").call();
 
-			try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) {
+			try (BlameGenerator generator = new BlameGenerator(db, FILE)) {
 				generator.push(null, db.resolve(Constants.HEAD));
 				assertEquals(3, generator.getResultContents().size());
 
@@ -47,7 +49,7 @@ public void testBoundLineDelete() throws Exception {
 				assertEquals(1, generator.getResultEnd());
 				assertEquals(0, generator.getSourceStart());
 				assertEquals(1, generator.getSourceEnd());
-				assertEquals("file.txt", generator.getSourcePath());
+				assertEquals(FILE, generator.getSourcePath());
 
 				assertTrue(generator.next());
 				assertEquals(c1, generator.getSourceCommit());
@@ -56,7 +58,7 @@ public void testBoundLineDelete() throws Exception {
 				assertEquals(3, generator.getResultEnd());
 				assertEquals(0, generator.getSourceStart());
 				assertEquals(2, generator.getSourceEnd());
-				assertEquals("file.txt", generator.getSourcePath());
+				assertEquals(FILE, generator.getSourcePath());
 
 				assertFalse(generator.next());
 			}
@@ -87,7 +89,8 @@ public void testRenamedBoundLineDelete() throws Exception {
 			git.add().addFilepattern(FILENAME_2).call();
 			RevCommit c2 = git.commit().setMessage("change file2").call();
 
-			try (BlameGenerator generator = new BlameGenerator(db, FILENAME_2)) {
+			try (BlameGenerator generator = new BlameGenerator(db,
+					FILENAME_2)) {
 				generator.push(null, db.resolve(Constants.HEAD));
 				assertEquals(3, generator.getResultContents().size());
 
@@ -113,7 +116,8 @@ public void testRenamedBoundLineDelete() throws Exception {
 			}
 
 			// and test again with other BlameGenerator API:
-			try (BlameGenerator generator = new BlameGenerator(db, FILENAME_2)) {
+			try (BlameGenerator generator = new BlameGenerator(db,
+					FILENAME_2)) {
 				generator.push(null, db.resolve(Constants.HEAD));
 				BlameResult result = generator.computeBlameResult();
 
@@ -136,21 +140,21 @@ public void testLinesAllDeletedShortenedWalk() throws Exception {
 		try (Git git = new Git(db)) {
 			String[] content1 = new String[] { "first", "second", "third" };
 
-			writeTrashFile("file.txt", join(content1));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content1));
+			git.add().addFilepattern(FILE).call();
 			git.commit().setMessage("create file").call();
 
 			String[] content2 = new String[] { "" };
 
-			writeTrashFile("file.txt", join(content2));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content2));
+			git.add().addFilepattern(FILE).call();
 			git.commit().setMessage("create file").call();
 
-			writeTrashFile("file.txt", join(content1));
-			git.add().addFilepattern("file.txt").call();
+			writeTrashFile(FILE, join(content1));
+			git.add().addFilepattern(FILE).call();
 			RevCommit c3 = git.commit().setMessage("create file").call();
 
-			try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) {
+			try (BlameGenerator generator = new BlameGenerator(db, FILE)) {
 				generator.push(null, db.resolve(Constants.HEAD));
 				assertEquals(3, generator.getResultContents().size());
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
index 7fb98ec..c41dd81 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
@@ -584,7 +584,7 @@ private void setupRepo(
 
 		}
 		if (infoAttributesContent != null) {
-			File f = new File(db.getDirectory(), Constants.INFO_ATTRIBUTES);
+			File f = new File(db.getCommonDirectory(), Constants.INFO_ATTRIBUTES);
 			write(f, infoAttributesContent);
 		}
 		config.save();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java
index f23469e..35b9533 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java
@@ -26,6 +26,7 @@
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.junit.Before;
 import org.junit.Test;
@@ -230,10 +231,10 @@ private void assertAttributesNode(TreeWalk walk, String pathName,
 		else {
 
 			Attributes entryAttributes = new Attributes();
-			new AttributesHandler(walk).mergeAttributes(attributesNode,
-					pathName,
-					false,
-					entryAttributes);
+			new AttributesHandler(walk,
+					() -> walk.getTree(CanonicalTreeParser.class))
+							.mergeAttributes(attributesNode, pathName, false,
+									entryAttributes);
 
 			if (nodeAttrs != null && !nodeAttrs.isEmpty()) {
 				for (Attribute attribute : nodeAttrs) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
index 1fcfbaf..dbbcb75 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
@@ -20,6 +20,7 @@
 
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.junit.After;
 import org.junit.Test;
@@ -156,8 +157,9 @@ public void testDoubleAsteriskAtEnd() throws IOException {
 	private void assertAttribute(String path, AttributesNode node,
 			Attributes attrs) throws IOException {
 		Attributes attributes = new Attributes();
-		new AttributesHandler(DUMMY_WALK).mergeAttributes(node, path, false,
-				attributes);
+		new AttributesHandler(DUMMY_WALK,
+				() -> DUMMY_WALK.getTree(CanonicalTreeParser.class))
+						.mergeAttributes(node, path, false, attributes);
 		assertEquals(attrs, attributes);
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
index 7b573e1..c6c9138 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
@@ -26,6 +26,7 @@
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.FileTreeIterator;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
@@ -194,9 +195,10 @@ private void assertAttributesNode(TreeWalk walk, String pathName,
 		else {
 
 			Attributes entryAttributes = new Attributes();
-			new AttributesHandler(walk).mergeAttributes(attributesNode,
-					pathName, false,
-					entryAttributes);
+			new AttributesHandler(walk,
+					() -> walk.getTree(CanonicalTreeParser.class))
+							.mergeAttributes(attributesNode, pathName, false,
+									entryAttributes);
 
 			if (nodeAttrs != null && !nodeAttrs.isEmpty()) {
 				for (Attribute attribute : nodeAttrs) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java
index 009ca8a..ac30c6c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java
@@ -268,6 +268,51 @@ public void mergeTextualFile_SetBinaryMerge_Conflict()
 	}
 
 	@Test
+	public void mergeTextualFile_SetUnionMerge() throws NoWorkTreeException,
+			NoFilepatternException, GitAPIException, IOException {
+		try (Git git = createRepositoryBinaryConflict(g -> {
+			try {
+				writeTrashFile(".gitattributes", "*.cat merge=union");
+				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				throw new UncheckedIOException(e);
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "G\n" + "C\n" + "F\n");
+			} catch (IOException e) {
+				throw new UncheckedIOException(e);
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				throw new UncheckedIOException(e);
+			}
+		})) {
+			// Check that the merge attribute is set to union
+			assertAddMergeAttributeCustom(REFS_HEADS_LEFT, "main.cat", "union");
+			assertAddMergeAttributeCustom(REFS_HEADS_RIGHT, "main.cat",
+					"union");
+
+			checkoutBranch(REFS_HEADS_LEFT);
+			// Merge refs/heads/left -> refs/heads/right
+
+			MergeResult mergeResult = git.merge()
+					.include(git.getRepository().resolve(REFS_HEADS_RIGHT))
+					.call();
+			assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus());
+
+			// Check that the file is the union of both branches (no conflict
+			// marker added)
+			String result = read(writeTrashFile("res.cat",
+					"A\n" + "G\n" + "E\n" + "C\n" + "F\n"));
+			assertEquals(result, read(git.getRepository().getWorkTree().toPath()
+					.resolve("main.cat").toFile()));
+		}
+	}
+
+	@Test
 	public void mergeBinaryFile_NoAttr_Conflict() throws IllegalStateException,
 			IOException, NoHeadException, ConcurrentRefUpdateException,
 			CheckoutConflictException, InvalidMergeHeadsException,
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java
new file mode 100644
index 0000000..65cac11
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * 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.blame;
+
+import static java.lang.String.join;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.blame.cache.BlameCache;
+import org.eclipse.jgit.blame.cache.CacheRegion;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class BlameGeneratorCacheTest extends RepositoryTestCase {
+	private static final String FILE = "file.txt";
+
+	/**
+	 * Simple history:
+	 *
+	 * <pre>
+	 *          C1    C2    C3    C4   C4 blame
+	 * lines ----------------------------------
+	 * L1    |  C1    C1    C1    C1     C1
+	 * L2    |  C1    C1   *C3   *C4     C4
+	 * L3    |  C1    C1   *C3    C3     C3
+	 * L4    |       *C2    C2   *C4     C4
+	 * </pre>
+	 *
+	 * @throws Exception any error
+	 */
+	@Test
+	public void blame_simple_correctRegions() throws Exception {
+		RevCommit c1, c2, c3, c4;
+		try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+			c1 = commit(r, lines("L1C1", "L2C1", "L3C1"));
+			c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1);
+			c3 = commit(r, lines("L1C1", "L2C3", "L3C3", "L4C2"), c2);
+			c4 = commit(r, lines("L1C1", "L2C4", "L3C3", "L4C4"), c3);
+		}
+
+		List<EmittedRegion> expectedRegions = Arrays.asList(
+				new EmittedRegion(c1, 0, 1),
+				new EmittedRegion(c4, 1, 2),
+				new EmittedRegion(c3, 2, 3),
+				new EmittedRegion(c4, 3, 4));
+
+		assertRegions(c4, null, expectedRegions, 4);
+		assertRegions(c4, emptyCache(), expectedRegions, 4);
+		assertRegions(c4, blameAndCache(c4), expectedRegions, 4);
+		assertRegions(c4, blameAndCache(c3), expectedRegions, 4);
+		assertRegions(c4, blameAndCache(c2), expectedRegions, 4);
+		assertRegions(c4, blameAndCache(c1), expectedRegions, 4);
+	}
+
+	@Test
+	public void blame_simple_cacheUsage() throws Exception {
+		RevCommit c1, c2, c3, c4;
+		try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+			c1 = commit(r, lines("L1C1", "L2C1", "L3C1"));
+			c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1);
+			c3 = commit(r, lines("L1C1", "L2C3", "L3C3", "L4C2"), c2);
+			c4 = commit(r, lines("L1C1", "L2C4", "L3C3", "L4C4"), c3);
+		}
+
+		assertCacheUsage(c4, null, false, 4);
+		assertCacheUsage(c4, emptyCache(), false, 4);
+		assertCacheUsage(c4, blameAndCache(c4), true, 1);
+		assertCacheUsage(c4, blameAndCache(c3), true, 2);
+		assertCacheUsage(c4, blameAndCache(c2), true, 3);
+		assertCacheUsage(c4, blameAndCache(c1), true, 4);
+	}
+
+	/**
+	 * Overwrite:
+	 *
+	 * <pre>
+	 *          C1    C2    C3    C3 blame
+	 * lines ----------------------------------
+	 * L1    |  C1    C1   *C3      C3
+	 * L2    |  C1    C1   *C3      C3
+	 * L3    |  C1    C1   *C3      C3
+	 * L4    |       *C2
+	 * </pre>
+	 *
+	 * @throws Exception any error
+	 */
+	@Test
+	public void blame_ovewrite_correctRegions() throws Exception {
+		RevCommit c1, c2, c3;
+		try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+			c1 = commit(r, lines("L1C1", "L2C1", "L3C1"));
+			c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1);
+			c3 = commit(r, lines("L1C3", "L2C3", "L3C3"), c2);
+		}
+
+		List<EmittedRegion> expectedRegions = Arrays.asList(
+				new EmittedRegion(c3, 0, 3));
+
+		assertRegions(c3, null, expectedRegions, 3);
+		assertRegions(c3, emptyCache(), expectedRegions, 3);
+		assertRegions(c3, blameAndCache(c3), expectedRegions, 3);
+		assertRegions(c3, blameAndCache(c2), expectedRegions, 3);
+		assertRegions(c3, blameAndCache(c1), expectedRegions, 3);
+	}
+
+	@Test
+	public void blame_overwrite_cacheUsage() throws Exception {
+		RevCommit c1, c2, c3;
+		try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+			c1 = commit(r, lines("L1C1", "L2C1", "L3C1"));
+			c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1);
+			c3 = commit(r, lines("L1C3", "L2C3", "L3C3"), c2);
+		}
+
+		assertCacheUsage(c3, null, false, 1);
+		assertCacheUsage(c3, emptyCache(), false, 1);
+		assertCacheUsage(c3, blameAndCache(c3), true, 1);
+		assertCacheUsage(c3, blameAndCache(c2), false, 1);
+		assertCacheUsage(c3, blameAndCache(c1), false, 1);
+	}
+
+	/**
+	 * Merge:
+	 *
+	 * <pre>
+	 *                 root
+	 *                 ----
+	 *                 L1  -
+	 *                 L2  -
+	 *                 L3  -
+	 *               /     \
+	 *           sideA     sideB
+	 *           -----     -----
+	 *           *L1 a      L1 -
+	 *           *L2 a      L2 -
+	 *           *L3 a      L3 -
+	 *           *L4 a     *L4 b
+	 *            L5 -     *L5 b
+	 *            L6 -     *L6 b
+	 *            L7 -     *L7 b
+	 *              \       /
+	 *                merge
+	 *                -----
+	 *              L1-L4 a (from sideA)
+	 *              L5-L7 - (common, from root)
+	 *              L8-L11 b (from sideB)
+	 * </pre>
+	 *
+	 * @throws Exception any error
+	 */
+	@Test
+	public void blame_merge_correctRegions() throws Exception {
+		RevCommit root, sideA, sideB, mergedTip;
+		try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+			root = commitAsLines(r, "---");
+			sideA = commitAsLines(r, "aaaa---", root);
+			sideB = commitAsLines(r, "---bbbb", root);
+			mergedTip = commitAsLines(r, "aaaa---bbbb", sideA, sideB);
+		}
+
+		List<EmittedRegion> expectedRegions = Arrays.asList(
+				new EmittedRegion(sideA, 0, 4),
+				new EmittedRegion(root, 4, 7),
+				new EmittedRegion(sideB, 7, 11));
+
+		assertRegions(mergedTip, null, expectedRegions, 11);
+		assertRegions(mergedTip, emptyCache(), expectedRegions, 11);
+		assertRegions(mergedTip, blameAndCache(root), expectedRegions, 11);
+		assertRegions(mergedTip, blameAndCache(sideA), expectedRegions, 11);
+		assertRegions(mergedTip, blameAndCache(sideB), expectedRegions, 11);
+		assertRegions(mergedTip, blameAndCache(mergedTip), expectedRegions, 11);
+	}
+
+	@Test
+	public void blame_merge_cacheUsage() throws Exception {
+		RevCommit root, sideA, sideB, mergedTip;
+		try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+			root = commitAsLines(r, "---");
+			sideA = commitAsLines(r, "aaaa---", root);
+			sideB = commitAsLines(r, "---bbbb", root);
+			mergedTip = commitAsLines(r, "aaaa---bbbb", sideA, sideB);
+		}
+
+		assertCacheUsage(mergedTip, null, /* cacheUsed */ false,
+				/* candidates */ 4);
+		assertCacheUsage(mergedTip, emptyCache(), false, 4);
+		assertCacheUsage(mergedTip, blameAndCache(mergedTip), true, 1);
+
+		// While splitting unblamed regions to parents, sideA comes first
+		// and gets "aaaa----". Processing is by commit time, so sideB is
+		// explored first
+		assertCacheUsage(mergedTip, blameAndCache(sideA), true, 3);
+		assertCacheUsage(mergedTip, blameAndCache(sideB), true, 4);
+		assertCacheUsage(mergedTip, blameAndCache(root), true, 4);
+	}
+
+	/**
+	 * Moving block (insertion)
+	 *
+	 * <pre>
+	 *          C1    C2    C3    C3 blame
+	 * lines ----------------------------------
+	 * L1    |  C1    C1    C1      C1
+	 * L2    |  C1   *C2    C2      C2
+	 * L3    |        C1   *C3      C3
+	 * L4    |              C1      C1
+	 * </pre>
+	 *
+	 * @throws Exception any error
+	 */
+	@Test
+	public void blame_movingBlock_correctRegions() throws Exception {
+		RevCommit c1, c2, c3;
+		try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+			c1 = commit(r, lines("L1C1", "L2C1"));
+			c2 = commit(r, lines("L1C1", "middle", "L2C1"), c1);
+			c3 = commit(r, lines("L1C1", "middle", "extra", "L2C1"), c2);
+		}
+
+		List<EmittedRegion> expectedRegions = Arrays.asList(
+				new EmittedRegion(c1, 0, 1),
+				new EmittedRegion(c2, 1, 2),
+				new EmittedRegion(c3, 2, 3),
+				new EmittedRegion(c1, 3, 4));
+
+		assertRegions(c3, null, expectedRegions, 4);
+		assertRegions(c3, emptyCache(), expectedRegions, 4);
+		assertRegions(c3, blameAndCache(c3), expectedRegions, 4);
+		assertRegions(c3, blameAndCache(c2), expectedRegions, 4);
+		assertRegions(c3, blameAndCache(c1), expectedRegions, 4);
+	}
+
+	@Test
+	public void blame_movingBlock_cacheUsage() throws Exception {
+		RevCommit c1, c2, c3;
+		try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+			c1 = commitAsLines(r, "root---");
+			c2 = commitAsLines(r, "rootXXX---", c1);
+			c3 = commitAsLines(r, "rootYYYXXX---", c2);
+		}
+
+		assertCacheUsage(c3, null, false, 3);
+		assertCacheUsage(c3, emptyCache(), false, 3);
+		assertCacheUsage(c3, blameAndCache(c3), true, 1);
+		assertCacheUsage(c3, blameAndCache(c2), true, 2);
+		assertCacheUsage(c3, blameAndCache(c1), true, 3);
+	}
+
+	private void assertRegions(RevCommit commit, InMemoryBlameCache cache,
+			List<EmittedRegion> expectedRegions, int resultLineCount)
+			throws IOException {
+		try (BlameGenerator gen = new BlameGenerator(db, FILE, cache)) {
+			gen.push(null, db.parseCommit(commit));
+			List<EmittedRegion> regions = consume(gen);
+			assertRegionsEquals(expectedRegions, regions);
+			assertAllLinesCovered(/* lines= */ resultLineCount, regions);
+		}
+	}
+
+	private void assertCacheUsage(RevCommit commit, InMemoryBlameCache cache,
+			boolean useCache, int candidatesVisited) throws IOException {
+		try (BlameGenerator gen = new BlameGenerator(db, FILE, cache)) {
+			gen.push(null, db.parseCommit(commit));
+			consume(gen);
+			assertEquals(useCache, gen.getStats().isCacheHit());
+			assertEquals(candidatesVisited,
+					gen.getStats().getCandidatesVisited());
+		}
+	}
+
+	private static void assertAllLinesCovered(int lines,
+			List<EmittedRegion> regions) {
+		Collections.sort(regions);
+		assertEquals("Starts in first line", 0, regions.get(0).resultStart());
+		for (int i = 1; i < regions.size(); i++) {
+			assertEquals("No gaps", regions.get(i).resultStart(),
+					regions.get(i - 1).resultEnd());
+		}
+		assertEquals("Ends in last line", lines,
+				regions.get(regions.size() - 1).resultEnd());
+	}
+
+	private static void assertRegionsEquals(
+			List<EmittedRegion> expected, List<EmittedRegion> actual) {
+		assertEquals(expected.size(), actual.size());
+		Collections.sort(actual);
+		for (int i = 0; i < expected.size(); i++) {
+			assertEquals(String.format("List differ in element %d", i),
+					expected.get(i), actual.get(i));
+		}
+	}
+
+	private static InMemoryBlameCache emptyCache() {
+		return new InMemoryBlameCache("<empty>");
+	}
+
+	private List<EmittedRegion> consume(BlameGenerator generator)
+			throws IOException {
+		List<EmittedRegion> result = new ArrayList<>();
+		while (generator.next()) {
+			EmittedRegion genRegion = new EmittedRegion(
+					generator.getSourceCommit().toObjectId(),
+					generator.getResultStart(), generator.getResultEnd());
+			result.add(genRegion);
+		}
+		return result;
+	}
+
+	private InMemoryBlameCache blameAndCache(RevCommit commit)
+			throws IOException {
+		List<CacheRegion> regions;
+		try (BlameGenerator generator = new BlameGenerator(db, FILE)) {
+			generator.push(null, commit);
+			regions = consume(generator).stream()
+					.map(EmittedRegion::asCacheRegion)
+					.collect(Collectors.toUnmodifiableList());
+		}
+		InMemoryBlameCache cache = new InMemoryBlameCache("<x>");
+		cache.put(commit, FILE, regions);
+		return cache;
+	}
+
+	private static RevCommit commitAsLines(TestRepository<?> r,
+			String charPerLine, RevCommit... parents) throws Exception {
+		return commit(r, charPerLine.replaceAll("\\S", "$0\n"), parents);
+	}
+
+	private static RevCommit commit(TestRepository<?> r, String contents,
+			RevCommit... parents) throws Exception {
+		return r.commit(r.tree(r.file(FILE, r.blob(contents))), parents);
+	}
+
+	private static String lines(String... l) {
+		return join("\n", l);
+	}
+
+	private record EmittedRegion(ObjectId oid, int resultStart, int resultEnd)
+			implements Comparable<EmittedRegion> {
+		@Override
+		public int compareTo(EmittedRegion o) {
+			return resultStart - o.resultStart;
+		}
+
+		CacheRegion asCacheRegion() {
+			return new CacheRegion(FILE, oid, resultStart, resultEnd);
+		}
+	}
+
+	private static class InMemoryBlameCache implements BlameCache {
+
+		private final Map<Key, List<CacheRegion>> cache = new HashMap<>();
+
+		private final String description;
+
+		public InMemoryBlameCache(String description) {
+			this.description = description;
+		}
+
+		@Override
+		public List<CacheRegion> get(Repository repo, ObjectId commitId,
+									 String path) throws IOException {
+			return cache.get(new Key(commitId.name(), path));
+		}
+
+		public void put(ObjectId commitId, String path,
+						List<CacheRegion> cachedRegions) {
+			cache.put(new Key(commitId.name(), path), cachedRegions);
+		}
+
+		@Override
+		public String toString() {
+			return "InMemoryCache: " + description;
+		}
+
+		record Key(String commitId, String path) {
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java
new file mode 100644
index 0000000..1b28676
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * 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.blame;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.blame.cache.CacheRegion;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class BlameRegionMergerTest extends RepositoryTestCase {
+
+	private static final ObjectId O1 = ObjectId
+			.fromString("ff6dd8db6edc9aa0ac58fea1d14a55be46c3eb14");
+
+	private static final ObjectId O2 = ObjectId
+			.fromString("c3c7f680c6bee238617f25f6aa85d0b565fc8ecb");
+
+	private static final ObjectId O3 = ObjectId
+			.fromString("29e014aad0399fe8ede7c101d01b6e440ac9966b");
+
+	List<RevCommit> fakeCommits = List.of(new FakeRevCommit(O1),
+			new FakeRevCommit(O2), new FakeRevCommit(O3));
+
+	// In reverse order, so the code doesn't assume a sorted list
+	List<CacheRegion> cachedRegions = List.of(
+			new CacheRegion("README", O3, 20, 30),
+			new CacheRegion("README", O2, 10, 20),
+			new CacheRegion("README", O1, 0, 10));
+
+	BlameRegionMerger blamer = new BlameRegionMergerFakeCommits(fakeCommits,
+			cachedRegions);
+
+	@Test
+	public void intersectRegions_allInside() {
+		Region unblamed = new Region(15, 18, 10);
+		CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+		Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+		// Same lines in result and source
+		assertEquals(15, result.resultStart);
+		assertEquals(18, result.sourceStart);
+		assertEquals(10, result.length);
+		assertNull(result.next);
+	}
+
+	@Test
+	public void intersectRegions_startsBefore() {
+		// Intesecting [4, 14) with [10, 90)
+		Region unblamed = new Region(30, 4, 10);
+		CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+		Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+
+		// The unblamed region starting at 4 (sourceStart), starts at 30 in the
+		// original file (resultStart). e.g. some commit introduced
+		// lines. If we take the second portion of the region, we need to move
+		// the result start accordingly.
+		assertEquals(36, result.resultStart);
+		assertEquals(10, result.sourceStart);
+		assertEquals(4, result.length);
+		assertNull(result.next);
+	}
+
+	@Test
+	public void intersectRegions_endsAfter() {
+		// Intesecting [85, 95) with [10, 90)
+		Region unblamed = new Region(30, 85, 10);
+		CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+		Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+
+		assertEquals(30, result.resultStart);
+		assertEquals(85, result.sourceStart);
+		assertEquals(5, result.length);
+		assertNull(result.next);
+	}
+
+	@Test
+	public void intersectRegions_spillOverBothSides() {
+		// Intesecting [5, 100) with [10, 90)
+		Region unblamed = new Region(30, 5, 95);
+		CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+		Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+
+		assertEquals(35, result.resultStart);
+		assertEquals(10, result.sourceStart);
+		assertEquals(80, result.length);
+		assertNull(result.next);
+	}
+
+	@Test
+	public void intersectRegions_exactMatch() {
+		// Intesecting [5, 100) with [10, 90)
+		Region unblamed = new Region(30, 10, 80);
+		CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+		Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+
+		assertEquals(30, result.resultStart);
+		assertEquals(10, result.sourceStart);
+		assertEquals(80, result.length);
+		assertNull(result.next);
+	}
+
+	@Test
+	public void findOverlaps_allInside() {
+		Region unblamed = new Region(0, 11, 4);
+		List<CacheRegion> overlaps = blamer.findOverlaps(unblamed);
+		assertEquals(1, overlaps.size());
+		assertEquals(10, overlaps.get(0).getStart());
+		assertEquals(20, overlaps.get(0).getEnd());
+	}
+
+	@Test
+	public void findOverlaps_overTwoRegions() {
+		Region unblamed = new Region(0, 8, 4);
+		List<CacheRegion> overlaps = blamer.findOverlaps(unblamed);
+		assertEquals(2, overlaps.size());
+		assertEquals(0, overlaps.get(0).getStart());
+		assertEquals(10, overlaps.get(0).getEnd());
+		assertEquals(10, overlaps.get(1).getStart());
+		assertEquals(20, overlaps.get(1).getEnd());
+	}
+
+	@Test
+	public void findOverlaps_overThreeRegions() {
+		Region unblamed = new Region(0, 8, 15);
+		List<CacheRegion> overlaps = blamer.findOverlaps(unblamed);
+		assertEquals(3, overlaps.size());
+		assertEquals(0, overlaps.get(0).getStart());
+		assertEquals(10, overlaps.get(0).getEnd());
+		assertEquals(10, overlaps.get(1).getStart());
+		assertEquals(20, overlaps.get(1).getEnd());
+		assertEquals(20, overlaps.get(2).getStart());
+		assertEquals(30, overlaps.get(2).getEnd());
+	}
+
+	@Test
+	public void blame_exactOverlap() throws IOException {
+		Region unblamed = new Region(0, 10, 10);
+		List<Candidate> blamed = blamer.mergeOneRegion(unblamed);
+
+		assertEquals(1, blamed.size());
+		Candidate c = blamed.get(0);
+		assertEquals(c.sourceCommit.name(), O2.name());
+		assertEquals(c.regionList.resultStart, unblamed.resultStart);
+		assertEquals(c.regionList.sourceStart, unblamed.sourceStart);
+		assertEquals(10, c.regionList.length);
+		assertNull(c.regionList.next);
+	}
+
+	@Test
+	public void blame_corruptedIndex() {
+		Region outOfRange = new Region(0, 43, 4);
+		// This region is out of the blamed area
+		assertThrows(IOException.class,
+				() -> blamer.mergeOneRegion(outOfRange));
+	}
+
+	@Test
+	public void blame_allInsideOneBlamedRegion() throws IOException {
+		Region unblamed = new Region(0, 5, 3);
+		// This region if fully blamed to O1
+		List<Candidate> blamed = blamer.mergeOneRegion(unblamed);
+		assertEquals(1, blamed.size());
+		Candidate c = blamed.get(0);
+		assertEquals(c.sourceCommit.name(), O1.name());
+		assertEquals(c.regionList.resultStart, unblamed.resultStart);
+		assertEquals(c.regionList.sourceStart, unblamed.sourceStart);
+		assertEquals(3, c.regionList.length);
+		assertNull(c.regionList.next);
+	}
+
+	@Test
+	public void blame_overTwoBlamedRegions() throws IOException {
+		Region unblamed = new Region(0, 8, 5);
+		// (8, 10) belongs go C1, (10, 13) to C2
+		List<Candidate> blamed = blamer.mergeOneRegion(unblamed);
+		assertEquals(2, blamed.size());
+		Candidate c = blamed.get(0);
+		assertEquals(c.sourceCommit.name(), O1.name());
+		assertEquals(unblamed.resultStart, c.regionList.resultStart);
+		assertEquals(unblamed.sourceStart, c.regionList.sourceStart);
+		assertEquals(2, c.regionList.length);
+		assertNull(c.regionList.next);
+
+		c = blamed.get(1);
+		assertEquals(c.sourceCommit.name(), O2.name());
+		assertEquals(2, c.regionList.resultStart);
+		assertEquals(10, c.regionList.sourceStart);
+		assertEquals(3, c.regionList.length);
+		assertNull(c.regionList.next);
+	}
+
+	@Test
+	public void blame_all() throws IOException {
+		Region unblamed = new Region(0, 0, 30);
+		List<Candidate> blamed = blamer.mergeOneRegion(unblamed);
+		assertEquals(3, blamed.size());
+		Candidate c = blamed.get(0);
+		assertEquals(c.sourceCommit.name(), O1.name());
+		assertEquals(unblamed.resultStart, c.regionList.resultStart);
+		assertEquals(unblamed.sourceStart, c.regionList.sourceStart);
+		assertEquals(10, c.regionList.length);
+		assertNull(c.regionList.next);
+
+		c = blamed.get(1);
+		assertEquals(c.sourceCommit.name(), O2.name());
+		assertEquals(10, c.regionList.resultStart);
+		assertEquals(10, c.regionList.sourceStart);
+		assertEquals(10, c.regionList.length);
+		assertNull(c.regionList.next);
+
+		c = blamed.get(2);
+		assertEquals(c.sourceCommit.name(), O3.name());
+		assertEquals(20, c.regionList.resultStart);
+		assertEquals(20, c.regionList.sourceStart);
+		assertEquals(10, c.regionList.length);
+		assertNull(c.regionList.next);
+	}
+
+	@Test
+	public void blame_fromCandidate() {
+		// We don't use anything from the candidate besides the
+		// regionList
+		Candidate c = new Candidate(null, null, null);
+		c.regionList = new Region(0, 8, 5);
+		c.regionList.next = new Region(22, 22, 4);
+
+		Candidate blamed = blamer.mergeCandidate(c);
+		// Three candidates
+		assertNotNull(blamed);
+		assertNotNull(blamed.queueNext);
+		assertNotNull(blamed.queueNext.queueNext);
+		assertNull(blamed.queueNext.queueNext.queueNext);
+
+		assertEquals(O1.name(), blamed.sourceCommit.name());
+
+		Candidate second = blamed.queueNext;
+		assertEquals(O2.name(), second.sourceCommit.name());
+
+		Candidate third = blamed.queueNext.queueNext;
+		assertEquals(O3.name(), third.sourceCommit.name());
+	}
+
+	@Test
+	public void blame_fromCandidate_twiceCandidateInOutput() {
+		Candidate c = new Candidate(null, null, null);
+		// This produces O1 and O2
+		c.regionList = new Region(0, 8, 5);
+		// This produces O2 and O3
+		c.regionList.next = new Region(20, 15, 7);
+
+		Candidate blamed = blamer.mergeCandidate(c);
+		assertCandidateSingleRegion(O1, 2, blamed);
+		blamed = blamed.queueNext;
+		assertCandidateSingleRegion(O2, 3, blamed);
+		// We do not merge candidates afterwards, so these are
+		// two different candidates to the same source
+		blamed = blamed.queueNext;
+		assertCandidateSingleRegion(O2, 5, blamed);
+		blamed = blamed.queueNext;
+		assertCandidateSingleRegion(O3, 2, blamed);
+		assertNull(blamed.queueNext);
+	}
+
+	private static void assertCandidateSingleRegion(ObjectId expectedOid,
+			int expectedLength, Candidate actual) {
+		assertNotNull("candidate", actual);
+		assertNotNull("region list not empty", actual.regionList);
+		assertNull("region list has only one element", actual.regionList.next);
+		assertEquals(expectedOid, actual.sourceCommit);
+		assertEquals(expectedLength, actual.regionList.length);
+	}
+
+	private static final class BlameRegionMergerFakeCommits
+			extends BlameRegionMerger {
+
+		private final Map<ObjectId, RevCommit> cache;
+
+		BlameRegionMergerFakeCommits(List<RevCommit> commits,
+				List<CacheRegion> blamedRegions) {
+			super(null, null, blamedRegions);
+			cache = commits.stream().collect(Collectors
+					.toMap(RevCommit::toObjectId, Function.identity()));
+		}
+
+		@Override
+		protected RevCommit parse(ObjectId oid) {
+			return cache.get(oid);
+		}
+	}
+
+	private static final class FakeRevCommit extends RevCommit {
+		FakeRevCommit(AnyObjectId id) {
+			super(id);
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java
new file mode 100644
index 0000000..1352871
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2024 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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.diff;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.junit.Test;
+
+public class DiffFormatterBuiltInDriverTest extends RepositoryTestCase {
+	@Test
+	public void testCppDriver() throws Exception {
+		String fileName = "greeting.c";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(fileName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.c   diff=cpp");
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("Good day", "Greetings")
+							.replace("baz", "qux"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected =
+					"@@ -27,7 +27,7 @@ void getPersonalizedGreeting(char *result, const char *name, const char *timeOfD\n"
+							+ "@@ -37,7 +37,7 @@ int main() {";
+			assertEquals(expected, actual);
+		}
+	}
+
+	@Test
+	public void testDtsDriver() throws Exception {
+		String fileName = "sample.dtsi";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(fileName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.dtsi   diff=dts");
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("clock-frequency = <24000000>",
+							"clock-frequency = <48000000>"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected = "@@ -20,6 +20,6 @@ uart0: uart@101f1000 {";
+			assertEquals(expected, actual);
+		}
+	}
+
+	@Test
+	public void testJavaDriver() throws Exception {
+		String resourceName = "greeting.javasource";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(resourceName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.java   diff=java");
+			String fileName = "Greeting.java";
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("Good day", "Greetings")
+							.replace("baz", "qux"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected =
+					"@@ -22,7 +22,7 @@ public String getPersonalizedGreeting(String name, String timeOfDay) {\n"
+							+ "@@ -32,6 +32,6 @@ public static void main(String[] args) {";
+			assertEquals(expected, actual);
+		}
+	}
+
+	@Test
+	public void testPythonDriver() throws Exception {
+		String fileName = "greeting.py";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(fileName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.py   diff=python");
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("Good day", "Greetings"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected = "@@ -16,7 +16,7 @@ def get_personalized_greeting(self, name, time_of_day):";
+			assertEquals(expected, actual);
+		}
+	}
+
+	@Test
+	public void testRustDriver() throws Exception {
+		String fileName = "greeting.rs";
+		String body = Files.readString(
+				Path.of(JGitTestUtil.getTestResourceFile(fileName)
+						.getAbsolutePath()));
+		RevCommit c1;
+		RevCommit c2;
+		try (Git git = new Git(db)) {
+			createCommit(git, ".gitattributes", "*.rs   diff=rust");
+			c1 = createCommit(git, fileName, body);
+			c2 = createCommit(git, fileName,
+					body.replace("Good day", "Greetings")
+							.replace("baz", "qux"));
+		}
+		try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+				DiffFormatter diffFormatter = new DiffFormatter(os)) {
+			String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+			String expected =
+					"@@ -14,7 +14,7 @@ fn get_personalized_greeting(&self, name: &str, time_of_day: &str) -> String {\n"
+							+ "@@ -23,5 +23,5 @@ fn main() {";
+			assertEquals(expected, actual);
+		}
+	}
+
+	private String getHunkHeaders(RevCommit c1, RevCommit c2,
+			ByteArrayOutputStream os, DiffFormatter diffFormatter)
+			throws IOException {
+		diffFormatter.setRepository(db);
+		diffFormatter.format(new CanonicalTreeParser(null, db.newObjectReader(),
+						c1.getTree()),
+				new CanonicalTreeParser(null, db.newObjectReader(),
+						c2.getTree()));
+		diffFormatter.flush();
+		return Arrays.stream(os.toString(StandardCharsets.UTF_8).split("\n"))
+				.filter(line -> line.startsWith("@@"))
+				.collect(Collectors.joining("\n"));
+	}
+
+	private RevCommit createCommit(Git git, String fileName, String body)
+			throws IOException, GitAPIException {
+		writeTrashFile(fileName, body);
+		git.add().addFilepattern(".").call();
+		return git.commit().setMessage("message").call();
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java
index c3b9387..5065b57 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java
@@ -12,6 +12,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -22,6 +23,7 @@
 import org.eclipse.jgit.gitrepo.BareSuperprojectWriter.BareWriterConfig;
 import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
@@ -68,6 +70,49 @@ public void write_setGitModulesContents() throws Exception {
 	}
 
 	@Test
+	public void write_setGitModulesContents_pinned() throws Exception {
+		try (Repository bareRepo = createBareRepository()) {
+			RepoProject pinWithUpstream = new RepoProject("pinWithUpstream",
+					"path/x", "cbc0fae7e1911d27e1de37d364698dba4411c78b",
+					"remote", "");
+			pinWithUpstream.setUrl("http://example.com/a");
+			pinWithUpstream.setUpstream("branchX");
+
+			RepoProject pinWithoutUpstream = new RepoProject(
+					"pinWithoutUpstream", "path/y",
+					"cbc0fae7e1911d27e1de37d364698dba4411c78b", "remote", "");
+			pinWithoutUpstream.setUrl("http://example.com/b");
+
+			RemoteReader mockRemoteReader = mock(RemoteReader.class);
+
+			BareSuperprojectWriter w = new BareSuperprojectWriter(bareRepo,
+					null, "refs/heads/master", author, mockRemoteReader,
+					BareWriterConfig.getDefault(), List.of());
+
+			RevCommit commit = w
+					.write(Arrays.asList(pinWithUpstream, pinWithoutUpstream));
+
+			String contents = readContents(bareRepo, commit, ".gitmodules");
+			Config cfg = new Config();
+			cfg.fromText(contents);
+
+			assertThat(cfg.getString("submodule", "pinWithUpstream", "path"),
+					is("path/x"));
+			assertThat(cfg.getString("submodule", "pinWithUpstream", "url"),
+					is("http://example.com/a"));
+			assertThat(cfg.getString("submodule", "pinWithUpstream", "ref"),
+					is("branchX"));
+
+			assertThat(cfg.getString("submodule", "pinWithoutUpstream", "path"),
+					is("path/y"));
+			assertThat(cfg.getString("submodule", "pinWithoutUpstream", "url"),
+					is("http://example.com/b"));
+			assertThat(cfg.getString("submodule", "pinWithoutUpstream", "ref"),
+					nullValue());
+		}
+	}
+
+	@Test
 	public void write_setExtraContents() throws Exception {
 		try (Repository bareRepo = createBareRepository()) {
 			RepoProject repoProject = new RepoProject("subprojectX", "path/to",
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
index 20958a8..fca27d3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
@@ -11,6 +11,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 static org.junit.Assert.fail;
 
@@ -18,7 +19,9 @@
 import java.io.IOException;
 import java.net.URI;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -138,6 +141,72 @@ public void testRemoveProject() throws Exception {
 						.collect(Collectors.toSet()));
 	}
 
+	@Test
+	public void testPinProjectWithUpstream() throws Exception {
+		StringBuilder xmlContent = new StringBuilder();
+		xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+				.append("<manifest>")
+				.append("<remote name=\"remote1\" fetch=\".\" />")
+				.append("<default revision=\"master\" remote=\"remote1\" />")
+				.append("<project path=\"foo\" name=\"pin-with-upstream\"")
+				.append("  revision=\"9b2fe85c0279f4d5ac69f07ddcd48566c3555405\"")
+				.append("  upstream=\"branchX\"/>")
+				.append("<project path=\"bar\" name=\"pin-without-upstream\"")
+				.append("  revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+				.append("</manifest>");
+
+		ManifestParser parser = new ManifestParser(null, null, "master",
+				"https://git.google.com/", null, null);
+		parser.read(new ByteArrayInputStream(
+				xmlContent.toString().getBytes(UTF_8)));
+
+		Map<String, RepoProject> repos = parser.getProjects().stream().collect(
+				Collectors.toMap(RepoProject::getName, Function.identity()));
+		assertEquals(2, repos.size());
+
+		RepoProject foo = repos.get("pin-with-upstream");
+		assertEquals("pin-with-upstream", foo.getName());
+		assertEquals("9b2fe85c0279f4d5ac69f07ddcd48566c3555405",
+				foo.getRevision());
+		assertEquals("branchX", foo.getUpstream());
+
+		RepoProject bar = repos.get("pin-without-upstream");
+		assertEquals("pin-without-upstream", bar.getName());
+		assertEquals("76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0",
+				bar.getRevision());
+		assertNull(bar.getUpstream());
+	}
+
+	@Test
+	public void testWithDestBranch() throws Exception {
+		StringBuilder xmlContent = new StringBuilder();
+		xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+				.append("<manifest>")
+				.append("<remote name=\"remote1\" fetch=\".\" />")
+				.append("<default revision=\"master\" remote=\"remote1\" />")
+				.append("<project path=\"foo\" name=\"foo\"")
+				.append("  dest-branch=\"branchX\"/>")
+				.append("<project path=\"bar\" name=\"bar\"/>")
+				.append("</manifest>");
+
+		ManifestParser parser = new ManifestParser(null, null, "master",
+				"https://git.google.com/", null, null);
+		parser.read(new ByteArrayInputStream(
+				xmlContent.toString().getBytes(UTF_8)));
+
+		Map<String, RepoProject> repos = parser.getProjects().stream().collect(
+				Collectors.toMap(RepoProject::getName, Function.identity()));
+		assertEquals(2, repos.size());
+
+		RepoProject foo = repos.get("foo");
+		assertEquals("foo", foo.getName());
+		assertEquals("branchX", foo.getDestBranch());
+
+		RepoProject bar = repos.get("bar");
+		assertEquals("bar", bar.getName());
+		assertNull(bar.getDestBranch());
+	}
+
 	void testNormalize(String in, String want) {
 		URI got = ManifestParser.normalizeEmptyPath(URI.create(in));
 		if (!got.toString().equals(want)) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
index ca6f2e1..3162e79 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
@@ -1171,6 +1171,94 @@ public void testRecordRemoteBranch() throws Exception {
 		}
 	}
 
+	@Test
+	public void testRecordRemoteBranch_pinned() throws Exception {
+		Repository remoteDb = createBareRepository();
+		Repository tempDb = createWorkRepository();
+
+		StringBuilder xmlContent = new StringBuilder();
+		xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+				.append("<manifest>")
+				.append("<remote name=\"remote1\" fetch=\".\" />")
+				.append("<default revision=\"master\" remote=\"remote1\" />")
+				.append("<project path=\"pin-noupstream\"")
+				.append("  name=\"pin-noupstream\"")
+				.append("  revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+				.append("<project path=\"pin-upstream\"")
+				.append("  name=\"pin-upstream\"")
+				.append("  upstream=\"branchX\"")
+				.append("  revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+				.append("</manifest>");
+		JGitTestUtil.writeTrashFile(tempDb, "manifest.xml",
+				xmlContent.toString());
+
+		RepoCommand command = new RepoCommand(remoteDb);
+		command.setPath(
+				tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+				.setURI(rootUri).setRecordRemoteBranch(true).call();
+		// Clone it
+		File directory = createTempDirectory("testBareRepo");
+		try (Repository localDb = Git.cloneRepository().setDirectory(directory)
+				.setURI(remoteDb.getDirectory().toURI().toString()).call()
+				.getRepository();) {
+			// The .gitmodules file should exist
+			File gitmodules = new File(localDb.getWorkTree(), ".gitmodules");
+			assertTrue("The .gitmodules file should exist",
+					gitmodules.exists());
+			FileBasedConfig c = new FileBasedConfig(gitmodules, FS.DETECTED);
+			c.load();
+			assertEquals("Pinned submodule with upstream records the ref",
+					"branchX", c.getString("submodule", "pin-upstream", "ref"));
+			assertNull("Pinned submodule without upstream don't have ref",
+					c.getString("submodule", "pin-noupstream", "ref"));
+		}
+	}
+
+	@Test
+	public void testRecordRemoteBranch_pinned_nameConflict() throws Exception {
+		Repository remoteDb = createBareRepository();
+		Repository tempDb = createWorkRepository();
+
+		StringBuilder xmlContent = new StringBuilder();
+		xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+				.append("<manifest>")
+				.append("<remote name=\"remote1\" fetch=\".\" />")
+				.append("<default revision=\"master\" remote=\"remote1\" />")
+				.append("<project path=\"pin-upstream\"")
+				.append("  name=\"pin-upstream\"")
+				.append("  upstream=\"branchX\"")
+				.append("  revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+				.append("<project path=\"pin-upstream-name-conflict\"")
+				.append("  name=\"pin-upstream\"")
+				.append("  upstream=\"branchX\"")
+				.append("  revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+				.append("</manifest>");
+		JGitTestUtil.writeTrashFile(tempDb, "manifest.xml",
+				xmlContent.toString());
+
+		RepoCommand command = new RepoCommand(remoteDb);
+		command.setPath(
+				tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+				.setURI(rootUri).setRecordRemoteBranch(true).call();
+		// Clone it
+		File directory = createTempDirectory("testBareRepo");
+		try (Repository localDb = Git.cloneRepository().setDirectory(directory)
+				.setURI(remoteDb.getDirectory().toURI().toString()).call()
+				.getRepository();) {
+			// The .gitmodules file should exist
+			File gitmodules = new File(localDb.getWorkTree(), ".gitmodules");
+			assertTrue("The .gitmodules file should exist",
+					gitmodules.exists());
+			FileBasedConfig c = new FileBasedConfig(gitmodules, FS.DETECTED);
+			c.load();
+			assertEquals("Upstream is preserved in name conflict", "branchX",
+					c.getString("submodule", "pin-upstream/pin-upstream",
+							"ref"));
+			assertEquals("Upstream is preserved in name conflict (other side)",
+					"branchX", c.getString("submodule",
+							"pin-upstream/pin-upstream-name-conflict", "ref"));
+		}
+	}
 
 	@Test
 	public void testRecordSubmoduleLabels() throws Exception {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java
index 9f65ee2..80a0f0c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java
@@ -10,6 +10,7 @@
 
 package org.eclipse.jgit.internal.storage.commitgraph;
 
+import static java.util.stream.Collectors.toList;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.junit.Assert.assertArrayEquals;
@@ -19,8 +20,12 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 
 import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -413,6 +418,27 @@ public void testReuseBloomFilters() throws Exception {
 				"119,69,63,-8,0,"));
 	}
 
+	@Test
+	public void testPathDiffCalculator_skipUnchangedTree() throws Exception {
+		RevCommit root = tr.commit(tr.tree(
+				tr.file("d/sd1/f1", tr.blob("f1")),
+				tr.file("d/sd2/f2", tr.blob("f2"))));
+		RevCommit tip = tr.commit(tr.tree(
+				tr.file("d/sd1/f1", tr.blob("f1")),
+				tr.file("d/sd2/f2", tr.blob("f2B"))), root);
+		CommitGraphWriter.PathDiffCalculator c = new CommitGraphWriter.PathDiffCalculator();
+
+		Optional<HashSet<ByteBuffer>> byteBuffers = c.changedPaths(walk.getObjectReader(), tip);
+
+		assertTrue(byteBuffers.isPresent());
+		List<String> asString = byteBuffers.get().stream()
+				.map(b -> StandardCharsets.UTF_8.decode(b).toString())
+				.collect(toList());
+		assertThat(asString, containsInAnyOrder("d", "d/sd2", "d/sd2/f2"));
+		// We don't walk into d/sd1/f1
+		assertEquals(1, c.stepCounter);
+	}
+
 	RevCommit commit(RevCommit... parents) throws Exception {
 		return tr.commit(parents);
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java
new file mode 100644
index 0000000..2c4b432
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2024, Google LLC 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
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertArrayEquals;
+
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.junit.Test;
+
+public class AggregatedBlockCacheStatsTest {
+	@Test
+	public void getName() {
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of());
+
+		assertThat(aggregatedBlockCacheStats.getName(),
+				equalTo(AggregatedBlockCacheStats.class.getName()));
+	}
+
+	@Test
+	public void getCurrentSize_aggregatesCurrentSizes() {
+		long[] currentSizes = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		packStats.addToLiveBytes(new TestKey(PackExt.PACK), 5);
+		currentSizes[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		bitmapStats.addToLiveBytes(new TestKey(PackExt.BITMAP_INDEX), 6);
+		currentSizes[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		indexStats.addToLiveBytes(new TestKey(PackExt.INDEX), 7);
+		currentSizes[PackExt.INDEX.getPosition()] = 7;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getCurrentSize(),
+				currentSizes);
+	}
+
+	@Test
+	public void getHitCount_aggregatesHitCounts() {
+		long[] hitCounts = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+		hitCounts[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> bitmapStats
+				.incrementHit(new TestKey(PackExt.BITMAP_INDEX)));
+		hitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementHit(new TestKey(PackExt.INDEX)));
+		hitCounts[PackExt.INDEX.getPosition()] = 7;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getHitCount(), hitCounts);
+	}
+
+	@Test
+	public void getMissCount_aggregatesMissCounts() {
+		long[] missCounts = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementMiss(new TestKey(PackExt.PACK)));
+		missCounts[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> bitmapStats
+				.incrementMiss(new TestKey(PackExt.BITMAP_INDEX)));
+		missCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+		missCounts[PackExt.INDEX.getPosition()] = 7;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getMissCount(), missCounts);
+	}
+
+	@Test
+	public void getTotalRequestCount_aggregatesRequestCounts() {
+		long[] totalRequestCounts = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5, () -> {
+			packStats.incrementHit(new TestKey(PackExt.PACK));
+			packStats.incrementMiss(new TestKey(PackExt.PACK));
+		});
+		totalRequestCounts[PackExt.PACK.getPosition()] = 10;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> {
+			bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+			bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+		});
+		totalRequestCounts[PackExt.BITMAP_INDEX.getPosition()] = 12;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7, () -> {
+			indexStats.incrementHit(new TestKey(PackExt.INDEX));
+			indexStats.incrementMiss(new TestKey(PackExt.INDEX));
+		});
+		totalRequestCounts[PackExt.INDEX.getPosition()] = 14;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getTotalRequestCount(),
+				totalRequestCounts);
+	}
+
+	@Test
+	public void getHitRatio_aggregatesHitRatios() {
+		long[] hitRatios = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+		hitRatios[PackExt.PACK.getPosition()] = 100;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> {
+			bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+			bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+		});
+		hitRatios[PackExt.BITMAP_INDEX.getPosition()] = 50;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+		hitRatios[PackExt.INDEX.getPosition()] = 0;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getHitRatio(), hitRatios);
+	}
+
+	@Test
+	public void getEvictions_aggregatesEvictions() {
+		long[] evictions = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementEvict(new TestKey(PackExt.PACK)));
+		evictions[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> bitmapStats
+				.incrementEvict(new TestKey(PackExt.BITMAP_INDEX)));
+		evictions[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementEvict(new TestKey(PackExt.INDEX)));
+		evictions[PackExt.INDEX.getPosition()] = 7;
+
+		BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+				.fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+		assertArrayEquals(aggregatedBlockCacheStats.getEvictions(), evictions);
+	}
+
+	private static void incrementCounter(int amount, Runnable fn) {
+		for (int i = 0; i < amount; i++) {
+			fn.run();
+		}
+	}
+
+	private static long[] createEmptyStatsArray() {
+		return new long[PackExt.values().length];
+	}
+
+	private static class TestKey extends DfsStreamKey {
+		TestKey(PackExt packExt) {
+			super(0, packExt);
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			return false;
+		}
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java
new file mode 100644
index 0000000..2e2f86b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java
@@ -0,0 +1,67 @@
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DEFAULT_NAME;
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.isA;
+
+import java.util.List;
+
+import org.junit.Test;
+
+public class ClockBlockCacheTableTest {
+	private static final String NAME = "name";
+
+	@Test
+	public void getName_nameNotConfigured_returnsDefaultName() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig());
+
+		assertThat(cacheTable.getName(), equalTo(DEFAULT_NAME));
+	}
+
+	@Test
+	public void getName_nameConfigured_returnsConfiguredName() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig().setName(NAME));
+
+		assertThat(cacheTable.getName(), equalTo(NAME));
+	}
+
+	@Test
+	public void getBlockCacheStats_nameNotConfigured_returnsBlockCacheStatsWithDefaultName() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig());
+
+		assertThat(cacheTable.getBlockCacheStats(), hasSize(1));
+		assertThat(cacheTable.getBlockCacheStats().get(0).getName(),
+				equalTo(DEFAULT_NAME));
+	}
+
+	@Test
+	public void getBlockCacheStats_nameConfigured_returnsBlockCacheStatsWithConfiguredName() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig().setName(NAME));
+
+		assertThat(cacheTable.getBlockCacheStats(), hasSize(1));
+		assertThat(cacheTable.getBlockCacheStats().get(0).getName(),
+				equalTo(NAME));
+	}
+
+	@Test
+	public void getAllBlockCacheStats() {
+		ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+				createBlockCacheConfig());
+
+		List<BlockCacheStats> blockCacheStats = cacheTable.getBlockCacheStats();
+		assertThat(blockCacheStats, contains(isA(BlockCacheStats.class)));
+	}
+
+	private static DfsBlockCacheConfig createBlockCacheConfig() {
+		return new DfsBlockCacheConfig().setBlockSize(512)
+				.setConcurrencyLevel(4).setBlockLimit(1024);
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java
index 2df0ba1..afa3179 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java
@@ -38,13 +38,37 @@
 
 package org.eclipse.jgit.internal.storage.dfs;
 
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DEFAULT_NAME;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_CACHE_PREFIX;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_LIMIT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_SIZE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CONCURRENCY_LEVEL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACK_EXTENSIONS;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.closeTo;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertThrows;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
+@SuppressWarnings("boxing")
 public class DfsBlockCacheConfigTest {
 
 	@Test
@@ -55,7 +79,6 @@ public void blockSizeNotPowerOfTwoExpectsException() {
 	}
 
 	@Test
-	@SuppressWarnings("boxing")
 	public void negativeBlockSizeIsConvertedToDefault() {
 		DfsBlockCacheConfig config = new DfsBlockCacheConfig();
 		config.setBlockSize(-1);
@@ -64,7 +87,6 @@ public void negativeBlockSizeIsConvertedToDefault() {
 	}
 
 	@Test
-	@SuppressWarnings("boxing")
 	public void tooSmallBlockSizeIsConvertedToDefault() {
 		DfsBlockCacheConfig config = new DfsBlockCacheConfig();
 		config.setBlockSize(10);
@@ -73,11 +95,295 @@ public void tooSmallBlockSizeIsConvertedToDefault() {
 	}
 
 	@Test
-	@SuppressWarnings("boxing")
 	public void validBlockSize() {
 		DfsBlockCacheConfig config = new DfsBlockCacheConfig();
 		config.setBlockSize(65536);
 
 		assertThat(config.getBlockSize(), is(65536));
 	}
+
+	@Test
+	public void fromConfigs() {
+		Config config = new Config();
+		config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_BLOCK_LIMIT, 50 * 1024);
+		config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_BLOCK_SIZE, 1024);
+		config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_CONCURRENCY_LEVEL, 3);
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_STREAM_RATIO, "0.5");
+
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		assertThat(cacheConfig.getBlockLimit(), is(50L * 1024L));
+		assertThat(cacheConfig.getBlockSize(), is(1024));
+		assertThat(cacheConfig.getConcurrencyLevel(), is(3));
+		assertThat(cacheConfig.getStreamRatio(), closeTo(0.5, 0.0001));
+	}
+
+	@Test
+	public void fromConfig_blockLimitNotAMultipleOfBlockSize_throws() {
+		Config config = new Config();
+		config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_BLOCK_LIMIT, 1025);
+		config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_BLOCK_SIZE, 1024);
+
+		assertThrows(IllegalArgumentException.class,
+				() -> new DfsBlockCacheConfig().fromConfig(config));
+	}
+
+	@Test
+	public void fromConfig_streamRatioInvalidFormat_throws() {
+		Config config = new Config();
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_STREAM_RATIO, "0.a5");
+
+		assertThrows(IllegalArgumentException.class,
+				() -> new DfsBlockCacheConfig().fromConfig(config));
+	}
+
+	@Test
+	public void fromConfig_generatesDfsBlockCachePackExtConfigs() {
+		Config config = new Config();
+		addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+				/* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+		addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX),
+				/* blockLimit= */ 25 * 1024, /* blockSize= */ 1024);
+
+		addPackExtConfigEntry(config, "index",
+				List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX,
+						PackExt.REVERSE_INDEX),
+				/* blockLimit= */ 30 * 1024, /* blockSize= */ 1024);
+
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		var configs = cacheConfig.getPackExtCacheConfigurations();
+		assertThat(configs, hasSize(3));
+		var packConfig = getConfigForExt(configs, PackExt.PACK);
+		assertThat(packConfig.getBlockLimit(), is(20L * 512L));
+		assertThat(packConfig.getBlockSize(), is(512));
+
+		var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX);
+		assertThat(bitmapConfig.getBlockLimit(), is(25L * 1024L));
+		assertThat(bitmapConfig.getBlockSize(), is(1024));
+
+		var indexConfig = getConfigForExt(configs, PackExt.INDEX);
+		assertThat(indexConfig.getBlockLimit(), is(30L * 1024L));
+		assertThat(indexConfig.getBlockSize(), is(1024));
+		assertThat(getConfigForExt(configs, PackExt.OBJECT_SIZE_INDEX),
+				is(indexConfig));
+		assertThat(getConfigForExt(configs, PackExt.REVERSE_INDEX),
+				is(indexConfig));
+	}
+
+	@Test
+	public void fromConfig_withExistingCacheHotMap_configWithPackExtConfigsHasHotMaps() {
+		Config config = new Config();
+		addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+				/* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+		addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX),
+				/* blockLimit= */ 25 * 1024, /* blockSize= */ 1024);
+
+		addPackExtConfigEntry(config, "index",
+				List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX,
+						PackExt.REVERSE_INDEX),
+				/* blockLimit= */ 30 * 1024, /* blockSize= */ 1024);
+
+		Map<PackExt, Integer> cacheHotMap = Map.of(PackExt.PACK, 1,
+				PackExt.BITMAP_INDEX, 2, PackExt.INDEX, 3, PackExt.REFTABLE, 4);
+
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig();
+		cacheConfig.setCacheHotMap(cacheHotMap);
+		cacheConfig.fromConfig(config);
+
+		var configs = cacheConfig.getPackExtCacheConfigurations();
+		assertThat(cacheConfig.getCacheHotMap(), is(cacheHotMap));
+		assertThat(configs, hasSize(3));
+		var packConfig = getConfigForExt(configs, PackExt.PACK);
+		assertThat(packConfig.getCacheHotMap(), is(Map.of(PackExt.PACK, 1)));
+
+		var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX);
+		assertThat(bitmapConfig.getCacheHotMap(),
+				is(Map.of(PackExt.BITMAP_INDEX, 2)));
+
+		var indexConfig = getConfigForExt(configs, PackExt.INDEX);
+		assertThat(indexConfig.getCacheHotMap(), is(Map.of(PackExt.INDEX, 3)));
+	}
+
+	@Test
+	public void setCacheHotMap_configWithPackExtConfigs_setsHotMaps() {
+		Config config = new Config();
+		addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+				/* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+		addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX),
+				/* blockLimit= */ 25 * 1024, /* blockSize= */ 1024);
+
+		addPackExtConfigEntry(config, "index",
+				List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX,
+						PackExt.REVERSE_INDEX),
+				/* blockLimit= */ 30 * 1024, /* blockSize= */ 1024);
+
+		Map<PackExt, Integer> cacheHotMap = Map.of(PackExt.PACK, 1,
+				PackExt.BITMAP_INDEX, 2, PackExt.INDEX, 3, PackExt.REFTABLE, 4);
+
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		cacheConfig.setCacheHotMap(cacheHotMap);
+
+		var configs = cacheConfig.getPackExtCacheConfigurations();
+		assertThat(cacheConfig.getCacheHotMap(), is(cacheHotMap));
+		assertThat(configs, hasSize(3));
+		var packConfig = getConfigForExt(configs, PackExt.PACK);
+		assertThat(packConfig.getCacheHotMap(), is(Map.of(PackExt.PACK, 1)));
+
+		var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX);
+		assertThat(bitmapConfig.getCacheHotMap(),
+				is(Map.of(PackExt.BITMAP_INDEX, 2)));
+
+		var indexConfig = getConfigForExt(configs, PackExt.INDEX);
+		assertThat(indexConfig.getCacheHotMap(), is(Map.of(PackExt.INDEX, 3)));
+	}
+
+	@Test
+	public void fromConfigs_baseConfigOnly_nameSetFromConfigDfsSubSection() {
+		Config config = new Config();
+
+		DfsBlockCacheConfig blockCacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		assertThat(blockCacheConfig.getName(), equalTo(DEFAULT_NAME));
+	}
+
+	@Test
+	public void fromConfigs_namesSetFromConfigDfsCachePrefixSubSections() {
+		Config config = new Config();
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_STREAM_RATIO, "0.5");
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "name1",
+				CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name());
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "name2",
+				CONFIG_KEY_PACK_EXTENSIONS, PackExt.BITMAP_INDEX.name());
+
+		DfsBlockCacheConfig blockCacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		assertThat(blockCacheConfig.getName(), equalTo("dfs"));
+		assertThat(
+				blockCacheConfig.getPackExtCacheConfigurations().get(0)
+						.getPackExtCacheConfiguration().getName(),
+				equalTo("dfs.name1"));
+		assertThat(
+				blockCacheConfig.getPackExtCacheConfigurations().get(1)
+						.getPackExtCacheConfiguration().getName(),
+				equalTo("dfs.name2"));
+	}
+
+	@Test
+	public void fromConfigs_dfsBlockCachePackExtConfigWithDuplicateExtensions_throws() {
+		Config config = new Config();
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1",
+				CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name());
+
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack2",
+				CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name());
+
+		assertThrows(IllegalArgumentException.class,
+				() -> new DfsBlockCacheConfig().fromConfig(config));
+	}
+
+	@Test
+	public void fromConfigs_dfsBlockCachePackExtConfigWithEmptyExtensions_throws() {
+		Config config = new Config();
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1",
+				CONFIG_KEY_PACK_EXTENSIONS, "");
+
+		assertThrows(IllegalArgumentException.class,
+				() -> new DfsBlockCacheConfig().fromConfig(config));
+	}
+
+	@Test
+	public void fromConfigs_dfsBlockCachePackExtConfigWithNoExtensions_throws() {
+		Config config = new Config();
+		config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1",
+				CONFIG_KEY_BLOCK_SIZE, 0);
+
+		assertThrows(IllegalArgumentException.class,
+				() -> new DfsBlockCacheConfig().fromConfig(config));
+	}
+
+	@Test
+	public void fromConfigs_dfsBlockCachePackExtConfigWithUnknownExtensions_throws() {
+		Config config = new Config();
+		config.setString(CONFIG_CORE_SECTION,
+				CONFIG_DFS_CACHE_PREFIX + "unknownExt",
+				CONFIG_KEY_PACK_EXTENSIONS, "NotAKnownExt");
+
+		assertThrows(IllegalArgumentException.class,
+				() -> new DfsBlockCacheConfig().fromConfig(config));
+	}
+
+	@Test
+	public void writeConfigurationDebug_writesConfigsToWriter()
+			throws Exception {
+		Config config = new Config();
+		config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_BLOCK_LIMIT, 50 * 1024);
+		config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_BLOCK_SIZE, 1024);
+		config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_CONCURRENCY_LEVEL, 3);
+		config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+				CONFIG_KEY_STREAM_RATIO, "0.5");
+		addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+				/* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+				.fromConfig(config);
+		Map<PackExt, Integer> hotmap = Map.of(PackExt.PACK, 10);
+		cacheConfig.setCacheHotMap(hotmap);
+
+		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+		cacheConfig.print(new PrintWriter(byteArrayOutputStream, true,
+				StandardCharsets.UTF_8));
+
+		String writenConfig = byteArrayOutputStream
+				.toString(StandardCharsets.UTF_8);
+
+		List<String> writenLines = Arrays.asList(writenConfig.split("\n"));
+		assertThat(writenLines,
+				equalTo(List.of("Name: dfs", "  BlockLimit: " + (50 * 1024),
+						"  BlockSize: 1024", "  StreamRatio: 0.5",
+						"  ConcurrencyLevel: 3",
+						"  CacheHotMapEntry: " + PackExt.PACK + " : " + 10,
+						"  Name: dfs.pack", "    BlockLimit: " + 20 * 512,
+						"    BlockSize: 512", "    StreamRatio: 0.3",
+						"    ConcurrencyLevel: 32",
+						"    CacheHotMapEntry: " + PackExt.PACK + " : " + 10,
+						"    PackExts: " + List.of(PackExt.PACK))));
+	}
+
+	private static void addPackExtConfigEntry(Config config, String configName,
+			List<PackExt> packExts, long blockLimit, int blockSize) {
+		String packExtConfigName = CONFIG_DFS_CACHE_PREFIX + configName;
+		config.setString(CONFIG_CORE_SECTION, packExtConfigName,
+				CONFIG_KEY_PACK_EXTENSIONS, packExts.stream().map(PackExt::name)
+						.collect(Collectors.joining(" ")));
+		config.setLong(CONFIG_CORE_SECTION, packExtConfigName,
+				CONFIG_KEY_BLOCK_LIMIT, blockLimit);
+		config.setInt(CONFIG_CORE_SECTION, packExtConfigName,
+				CONFIG_KEY_BLOCK_SIZE, blockSize);
+	}
+
+	private static DfsBlockCacheConfig getConfigForExt(
+			List<DfsBlockCachePackExtConfig> configs, PackExt packExt) {
+		for (DfsBlockCachePackExtConfig config : configs) {
+			if (config.getPackExts().contains(packExt)) {
+				return config.getPackExtCacheConfiguration();
+			}
+		}
+		return null;
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
index fef0563..3c7cc07 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
@@ -13,20 +13,24 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.LongStream;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.LongStream;
 
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig;
 import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.IndexEventConsumer;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.junit.TestRepository;
@@ -39,14 +43,35 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
+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 DfsBlockCacheTest {
 	@Rule
 	public TestName testName = new TestName();
+
 	private TestRng rng;
+
 	private DfsBlockCache cache;
+
 	private ExecutorService pool;
 
+	private enum CacheType {
+		SINGLE_TABLE_CLOCK_BLOCK_CACHE, EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE
+	}
+
+	@Parameters(name = "cache type: {0}")
+	public static Iterable<? extends Object> data() {
+		return Arrays.asList(CacheType.SINGLE_TABLE_CLOCK_BLOCK_CACHE,
+				CacheType.EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE);
+	}
+
+	@Parameter
+	public CacheType cacheType;
+
 	@Before
 	public void setUp() {
 		rng = new TestRng(testName.getMethodName());
@@ -448,8 +473,28 @@ private void resetCache() {
 	}
 
 	private void resetCache(int concurrencyLevel) {
-		DfsBlockCache.reconfigure(new DfsBlockCacheConfig().setBlockSize(512)
-				.setConcurrencyLevel(concurrencyLevel).setBlockLimit(1 << 20));
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+				.setBlockSize(512).setConcurrencyLevel(concurrencyLevel)
+				.setBlockLimit(1 << 20);
+		switch (cacheType) {
+		case SINGLE_TABLE_CLOCK_BLOCK_CACHE:
+			// SINGLE_TABLE_CLOCK_BLOCK_CACHE doesn't modify the config.
+			break;
+		case EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE:
+			List<DfsBlockCachePackExtConfig> packExtCacheConfigs = new ArrayList<>();
+			for (PackExt packExt : PackExt.values()) {
+				DfsBlockCacheConfig extCacheConfig = new DfsBlockCacheConfig()
+						.setBlockSize(512).setConcurrencyLevel(concurrencyLevel)
+						.setBlockLimit(1 << 20)
+						.setPackExtCacheConfigurations(packExtCacheConfigs);
+				packExtCacheConfigs.add(new DfsBlockCachePackExtConfig(
+						EnumSet.of(packExt), extCacheConfig));
+			}
+			cacheConfig.setPackExtCacheConfigurations(packExtCacheConfigs);
+			break;
+		}
+		assertNotNull(cacheConfig);
+		DfsBlockCache.reconfigure(cacheConfig);
 		cache = DfsBlockCache.getInstance();
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
index e193de9..00a3760 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
@@ -6,6 +6,7 @@
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -15,14 +16,18 @@
 import static org.junit.Assert.fail;
 
 import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.concurrent.TimeUnit;
+
 import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph;
 import org.eclipse.jgit.internal.storage.commitgraph.CommitGraphWriter;
 import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
 import org.eclipse.jgit.internal.storage.file.PackBitmapIndex;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.reftable.LogCursor;
 import org.eclipse.jgit.internal.storage.reftable.RefCursor;
 import org.eclipse.jgit.internal.storage.reftable.ReftableConfig;
 import org.eclipse.jgit.internal.storage.reftable.ReftableReader;
@@ -36,6 +41,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevBlob;
@@ -43,6 +49,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.GitTimeParser;
 import org.eclipse.jgit.util.SystemReader;
 import org.junit.After;
 import org.junit.Before;
@@ -1171,6 +1178,7 @@ public void objectSizeIdx_reachableBlob_bigEnough_indexed() throws Exception {
 
 		gcWithObjectSizeIndex(10);
 
+		odb.getReaderOptions().setUseObjectSizeIndex(true);
 		DfsReader reader = odb.newReader();
 		DfsPackFile gcPack = findFirstBySource(odb.getPacks(), GC);
 		assertTrue(gcPack.hasObjectSizeIndex(reader));
@@ -1191,6 +1199,7 @@ public void objectSizeIdx_reachableBlob_tooSmall_notIndexed() throws Exception {
 
 		gcWithObjectSizeIndex(10);
 
+		odb.getReaderOptions().setUseObjectSizeIndex(true);
 		DfsReader reader = odb.newReader();
 		DfsPackFile gcPack = findFirstBySource(odb.getPacks(), GC);
 		assertTrue(gcPack.hasObjectSizeIndex(reader));
@@ -1272,6 +1281,87 @@ public void bitmapIndexWrittenDuringGc() throws Exception {
 				bitmapIndex.getXorBitmapCount() > 0);
 	}
 
+	@Test
+	public void gitGCWithRefLogExpire() throws Exception {
+		String master = "refs/heads/master";
+		RevCommit commit0 = commit().message("0").create();
+		RevCommit commit1 = commit().message("1").parent(commit0).create();
+		git.update(master, commit1);
+		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
+		gc.setReftableConfig(new ReftableConfig());
+		run(gc);
+		DfsPackDescription t1 = odb.newPack(INSERT);
+		Ref next = new ObjectIdRef.PeeledNonTag(Ref.Storage.LOOSE,
+				"refs/heads/next", commit0.copy());
+		Instant currentDay = Instant.now();
+		Instant ten_days_ago = GitTimeParser.parseInstant("10 days ago");
+		Instant twenty_days_ago = GitTimeParser.parseInstant("20 days ago");
+		Instant thirty_days_ago = GitTimeParser.parseInstant("30 days ago");
+		Instant fifty_days_ago = GitTimeParser.parseInstant("50 days ago");
+		final ZoneOffset offset = ZoneOffset.ofHours(-8);
+		PersonIdent who2 = new PersonIdent("J.Author", "authemail", currentDay,
+				offset);
+		PersonIdent who3 = new PersonIdent("J.Author", "authemail",
+				ten_days_ago, offset);
+		PersonIdent who4 = new PersonIdent("J.Author", "authemail",
+				twenty_days_ago, offset);
+		PersonIdent who5 = new PersonIdent("J.Author", "authemail",
+				thirty_days_ago, offset);
+		PersonIdent who6 = new PersonIdent("J.Author", "authemail",
+				fifty_days_ago, offset);
+
+		try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) {
+			ReftableWriter w = new ReftableWriter(out);
+			w.setMinUpdateIndex(42);
+			w.setMaxUpdateIndex(42);
+			w.begin();
+			w.sortAndWriteRefs(Collections.singleton(next));
+			w.writeLog("refs/heads/branch", 1, who2, ObjectId.zeroId(),id(2), "Branch Message");
+			w.writeLog("refs/heads/branch1", 2, who3, ObjectId.zeroId(),id(3), "Branch Message1");
+			w.writeLog("refs/heads/branch2", 2, who4, ObjectId.zeroId(),id(4), "Branch Message2");
+			w.writeLog("refs/heads/branch3", 2, who5, ObjectId.zeroId(),id(5), "Branch Message3");
+			w.writeLog("refs/heads/branch4", 2, who6, ObjectId.zeroId(),id(6), "Branch Message4");
+			w.finish();
+			t1.addFileExt(REFTABLE);
+			t1.setReftableStats(w.getStats());
+		}
+		odb.commitPack(Collections.singleton(t1), null);
+
+		gc = new DfsGarbageCollector(repo);
+		gc.setReftableConfig(new ReftableConfig());
+		// Expire ref log entries older than 30 days
+		gc.setRefLogExpire(thirty_days_ago);
+		run(gc);
+
+		// Single GC pack present with all objects.
+		assertEquals(1, odb.getPacks().length);
+		DfsPackFile pack = odb.getPacks()[0];
+		DfsPackDescription desc = pack.getPackDescription();
+
+		DfsReftable table = new DfsReftable(DfsBlockCache.getInstance(), desc);
+		try (DfsReader ctx = odb.newReader();
+			 ReftableReader rr = table.open(ctx);
+			 RefCursor rc = rr.allRefs();
+			 LogCursor lc = rr.allLogs()) {
+			assertTrue(rc.next());
+			assertEquals(master, rc.getRef().getName());
+			assertEquals(commit1, rc.getRef().getObjectId());
+			assertTrue(rc.next());
+			assertEquals(next.getName(), rc.getRef().getName());
+			assertEquals(commit0, rc.getRef().getObjectId());
+			assertFalse(rc.next());
+			assertTrue(lc.next());
+			assertEquals(lc.getRefName(),"refs/heads/branch");
+			assertTrue(lc.next());
+			assertEquals(lc.getRefName(),"refs/heads/branch1");
+			assertTrue(lc.next());
+			assertEquals(lc.getRefName(),"refs/heads/branch2");
+			// Old entries are purged
+			assertFalse(lc.next());
+		}
+	}
+
+
 	private RevCommit commitChain(RevCommit parent, int length)
 			throws Exception {
 		for (int i = 0; i < length; i++) {
@@ -1361,4 +1451,12 @@ private int countPacks(PackSource source) throws IOException {
 		}
 		return cnt;
 	}
+	private static ObjectId id(int i) {
+		byte[] buf = new byte[OBJECT_ID_LENGTH];
+		buf[0] = (byte) (i & 0xff);
+		buf[1] = (byte) ((i >>> 8) & 0xff);
+		buf[2] = (byte) ((i >>> 16) & 0xff);
+		buf[3] = (byte) (i >>> 24);
+		return ObjectId.fromRaw(buf);
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java
index b84a0b0..0b558ed 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java
@@ -295,6 +295,7 @@ public void testObjectSizePopulated() throws IOException {
 	public void testObjectSizeIndexOnInsert() throws IOException {
 		db.getConfig().setInt(CONFIG_PACK_SECTION, null,
 				CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 0);
+		db.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true);
 
 		byte[] contents = Constants.encode("foo");
 		ObjectId fooId;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java
index c516e30..c3b6aa8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java
@@ -12,13 +12,18 @@
 
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.COMPACT;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Optional;
 
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
@@ -98,6 +103,40 @@ public void testEstimateGcPackSizeWithAnExistingGcPack() throws Exception {
 				pack.getPackDescription().getEstimatedPackSize());
 	}
 
+	@Test
+	public void testObjectSizeIndexWritten() throws Exception {
+		writeObjectSizeIndex(repo, true);
+		RevCommit commit0 = commit().message("0").create();
+		RevCommit commit1 = commit().message("1").parent(commit0).create();
+		git.update("master", commit1);
+
+		compact();
+
+		Optional<DfsPackFile> compactPack = Arrays.stream(odb.getPacks())
+				.filter(pack -> pack.getPackDescription()
+						.getPackSource() == COMPACT)
+				.findFirst();
+		assertTrue(compactPack.isPresent());
+		assertTrue(compactPack.get().getPackDescription().hasFileExt(OBJECT_SIZE_INDEX));
+	}
+
+	@Test
+	public void testObjectSizeIndexNotWritten() throws Exception {
+		writeObjectSizeIndex(repo, false);
+		RevCommit commit0 = commit().message("0").create();
+		RevCommit commit1 = commit().message("1").parent(commit0).create();
+		git.update("master", commit1);
+
+		compact();
+
+		Optional<DfsPackFile> compactPack = Arrays.stream(odb.getPacks())
+				.filter(pack -> pack.getPackDescription()
+						.getPackSource() == COMPACT)
+				.findFirst();
+		assertTrue(compactPack.isPresent());
+		assertFalse(compactPack.get().getPackDescription().hasFileExt(OBJECT_SIZE_INDEX));
+	}
+
 	private TestRepository<InMemoryRepository>.CommitBuilder commit() {
 		return git.commit();
 	}
@@ -108,4 +147,9 @@ private void compact() throws IOException {
 		compactor.compact(null);
 		odb.clearCache();
 	}
+
+	private static void writeObjectSizeIndex(DfsRepository repo, boolean should) {
+		repo.getConfig().setInt(ConfigConstants.CONFIG_PACK_SECTION, null,
+				ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, should ? 0 : -1);
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java
index d21e51f..9680019 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java
@@ -41,6 +41,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.Before;
 import org.junit.Test;
@@ -126,6 +127,7 @@ public void testLoadObjectSizeIndex() throws IOException {
 		setObjectSizeIndexMinBytes(0);
 		ObjectId blobId = setupPack(512, 800);
 
+		db.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true);
 		DfsReader reader = db.getObjectDatabase().newReader();
 		DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
 		assertTrue(pack.hasObjectSizeIndex(reader));
@@ -308,7 +310,7 @@ private ObjectId setupPack(int bs, int ps) throws IOException {
 
 	private void assertPackSize() throws IOException {
 		try (DfsReader ctx = db.getObjectDatabase().newReader();
-				PackWriter pw = new PackWriter(ctx);
+		     PackWriter pw = new PackWriter(new PackConfig(), ctx);
 				ByteArrayOutputStream os = new ByteArrayOutputStream();
 				PackOutputStream out = new PackOutputStream(
 						NullProgressMonitor.INSTANCE, os, pw)) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java
index 130af27..c1cd231 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java
@@ -61,6 +61,7 @@ public void parse_writeObjSizeIdx() throws IOException {
 			ins.flush();
 		}
 
+		repo.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true);
 		DfsReader reader = repo.getObjectDatabase().newReader();
 		PackList packList = repo.getObjectDatabase().getPackList();
 		assertEquals(1, packList.packs.length);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java
index 254184e..a0c2289 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java
@@ -37,6 +37,8 @@ public class DfsReaderTest {
 	@Before
 	public void setUp() {
 		db = new InMemoryRepository(new DfsRepositoryDescription("test"));
+		// These tests assume the object size index is enabled.
+		db.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true);
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java
new file mode 100644
index 0000000..e7627bc
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java
@@ -0,0 +1,679 @@
+/*
+ * Copyright (c) 2024, Google LLC 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
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.Ref;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.RefLoader;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+@SuppressWarnings({ "boxing", "unchecked" })
+public class PackExtBlockCacheTableTest {
+	private static final String CACHE_NAME = "CacheName";
+
+	@Test
+	public void fromBlockCacheConfigs_createsDfsPackExtBlockCacheTables() {
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig();
+		cacheConfig.setPackExtCacheConfigurations(
+				List.of(new DfsBlockCachePackExtConfig(EnumSet.of(PackExt.PACK),
+						new DfsBlockCacheConfig())));
+		assertNotNull(
+				PackExtBlockCacheTable.fromBlockCacheConfigs(cacheConfig));
+	}
+
+	@Test
+	public void fromBlockCacheConfigs_noPackExtConfigurationGiven_packExtCacheConfigurationsIsEmpty_throws() {
+		DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig();
+		cacheConfig.setPackExtCacheConfigurations(List.of());
+		assertThrows(IllegalArgumentException.class,
+				() -> PackExtBlockCacheTable
+						.fromBlockCacheConfigs(cacheConfig));
+	}
+
+	@Test
+	public void hasBlock0_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+		DfsStreamKey streamKey = new TestKey(PackExt.BITMAP_INDEX);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.hasBlock0(any(DfsStreamKey.class)))
+				.thenReturn(true);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertTrue(tables.hasBlock0(streamKey));
+	}
+
+	@Test
+	public void hasBlock0_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+		DfsStreamKey streamKey = new TestKey(PackExt.PACK);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.hasBlock0(any(DfsStreamKey.class)))
+				.thenReturn(true);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertTrue(tables.hasBlock0(streamKey));
+	}
+
+	@Test
+	public void getOrLoad_packExtMapsToCacheTable_callsBitmapIndexCacheTable()
+			throws Exception {
+		BlockBasedFile blockBasedFile = new BlockBasedFile(null,
+				mock(DfsPackDescription.class), PackExt.BITMAP_INDEX) {
+			// empty
+		};
+		DfsBlock dfsBlock = mock(DfsBlock.class);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.getOrLoad(any(BlockBasedFile.class),
+				anyLong(), any(DfsReader.class),
+				any(DfsBlockCache.ReadableChannelSupplier.class)))
+				.thenReturn(mock(DfsBlock.class));
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.getOrLoad(any(BlockBasedFile.class),
+				anyLong(), any(DfsReader.class),
+				any(DfsBlockCache.ReadableChannelSupplier.class)))
+				.thenReturn(dfsBlock);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(
+				tables.getOrLoad(blockBasedFile, 0, mock(DfsReader.class),
+						mock(DfsBlockCache.ReadableChannelSupplier.class)),
+				sameInstance(dfsBlock));
+	}
+
+	@Test
+	public void getOrLoad_packExtDoesNotMapToCacheTable_callsDefaultCache()
+			throws Exception {
+		BlockBasedFile blockBasedFile = new BlockBasedFile(null,
+				mock(DfsPackDescription.class), PackExt.PACK) {
+			// empty
+		};
+		DfsBlock dfsBlock = mock(DfsBlock.class);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.getOrLoad(any(BlockBasedFile.class),
+				anyLong(), any(DfsReader.class),
+				any(DfsBlockCache.ReadableChannelSupplier.class)))
+				.thenReturn(dfsBlock);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.getOrLoad(any(BlockBasedFile.class),
+				anyLong(), any(DfsReader.class),
+				any(DfsBlockCache.ReadableChannelSupplier.class)))
+				.thenReturn(mock(DfsBlock.class));
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(
+				tables.getOrLoad(blockBasedFile, 0, mock(DfsReader.class),
+						mock(DfsBlockCache.ReadableChannelSupplier.class)),
+				sameInstance(dfsBlock));
+	}
+
+	@Test
+	public void getOrLoadRef_packExtMapsToCacheTable_callsBitmapIndexCacheTable()
+			throws Exception {
+		Ref<Integer> ref = mock(Ref.class);
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.getOrLoadRef(any(DfsStreamKey.class),
+				anyLong(), any(RefLoader.class))).thenReturn(mock(Ref.class));
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.getOrLoadRef(any(DfsStreamKey.class),
+				anyLong(), any(RefLoader.class))).thenReturn(ref);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(tables.getOrLoadRef(dfsStreamKey, 0, mock(RefLoader.class)),
+				sameInstance(ref));
+	}
+
+	@Test
+	public void getOrLoadRef_packExtDoesNotMapToCacheTable_callsDefaultCache()
+			throws Exception {
+		Ref<Integer> ref = mock(Ref.class);
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.getOrLoadRef(any(DfsStreamKey.class),
+				anyLong(), any(RefLoader.class))).thenReturn(ref);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.getOrLoadRef(any(DfsStreamKey.class),
+				anyLong(), any(RefLoader.class))).thenReturn(mock(Ref.class));
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(tables.getOrLoadRef(dfsStreamKey, 0, mock(RefLoader.class)),
+				sameInstance(ref));
+	}
+
+	@Test
+	public void putDfsBlock_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+		DfsBlock dfsBlock = new DfsBlock(dfsStreamKey, 0, new byte[0]);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		tables.put(dfsBlock);
+		Mockito.verify(bitmapIndexCacheTable, times(1)).put(dfsBlock);
+	}
+
+	@Test
+	public void putDfsBlock_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+		DfsBlock dfsBlock = new DfsBlock(dfsStreamKey, 0, new byte[0]);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		tables.put(dfsBlock);
+		Mockito.verify(defaultBlockCacheTable, times(1)).put(dfsBlock);
+	}
+
+	@Test
+	public void putDfsStreamKey_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+		Ref<Integer> ref = mock(Ref.class);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.put(any(DfsStreamKey.class), anyLong(),
+				anyLong(), anyInt())).thenReturn(mock(Ref.class));
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.put(any(DfsStreamKey.class), anyLong(),
+				anyLong(), anyInt())).thenReturn(ref);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(tables.put(dfsStreamKey, 0, 0, 0), sameInstance(ref));
+	}
+
+	@Test
+	public void putDfsStreamKey_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+		Ref<Integer> ref = mock(Ref.class);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.put(any(DfsStreamKey.class), anyLong(),
+				anyLong(), anyInt())).thenReturn(ref);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.put(any(DfsStreamKey.class), anyLong(),
+				anyLong(), anyInt())).thenReturn(mock(Ref.class));
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(tables.put(dfsStreamKey, 0, 0, 0), sameInstance(ref));
+	}
+
+	@Test
+	public void putRef_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+		Ref<Integer> ref = mock(Ref.class);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.putRef(any(DfsStreamKey.class), anyLong(),
+				anyInt())).thenReturn(mock(Ref.class));
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.putRef(any(DfsStreamKey.class), anyLong(),
+				anyInt())).thenReturn(ref);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(tables.putRef(dfsStreamKey, 0, 0), sameInstance(ref));
+	}
+
+	@Test
+	public void putRef_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+		Ref<Integer> ref = mock(Ref.class);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.putRef(any(DfsStreamKey.class), anyLong(),
+				anyInt())).thenReturn(ref);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.putRef(any(DfsStreamKey.class), anyLong(),
+				anyInt())).thenReturn(mock(Ref.class));
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(tables.putRef(dfsStreamKey, 0, 0), sameInstance(ref));
+	}
+
+	@Test
+	public void contains_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+		DfsStreamKey streamKey = new TestKey(PackExt.BITMAP_INDEX);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.contains(any(DfsStreamKey.class), anyLong()))
+				.thenReturn(true);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertTrue(tables.contains(streamKey, 0));
+	}
+
+	@Test
+	public void contains_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+		DfsStreamKey streamKey = new TestKey(PackExt.PACK);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.contains(any(DfsStreamKey.class),
+				anyLong())).thenReturn(true);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertTrue(tables.contains(streamKey, 0));
+	}
+
+	@Test
+	public void get_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+		Ref<Integer> ref = mock(Ref.class);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.get(any(DfsStreamKey.class), anyLong()))
+				.thenReturn(mock(Ref.class));
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.get(any(DfsStreamKey.class), anyLong()))
+				.thenReturn(ref);
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(tables.get(dfsStreamKey, 0), sameInstance(ref));
+	}
+
+	@Test
+	public void get_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+		DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+		Ref<Integer> ref = mock(Ref.class);
+		DfsBlockCacheTable defaultBlockCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(defaultBlockCacheTable.get(any(DfsStreamKey.class), anyLong()))
+				.thenReturn(ref);
+		DfsBlockCacheTable bitmapIndexCacheTable = mock(
+				DfsBlockCacheTable.class);
+		when(bitmapIndexCacheTable.get(any(DfsStreamKey.class), anyLong()))
+				.thenReturn(mock(Ref.class));
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				defaultBlockCacheTable,
+				Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+		assertThat(tables.get(dfsStreamKey, 0), sameInstance(ref));
+	}
+
+	@Test
+	public void getName() {
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+				cacheTableWithStats(/* name= */ "defaultName", packStats),
+				Map.of(PackExt.PACK, cacheTableWithStats(/* name= */ "packName",
+						packStats)));
+
+		assertThat(tables.getName(), equalTo("defaultName,packName"));
+	}
+
+	@Test
+	public void getAllBlockCacheStats() {
+		String defaultTableName = "default table";
+		DfsBlockCacheStats defaultStats = new DfsBlockCacheStats(
+				defaultTableName);
+		incrementCounter(4,
+				() -> defaultStats.incrementHit(new TestKey(PackExt.REFTABLE)));
+
+		String packTableName = "pack table";
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats(packTableName);
+		incrementCounter(5,
+				() -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+
+		String bitmapTableName = "bitmap table";
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(
+				bitmapTableName);
+		incrementCounter(6, () -> bitmapStats
+				.incrementHit(new TestKey(PackExt.BITMAP_INDEX)));
+
+		DfsBlockCacheTable defaultTable = cacheTableWithStats(defaultStats);
+		DfsBlockCacheTable packTable = cacheTableWithStats(packStats);
+		DfsBlockCacheTable bitmapTable = cacheTableWithStats(bitmapStats);
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable
+				.fromCacheTables(defaultTable, Map.of(PackExt.PACK, packTable,
+						PackExt.BITMAP_INDEX, bitmapTable));
+
+		List<BlockCacheStats> statsList = tables.getBlockCacheStats();
+		assertThat(statsList, hasSize(3));
+
+		long[] defaultTableHitCounts = createEmptyStatsArray();
+		defaultTableHitCounts[PackExt.REFTABLE.getPosition()] = 4;
+		assertArrayEquals(
+				getCacheStatsByName(statsList, defaultTableName).getHitCount(),
+				defaultTableHitCounts);
+
+		long[] packTableHitCounts = createEmptyStatsArray();
+		packTableHitCounts[PackExt.PACK.getPosition()] = 5;
+		assertArrayEquals(
+				getCacheStatsByName(statsList, packTableName).getHitCount(),
+				packTableHitCounts);
+
+		long[] bitmapHitCounts = createEmptyStatsArray();
+		bitmapHitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+		assertArrayEquals(
+				getCacheStatsByName(statsList, bitmapTableName).getHitCount(),
+				bitmapHitCounts);
+	}
+
+	@Test
+	public void getBlockCacheStats_getCurrentSize_consolidatesAllTableCurrentSizes() {
+		long[] currentSizes = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		packStats.addToLiveBytes(new TestKey(PackExt.PACK), 5);
+		currentSizes[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		bitmapStats.addToLiveBytes(new TestKey(PackExt.BITMAP_INDEX), 6);
+		currentSizes[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		indexStats.addToLiveBytes(new TestKey(PackExt.INDEX), 7);
+		currentSizes[PackExt.INDEX.getPosition()] = 7;
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable
+				.fromCacheTables(cacheTableWithStats(packStats),
+						Map.of(PackExt.BITMAP_INDEX,
+								cacheTableWithStats(bitmapStats), PackExt.INDEX,
+								cacheTableWithStats(indexStats)));
+
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getCurrentSize(),
+				currentSizes);
+	}
+
+	@Test
+	public void getBlockCacheStats_GetHitCount_consolidatesAllTableHitCounts() {
+		long[] hitCounts = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+		hitCounts[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> bitmapStats
+				.incrementHit(new TestKey(PackExt.BITMAP_INDEX)));
+		hitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementHit(new TestKey(PackExt.INDEX)));
+		hitCounts[PackExt.INDEX.getPosition()] = 7;
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable
+				.fromCacheTables(cacheTableWithStats(packStats),
+						Map.of(PackExt.BITMAP_INDEX,
+								cacheTableWithStats(bitmapStats), PackExt.INDEX,
+								cacheTableWithStats(indexStats)));
+
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getHitCount(),
+				hitCounts);
+	}
+
+	@Test
+	public void getBlockCacheStats_getMissCount_consolidatesAllTableMissCounts() {
+		long[] missCounts = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementMiss(new TestKey(PackExt.PACK)));
+		missCounts[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> bitmapStats
+				.incrementMiss(new TestKey(PackExt.BITMAP_INDEX)));
+		missCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+		missCounts[PackExt.INDEX.getPosition()] = 7;
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable
+				.fromCacheTables(cacheTableWithStats(packStats),
+						Map.of(PackExt.BITMAP_INDEX,
+								cacheTableWithStats(bitmapStats), PackExt.INDEX,
+								cacheTableWithStats(indexStats)));
+
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getMissCount(),
+				missCounts);
+	}
+
+	@Test
+	public void getBlockCacheStats_getTotalRequestCount_consolidatesAllTableTotalRequestCounts() {
+		long[] totalRequestCounts = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5, () -> {
+			packStats.incrementHit(new TestKey(PackExt.PACK));
+			packStats.incrementMiss(new TestKey(PackExt.PACK));
+		});
+		totalRequestCounts[PackExt.PACK.getPosition()] = 10;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> {
+			bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+			bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+		});
+		totalRequestCounts[PackExt.BITMAP_INDEX.getPosition()] = 12;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7, () -> {
+			indexStats.incrementHit(new TestKey(PackExt.INDEX));
+			indexStats.incrementMiss(new TestKey(PackExt.INDEX));
+		});
+		totalRequestCounts[PackExt.INDEX.getPosition()] = 14;
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable
+				.fromCacheTables(cacheTableWithStats(packStats),
+						Map.of(PackExt.BITMAP_INDEX,
+								cacheTableWithStats(bitmapStats), PackExt.INDEX,
+								cacheTableWithStats(indexStats)));
+
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats())
+				.getTotalRequestCount(), totalRequestCounts);
+	}
+
+	@Test
+	public void getBlockCacheStats_getHitRatio_consolidatesAllTableHitRatios() {
+		long[] hitRatios = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+		hitRatios[PackExt.PACK.getPosition()] = 100;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> {
+			bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+			bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+		});
+		hitRatios[PackExt.BITMAP_INDEX.getPosition()] = 50;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+		hitRatios[PackExt.INDEX.getPosition()] = 0;
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable
+				.fromCacheTables(cacheTableWithStats(packStats),
+						Map.of(PackExt.BITMAP_INDEX,
+								cacheTableWithStats(bitmapStats), PackExt.INDEX,
+								cacheTableWithStats(indexStats)));
+
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getHitRatio(),
+				hitRatios);
+	}
+
+	@Test
+	public void getBlockCacheStats_getEvictions_consolidatesAllTableEvictions() {
+		long[] evictions = createEmptyStatsArray();
+
+		DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+		incrementCounter(5,
+				() -> packStats.incrementEvict(new TestKey(PackExt.PACK)));
+		evictions[PackExt.PACK.getPosition()] = 5;
+
+		DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+		incrementCounter(6, () -> bitmapStats
+				.incrementEvict(new TestKey(PackExt.BITMAP_INDEX)));
+		evictions[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+		DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+		incrementCounter(7,
+				() -> indexStats.incrementEvict(new TestKey(PackExt.INDEX)));
+		evictions[PackExt.INDEX.getPosition()] = 7;
+
+		PackExtBlockCacheTable tables = PackExtBlockCacheTable
+				.fromCacheTables(cacheTableWithStats(packStats),
+						Map.of(PackExt.BITMAP_INDEX,
+								cacheTableWithStats(bitmapStats), PackExt.INDEX,
+								cacheTableWithStats(indexStats)));
+
+		assertArrayEquals(AggregatedBlockCacheStats
+				.fromStatsList(tables.getBlockCacheStats()).getEvictions(),
+				evictions);
+	}
+
+	private BlockCacheStats getCacheStatsByName(
+			List<BlockCacheStats> blockCacheStats, String name) {
+		for (BlockCacheStats entry : blockCacheStats) {
+			if (entry.getName().equals(name)) {
+				return entry;
+			}
+		}
+		return null;
+	}
+
+	private static void incrementCounter(int amount, Runnable fn) {
+		for (int i = 0; i < amount; i++) {
+			fn.run();
+		}
+	}
+
+	private static long[] createEmptyStatsArray() {
+		return new long[PackExt.values().length];
+	}
+
+	private static DfsBlockCacheTable cacheTableWithStats(
+			BlockCacheStats dfsBlockCacheStats) {
+		return cacheTableWithStats(CACHE_NAME, dfsBlockCacheStats);
+	}
+
+	private static DfsBlockCacheTable cacheTableWithStats(String name,
+			BlockCacheStats dfsBlockCacheStats) {
+		DfsBlockCacheTable cacheTable = mock(DfsBlockCacheTable.class);
+		when(cacheTable.getName()).thenReturn(name);
+		when(cacheTable.getBlockCacheStats())
+				.thenReturn(List.of(dfsBlockCacheStats));
+		return cacheTable;
+	}
+
+	private static class TestKey extends DfsStreamKey {
+		TestKey(PackExt packExt) {
+			super(0, packExt);
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			return false;
+		}
+	}
+}
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 bd36337..41a33df 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
@@ -29,6 +29,7 @@
 
 import org.eclipse.jgit.errors.AmbiguousObjectException;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
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/BasePackWriterTest.java
similarity index 99%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackWriterTest.java
index 24a81b6..92d7465 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/BasePackWriterTest.java
@@ -66,7 +66,7 @@
 import org.junit.Test;
 import org.mockito.Mockito;
 
-public class PackWriterTest extends SampleDataRepositoryTestCase {
+public class BasePackWriterTest extends SampleDataRepositoryTestCase {
 
 	private static final List<RevObject> EMPTY_LIST_REVS = Collections
 			.<RevObject> emptyList();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
index daf4382..a0afc3e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
@@ -171,7 +171,7 @@ public void packedRefsFileIsSorted() throws IOException {
 			assertEquals(c2.getResult(), ReceiveCommand.Result.OK);
 		}
 
-		File packed = new File(diskRepo.getDirectory(), "packed-refs");
+		File packed = new File(diskRepo.getCommonDirectory(), "packed-refs");
 		String packedStr = new String(Files.readAllBytes(packed.toPath()),
 				UTF_8);
 
@@ -1263,7 +1263,7 @@ private Map<String, ReflogEntry> getLastReflogs(String... names)
 	}
 
 	private ReflogEntry getLastReflog(String name) throws IOException {
-		ReflogReader r = diskRepo.getReflogReader(name);
+		ReflogReader r = diskRepo.getRefDatabase().getReflogReader(name);
 		if (r == null) {
 			return null;
 		}
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 32342e3..5756b41 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
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -33,8 +34,15 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
-
 import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -51,6 +59,10 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
 import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
 import org.junit.Test;
 
 public class FileReftableTest extends SampleDataRepositoryTestCase {
@@ -66,6 +78,30 @@ public void setUp() throws Exception {
 
 	@SuppressWarnings("boxing")
 	@Test
+	public void testReloadIfNecessary() throws Exception {
+		ObjectId id = db.resolve("master");
+		try (FileRepository repo1 = new FileRepository(db.getDirectory());
+				FileRepository repo2 = new FileRepository(db.getDirectory())) {
+			((FileReftableDatabase) repo1.getRefDatabase())
+					.setAutoRefresh(true);
+			((FileReftableDatabase) repo2.getRefDatabase())
+					.setAutoRefresh(true);
+			FileRepository repos[] = { repo1, repo2 };
+			for (int i = 0; i < 10; i++) {
+				for (int j = 0; j < 2; j++) {
+					FileRepository repo = repos[j];
+					RefUpdate u = repo.getRefDatabase().newUpdate(
+							String.format("branch%d", i * 10 + j), false);
+					u.setNewObjectId(id);
+					RefUpdate.Result r = u.update();
+					assertEquals(Result.NEW, r);
+				}
+			}
+		}
+	}
+
+	@SuppressWarnings("boxing")
+	@Test
 	public void testRacyReload() throws Exception {
 		ObjectId id = db.resolve("master");
 		int retry = 0;
@@ -87,13 +123,61 @@ public void testRacyReload() throws Exception {
 
 						u.setNewObjectId(id);
 						r = u.update();
-						assertEquals(r, Result.NEW);
+						assertEquals(Result.NEW, r);
 					}
 				}
 			}
 
 			// only the first one succeeds
-			assertEquals(retry, 19);
+			assertEquals(19, retry);
+		}
+	}
+
+	@Test
+	public void testConcurrentRacyReload() throws Exception {
+		ObjectId id = db.resolve("master");
+		final CyclicBarrier barrier = new CyclicBarrier(2);
+
+		class UpdateRef implements Callable<RefUpdate.Result> {
+
+			private RefUpdate u;
+
+			UpdateRef(FileRepository repo, String branchName)
+					throws IOException {
+				u = repo.getRefDatabase().newUpdate(branchName,
+						false);
+				u.setNewObjectId(id);
+			}
+
+			@Override
+			public RefUpdate.Result call() throws Exception {
+				barrier.await(); // wait for the other thread to prepare
+				return u.update();
+			}
+		}
+
+		ExecutorService pool = Executors.newFixedThreadPool(2);
+		try (FileRepository repo1 = new FileRepository(db.getDirectory());
+				FileRepository repo2 = new FileRepository(db.getDirectory())) {
+			((FileReftableDatabase) repo1.getRefDatabase())
+					.setAutoRefresh(true);
+			((FileReftableDatabase) repo2.getRefDatabase())
+					.setAutoRefresh(true);
+			for (int i = 0; i < 10; i++) {
+				String branchName = String.format("branch%d",
+						Integer.valueOf(i));
+				Future<RefUpdate.Result> ru1 = pool
+						.submit(new UpdateRef(repo1, branchName));
+				Future<RefUpdate.Result> ru2 = pool
+						.submit(new UpdateRef(repo2, branchName));
+				assertTrue((ru1.get() == Result.NEW
+						&& ru2.get() == Result.LOCK_FAILURE)
+						|| (ru1.get() == Result.LOCK_FAILURE
+								&& ru2.get() == Result.NEW));
+			}
+		} finally {
+			pool.shutdown();
+			pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
 		}
 	}
 
@@ -105,13 +189,13 @@ public void testCompactFully() throws Exception {
 			RefUpdate u = db.updateRef("refs/heads/master");
 			u.setForceUpdate(true);
 			u.setNewObjectId((i%2) == 0 ? c1 : c2);
-			assertEquals(u.update(), FORCED);
+			assertEquals(FORCED, u.update());
 		}
 
 		File tableDir = new File(db.getDirectory(), Constants.REFTABLE);
 		assertTrue(tableDir.listFiles().length > 2);
 		((FileReftableDatabase)db.getRefDatabase()).compactFully();
-		assertEquals(tableDir.listFiles().length,2);
+		assertEquals(2, tableDir.listFiles().length);
 	}
 
 	@Test
@@ -171,9 +255,10 @@ public void testConvertToRefdirReflog() throws Exception {
 		v.update();
 
 		db.convertToPackedRefs(true, false);
-		List<ReflogEntry> logs = db.getReflogReader("refs/heads/master").getReverseEntries(2);
-		assertEquals(logs.get(0).getComment(), "banana");
-		assertEquals(logs.get(1).getComment(), "apple");
+		List<ReflogEntry> logs = db.getRefDatabase()
+				.getReflogReader("refs/heads/master").getReverseEntries(2);
+		assertEquals("banana", logs.get(0).getComment());
+		assertEquals("apple", logs.get(1).getComment());
 	}
 
 	@Test
@@ -185,8 +270,9 @@ public void testBatchrefUpdate() throws Exception {
 		ReceiveCommand rc1 = new ReceiveCommand(ObjectId.zeroId(), cur, "refs/heads/batch1");
 		ReceiveCommand rc2 = new ReceiveCommand(ObjectId.zeroId(), prev, "refs/heads/batch2");
 		String msg =  "message";
+		RefDatabase refDb = db.getRefDatabase();
 		try (RevWalk rw = new RevWalk(db)) {
-			db.getRefDatabase().newBatchUpdate()
+			refDb.newBatchUpdate()
 					.addCommand(rc1, rc2)
 					.setAtomic(true)
 					.setRefLogIdent(person)
@@ -194,15 +280,17 @@ public void testBatchrefUpdate() throws Exception {
 					.execute(rw, NullProgressMonitor.INSTANCE);
 		}
 
-		assertEquals(rc1.getResult(), ReceiveCommand.Result.OK);
-		assertEquals(rc2.getResult(), ReceiveCommand.Result.OK);
+		assertEquals(ReceiveCommand.Result.OK, rc1.getResult());
+		assertEquals(ReceiveCommand.Result.OK, rc2.getResult());
 
-		ReflogEntry e = db.getReflogReader("refs/heads/batch1").getLastEntry();
+		ReflogEntry e = refDb.getReflogReader("refs/heads/batch1")
+				.getLastEntry();
 		assertEquals(msg, e.getComment());
 		assertEquals(person, e.getWho());
 		assertEquals(cur, e.getNewId());
 
-		e = db.getReflogReader("refs/heads/batch2").getLastEntry();
+		e = refDb.getReflogReader("refs/heads/batch2")
+				.getLastEntry();
 		assertEquals(msg, e.getComment());
 		assertEquals(person, e.getWho());
 		assertEquals(prev, e.getNewId());
@@ -267,7 +355,7 @@ public void testDelete() throws Exception {
 		RefUpdate up = db.getRefDatabase().newUpdate("refs/heads/a", false);
 		up.setForceUpdate(true);
 		RefUpdate.Result res = up.delete();
-		assertEquals(res, FORCED);
+		assertEquals(FORCED, res);
 		assertNull(db.exactRef("refs/heads/a"));
 	}
 
@@ -309,7 +397,7 @@ public void testUpdateRefDetached() throws Exception {
 
 		// the branch HEAD referred to is left untouched
 		assertEquals(pid, db.resolve("refs/heads/master"));
-		ReflogReader reflogReader = db.getReflogReader("HEAD");
+		ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD");
 		ReflogEntry e = reflogReader.getReverseEntries().get(0);
 		assertEquals(ppid, e.getNewId());
 		assertEquals("GIT_COMMITTER_EMAIL", e.getWho().getEmailAddress());
@@ -330,12 +418,13 @@ public void testWriteReflog() throws Exception {
 		updateRef.setForceUpdate(true);
 		RefUpdate.Result update = updateRef.update();
 		assertEquals(FORCED, update); // internal
-		ReflogReader r = db.getReflogReader("refs/heads/master");
+		ReflogReader r = db.getRefDatabase()
+				.getReflogReader("refs/heads/master");
 
 		ReflogEntry e = r.getLastEntry();
-		assertEquals(e.getNewId(), pid);
-		assertEquals(e.getComment(), "REFLOG!: FORCED");
-		assertEquals(e.getWho(), person);
+		assertEquals(pid, e.getNewId());
+		assertEquals("REFLOG!: FORCED", e.getComment());
+		assertEquals(person, e.getWho());
 	}
 
 	@Test
@@ -352,10 +441,11 @@ public void testLooseDelete() throws IOException {
 		ref = db.updateRef(newRef);
 		ref.setNewObjectId(db.resolve(Constants.HEAD));
 
-		assertEquals(ref.delete(), RefUpdate.Result.NO_CHANGE);
+		assertEquals(RefUpdate.Result.NO_CHANGE, ref.delete());
 
 		// Differs from RefupdateTest. Deleting a loose ref leaves reflog trail.
-		ReflogReader reader = db.getReflogReader("refs/heads/abc");
+		ReflogReader reader = db.getRefDatabase()
+				.getReflogReader("refs/heads/abc");
 		assertEquals(ObjectId.zeroId(), reader.getReverseEntry(1).getOldId());
 		assertEquals(nonZero, reader.getReverseEntry(1).getNewId());
 		assertEquals(nonZero, reader.getReverseEntry(0).getOldId());
@@ -382,8 +472,9 @@ public void testNoCacheObjectIdSubclass() throws IOException {
 		assertNotSame(newid, r.getObjectId());
 		assertSame(ObjectId.class, r.getObjectId().getClass());
 		assertEquals(newid, r.getObjectId());
-		List<ReflogEntry> reverseEntries1 = db.getReflogReader("refs/heads/abc")
-				.getReverseEntries();
+		RefDatabase refDb = db.getRefDatabase();
+		List<ReflogEntry> reverseEntries1 = refDb
+				.getReflogReader("refs/heads/abc").getReverseEntries();
 		ReflogEntry entry1 = reverseEntries1.get(0);
 		assertEquals(1, reverseEntries1.size());
 		assertEquals(ObjectId.zeroId(), entry1.getOldId());
@@ -392,7 +483,7 @@ public void testNoCacheObjectIdSubclass() throws IOException {
 		assertEquals(new PersonIdent(db).toString(),
 				entry1.getWho().toString());
 		assertEquals("", entry1.getComment());
-		List<ReflogEntry> reverseEntries2 = db.getReflogReader("HEAD")
+		List<ReflogEntry> reverseEntries2 = refDb.getReflogReader("HEAD")
 				.getReverseEntries();
 		assertEquals(0, reverseEntries2.size());
 	}
@@ -431,7 +522,7 @@ public void writeUnbornHead() throws Exception {
 
 		Ref head = db.exactRef("HEAD");
 		assertTrue(head.isSymbolic());
-		assertEquals(head.getTarget().getName(), "refs/heads/unborn");
+		assertEquals("refs/heads/unborn", head.getTarget().getName());
 	}
 
 	/**
@@ -455,7 +546,7 @@ public void testUpdateRefDetachedUnbornHead() throws Exception {
 
 		// the branch HEAD referred to is left untouched
 		assertNull(db.resolve("refs/heads/unborn"));
-		ReflogReader reflogReader = db.getReflogReader("HEAD");
+		ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD");
 		ReflogEntry e = reflogReader.getReverseEntries().get(0);
 		assertEquals(ObjectId.zeroId(), e.getOldId());
 		assertEquals(ppid, e.getNewId());
@@ -499,7 +590,7 @@ public void testRenameCurrentBranch() throws IOException {
 		names.add("refs/heads/new/name");
 
 		for (String nm : names) {
-			ReflogReader rd = db.getReflogReader(nm);
+			ReflogReader rd = db.getRefDatabase().getReflogReader(nm);
 			assertNotNull(rd);
 			ReflogEntry last = rd.getLastEntry();
 			ObjectId id = last.getNewId();
@@ -573,10 +664,10 @@ public void compactFully() throws Exception {
 			assertTrue(res == Result.NEW || res == FORCED);
 		}
 
-		assertEquals(refDb.exactRef(refName).getObjectId(), bId);
+		assertEquals(bId, refDb.exactRef(refName).getObjectId());
 		assertTrue(randomStr.equals(refDb.getReflogReader(refName).getReverseEntry(1).getComment()));
 		refDb.compactFully();
-		assertEquals(refDb.exactRef(refName).getObjectId(), bId);
+		assertEquals(bId, refDb.exactRef(refName).getObjectId());
 		assertTrue(randomStr.equals(refDb.getReflogReader(refName).getReverseEntry(1).getComment()));
 	}
 
@@ -644,6 +735,54 @@ public void testGetRefsWithPrefixExcludingOverlappingPrefixes() throws IOExcepti
 		checkContainsRef(refs, db.exactRef("HEAD"));
 	}
 
+	@Test
+	public void testExternalUpdate_bug_102() throws Exception {
+		((FileReftableDatabase) db.getRefDatabase()).setAutoRefresh(true);
+		assumeTrue(atLeastGitVersion(2, 45));
+		Git git = Git.wrap(db);
+		git.tag().setName("foo").call();
+		Ref ref = db.exactRef("refs/tags/foo");
+		assertNotNull(ref);
+		runGitCommand("tag", "--force", "foo", "e");
+		Ref e = db.exactRef("refs/heads/e");
+		Ref foo = db.exactRef("refs/tags/foo");
+		assertEquals(e.getObjectId(), foo.getObjectId());
+	}
+
+	private String toString(TemporaryBuffer b) throws IOException {
+		return RawParseUtils.decode(b.toByteArray());
+	}
+
+	private ExecutionResult runGitCommand(String... args)
+			throws IOException, InterruptedException {
+		FS fs = db.getFS();
+		ProcessBuilder pb = fs.runInShell("git", args);
+		pb.directory(db.getWorkTree());
+		System.err.println("PATH=" + pb.environment().get("PATH"));
+		ExecutionResult result = fs.execute(pb, null);
+		assertEquals(0, result.getRc());
+		String err = toString(result.getStderr());
+		if (!err.isEmpty()) {
+			System.err.println(err);
+		}
+		String out = toString(result.getStdout());
+		if (!out.isEmpty()) {
+			System.out.println(out);
+		}
+		return result;
+	}
+
+	private boolean atLeastGitVersion(int minMajor, int minMinor)
+			throws IOException, InterruptedException {
+		String version = toString(runGitCommand("version").getStdout())
+				.split(" ")[2];
+		System.out.println(version);
+		String[] digits = version.split("\\.");
+		int major = Integer.parseInt(digits[0]);
+		int minor = Integer.parseInt(digits[1]);
+		return (major >= minMajor) && (minor >= minMinor);
+	}
+
 	private RefUpdate updateRef(String name) throws IOException {
 		final RefUpdate ref = db.updateRef(name);
 		ref.setNewObjectId(db.resolve(Constants.HEAD));
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 6cad8b6..434f7e4 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
@@ -16,9 +16,9 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Date;
 import java.util.List;
 
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
@@ -206,7 +206,7 @@ public void testDonePruneTooYoungPacks() throws Exception {
 
 		// The old packfile is too young to be deleted. We should end up with
 		// two pack files
-		gc.setExpire(new Date(oldPackfile.lastModified() - 1));
+		gc.setExpire(Instant.ofEpochMilli(oldPackfile.lastModified() - 1));
 		gc.gc().get();
 		stats = gc.getStatistics();
 		assertEquals(0, stats.numberOfLooseObjects);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java
new file mode 100644
index 0000000..cd1264e
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2024 Jacek Centkowski <geminica.programs@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.stream.StreamSupport;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class GcNumberOfPackFilesSinceBitmapStatisticsTest extends GcTestCase {
+	@Test
+	public void testShouldReportZeroObjectsForInitializedRepo()
+			throws IOException {
+		assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportAllPackFilesWhenNoGcWasPerformed()
+			throws Exception {
+		tr.packAndPrune();
+		long result = gc.getStatistics().numberOfPackFilesSinceBitmap;
+
+		assertEquals(repo.getObjectDatabase().getPacks().size(), result);
+	}
+
+	@Test
+	public void testShouldReportNoObjectsDirectlyAfterGc() throws Exception {
+		// given
+		addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+		assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewObjectsSinceGcWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+
+		// progress & pack
+		addCommit(parent);
+		tr.packAndPrune();
+
+		assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewObjectsFromTheLatestBitmapWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+
+		// progress & gc
+		parent = addCommit(parent);
+		gc.gc().get();
+		assertEquals(2L, repositoryBitmapFiles());
+
+		// progress & pack
+		addCommit(parent);
+		tr.packAndPrune();
+
+		assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	private RevCommit addCommit(RevCommit parent) throws Exception {
+		PersonIdent ident = new PersonIdent("repo-metrics", "repo@metrics.com");
+		TestRepository<FileRepository>.CommitBuilder builder = tr.commit()
+				.author(ident);
+		if (parent != null) {
+			builder.parent(parent);
+		}
+		RevCommit commit = builder.create();
+		tr.update("master", commit);
+		parent = commit;
+		return parent;
+	}
+
+	private long repositoryBitmapFiles() throws IOException {
+		return StreamSupport
+				.stream(Files
+						.newDirectoryStream(repo.getObjectDatabase()
+								.getPackDirectory().toPath(), "pack-*.bitmap")
+						.spliterator(), false)
+				.count();
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
index 8baa3cc..f84be21 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertSame;
 
 import java.io.File;
-import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.concurrent.BrokenBarrierException;
@@ -31,6 +30,8 @@
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.PackRefsCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
@@ -49,7 +50,7 @@ public void looseRefPacked() throws Exception {
 		RevBlob a = tr.blob("a");
 		tr.lightweightTag("t", a);
 
-		gc.packRefs();
+		packRefs(false);
 		assertSame(repo.exactRef("refs/tags/t").getStorage(), Storage.PACKED);
 	}
 
@@ -58,9 +59,9 @@ public void emptyRefDirectoryDeleted() throws Exception {
 		String ref = "dir/ref";
 		tr.branch(ref).commit().create();
 		String name = repo.findRef(ref).getName();
-		Path dir = repo.getDirectory().toPath().resolve(name).getParent();
+		Path dir = repo.getCommonDirectory().toPath().resolve(name).getParent();
 		assertNotNull(dir);
-		gc.packRefs();
+		packRefs(true);
 		assertFalse(Files.exists(dir));
 	}
 
@@ -75,9 +76,9 @@ public void concurrentOnlyOneWritesPackedRefs() throws Exception {
 		Callable<Integer> packRefs = () -> {
 			syncPoint.await();
 			try {
-				gc.packRefs();
+				packRefs(false);
 				return 0;
-			} catch (IOException e) {
+			} catch (GitAPIException e) {
 				return 1;
 			}
 		};
@@ -102,7 +103,7 @@ public void whileRefLockedRefNotPackedNoError()
 				"refs/tags/t1"));
 		try {
 			refLock.lock();
-			gc.packRefs();
+			packRefs(false);
 		} finally {
 			refLock.unlock();
 		}
@@ -145,7 +146,7 @@ public boolean isForceUpdate() {
 
 			Future<Result> result2 = pool.submit(() -> {
 				refUpdateLockedRef.await();
-				gc.packRefs();
+				packRefs(false);
 				packRefsDone.await();
 				return null;
 			});
@@ -173,19 +174,20 @@ public void dontPackHEAD_nonBare() throws Exception {
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
 		assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
-		gc.packRefs();
+		PackRefsCommand packRefsCommand = git.packRefs().setAll(true);
+		packRefsCommand.call();
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
 		assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
 
 		git.checkout().setName("refs/heads/side").call();
-		gc.packRefs();
+		packRefsCommand.call();
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 
 		// check for detached HEAD
 		git.checkout().setName(first.getName()).call();
-		gc.packRefs();
+		packRefsCommand.call();
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 	}
 
@@ -208,7 +210,7 @@ public void dontPackHEAD_bare() throws Exception {
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
 		assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
-		gc.packRefs();
+		packRefs(true);
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
@@ -216,9 +218,14 @@ public void dontPackHEAD_bare() throws Exception {
 
 		// check for non-detached HEAD
 		repo.updateRef(Constants.HEAD).link("refs/heads/side");
-		gc.packRefs();
+		packRefs(true);
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 		assertEquals(repo.exactRef("HEAD").getTarget().getObjectId(),
 				second.getId());
 	}
+
+	private void packRefs(boolean all) throws GitAPIException {
+		new PackRefsCommand(repo).setAll(all).call();
+	}
+
 }
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 ca0f684..84ec132 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
@@ -16,8 +16,8 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
+import java.time.Instant;
 import java.util.Collections;
-import java.util.Date;
 
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.eclipse.jgit.lib.ObjectId;
@@ -30,7 +30,7 @@ public class GcPruneNonReferencedTest extends GcTestCase {
 	@Test
 	public void nonReferencedNonExpiredObject_notPruned() throws Exception {
 		RevBlob a = tr.blob("a");
-		gc.setExpire(new Date(lastModified(a)));
+		gc.setExpire(Instant.ofEpochMilli(lastModified(a)));
 		gc.prune(Collections.<ObjectId> emptySet());
 		assertTrue(repo.getObjectDatabase().has(a));
 	}
@@ -58,7 +58,7 @@ public void nonReferencedExpiredObjectTree_pruned() throws Exception {
 	@Test
 	public void nonReferencedObjects_onlyExpiredPruned() throws Exception {
 		RevBlob a = tr.blob("a");
-		gc.setExpire(new Date(lastModified(a) + 1));
+		gc.setExpire(Instant.ofEpochMilli(lastModified(a) + 1));
 
 		fsTick();
 		RevBlob b = tr.blob("b");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java
index e6c1ee5..29f180d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java
@@ -30,7 +30,7 @@ public void testPruneNone() throws Exception {
 		BranchBuilder bb = tr.branch("refs/heads/master");
 		bb.commit().add("A", "A").add("B", "B").create();
 		bb.commit().add("A", "A2").add("B", "B2").create();
-		new File(repo.getDirectory(), Constants.LOGS + "/refs/heads/master")
+		new File(repo.getCommonDirectory(), Constants.LOGS + "/refs/heads/master")
 				.delete();
 		stats = gc.getStatistics();
 		assertEquals(8, stats.numberOfLooseObjects);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java
new file mode 100644
index 0000000..af52e2c
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2024 Jacek Centkowski <geminica.programs@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collection;
+import java.util.stream.StreamSupport;
+
+import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.storage.pack.PackConfig;
+import org.junit.Test;
+
+public class GcSinceBitmapStatisticsTest extends GcTestCase {
+	@Test
+	public void testShouldReportZeroPacksAndObjectsForInitializedRepo()
+			throws IOException {
+		RepoStatistics s = gc.getStatistics();
+		assertEquals(0L, s.numberOfPackFilesSinceBitmap);
+		assertEquals(0L, s.numberOfObjectsSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportAllPackFilesWhenNoGcWasPerformed()
+			throws Exception {
+		tr.packAndPrune();
+		long result = gc.getStatistics().numberOfPackFilesSinceBitmap;
+
+		assertEquals(repo.getObjectDatabase().getPacks().size(), result);
+	}
+
+	@Test
+	public void testShouldReportAllObjectsWhenNoGcWasPerformed()
+			throws Exception {
+		tr.packAndPrune();
+
+		assertEquals(
+				getNumberOfObjectsInPacks(repo.getObjectDatabase().getPacks()),
+				gc.getStatistics().numberOfObjectsSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNoPacksFilesSinceBitmapWhenPackfilesAreOlderThanBitmapFile()
+			throws Exception {
+		addCommit(null);
+		configureGC(/* buildBitmap */ false).gc().get();
+		assertEquals(1L, gc.getStatistics().numberOfPackFiles);
+		assertEquals(0L, repositoryBitmapFiles());
+		assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+
+		addCommit(null);
+		configureGC(/* buildBitmap */ true).gc().get();
+
+		assertEquals(1L, repositoryBitmapFiles());
+		assertEquals(2L, gc.getStatistics().numberOfPackFiles);
+		assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNoObjectsDirectlyAfterGc() throws Exception {
+		// given
+		addCommit(null);
+		assertEquals(2L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		gc.gc().get();
+		assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewPacksSinceGcWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+
+		// progress & pack
+		addCommit(parent);
+		assertEquals(1L, gc.getStatistics().numberOfPackFiles);
+		assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+
+		tr.packAndPrune();
+		assertEquals(2L, gc.getStatistics().numberOfPackFiles);
+		assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewObjectsSinceGcWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(0L, gc.getStatistics().numberOfLooseObjects);
+		assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		// progress & pack
+		addCommit(parent);
+		assertEquals(1L, gc.getStatistics().numberOfLooseObjects);
+		assertEquals(1L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		tr.packAndPrune();
+		assertEquals(0L, gc.getStatistics().numberOfLooseObjects);
+		// Number of objects contained in the newly created PackFile
+		assertEquals(3L, gc.getStatistics().numberOfObjectsSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewPacksFromTheLatestBitmapWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+		assertEquals(1L, repositoryBitmapFiles());
+
+		// progress & gc
+		parent = addCommit(parent);
+		gc.gc().get();
+		assertEquals(2L, repositoryBitmapFiles());
+
+		// progress & pack
+		addCommit(parent);
+		tr.packAndPrune();
+
+		assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+	}
+
+	@Test
+	public void testShouldReportNewObjectsFromTheLatestBitmapWhenRepositoryProgresses()
+			throws Exception {
+		// commit & gc
+		RevCommit parent = addCommit(null);
+		gc.gc().get();
+
+		// progress & gc
+		parent = addCommit(parent);
+		gc.gc().get();
+		assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		// progress & pack
+		addCommit(parent);
+		assertEquals(1L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+		tr.packAndPrune();
+		assertEquals(4L, gc.getStatistics().numberOfObjectsSinceBitmap);
+	}
+
+	private RevCommit addCommit(RevCommit parent) throws Exception {
+		return tr.branch("master").commit()
+				.author(new PersonIdent("repo-metrics", "repo@metrics.com"))
+				.parent(parent).create();
+	}
+
+	private long repositoryBitmapFiles() throws IOException {
+		return StreamSupport
+				.stream(Files
+						.newDirectoryStream(repo.getObjectDatabase()
+								.getPackDirectory().toPath(), "pack-*.bitmap")
+						.spliterator(), false)
+				.count();
+	}
+
+	private long getNumberOfObjectsInPacks(Collection<Pack> packs) {
+		return packs.stream().mapToLong(pack -> {
+			try {
+				return pack.getObjectCount();
+			} catch (IOException e) {
+				throw new RuntimeException(e);
+			}
+		}).sum();
+	}
+
+	private GC configureGC(boolean buildBitmap) {
+		PackConfig pc = new PackConfig(repo.getObjectDatabase().getConfig());
+		pc.setBuildBitmaps(buildBitmap);
+		gc.setPackConfig(pc);
+		return gc;
+	}
+}
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 746a0a1..33cbc86 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
@@ -49,7 +49,10 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
 import java.io.File;
@@ -66,6 +69,7 @@
 
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -207,33 +211,35 @@ public void testOpenLooseObjectSuppressStaleFileHandleException()
 				.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b");
 		WindowCursor curs = new WindowCursor(db.getObjectDatabase());
 
-		LooseObjects mock = mock(LooseObjects.class);
+		Config config = new Config();
+		config.setString("core", null, "trustLooseObjectStat", "ALWAYS");
+		LooseObjects spy = Mockito.spy(new LooseObjects(config, trash));
 		UnpackedObjectCache unpackedObjectCacheMock = mock(
 				UnpackedObjectCache.class);
 
-		Mockito.when(mock.getObjectLoader(any(), any(), any()))
-				.thenThrow(new IOException("Stale File Handle"));
-		Mockito.when(mock.open(curs, id)).thenCallRealMethod();
-		Mockito.when(mock.unpackedObjectCache())
-				.thenReturn(unpackedObjectCacheMock);
+		doThrow(new IOException("Stale File Handle")).when(spy)
+				.getObjectLoader(any(), any(), any());
+		doReturn(unpackedObjectCacheMock).when(spy).unpackedObjectCache();
 
-		assertNull(mock.open(curs, id));
+		assertNull(spy.open(curs, id));
 		verify(unpackedObjectCacheMock).remove(id);
 	}
 
-	@Test
+	@Test(expected = IOException.class)
 	public void testOpenLooseObjectPropagatesIOExceptions() throws Exception {
 		ObjectId id = ObjectId
 				.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b");
 		WindowCursor curs = new WindowCursor(db.getObjectDatabase());
 
-		LooseObjects mock = mock(LooseObjects.class);
+		Config config = new Config();
+		config.setString("core", null, "trustLooseObjectStat", "NEVER");
+		LooseObjects spy = spy(new LooseObjects(config,
+				db.getObjectDatabase().getDirectory()));
 
-		Mockito.when(mock.getObjectLoader(any(), any(), any()))
-				.thenThrow(new IOException("some IO failure"));
-		Mockito.when(mock.open(curs, id)).thenCallRealMethod();
+		doThrow(new IOException("some IO failure")).when(spy)
+				.getObjectLoader(any(), any(), any());
 
-		assertThrows(IOException.class, () -> mock.open(curs, id));
+		spy.open(curs, id);
 	}
 
 	@Test
@@ -243,17 +249,18 @@ public void testWindowCursorGetCommitGraph() throws Exception {
 		db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null,
 				ConfigConstants.CONFIG_KEY_WRITE_COMMIT_GRAPH, true);
 
-		WindowCursor curs = new WindowCursor(db.getObjectDatabase());
-		assertTrue(curs.getCommitGraph().isEmpty());
-		commitFile("file.txt", "content", "master");
-		GC gc = new GC(db);
-		gc.gc().get();
-		assertTrue(curs.getCommitGraph().isPresent());
+		try (WindowCursor curs = new WindowCursor(db.getObjectDatabase())) {
+			assertTrue(curs.getCommitGraph().isEmpty());
+			commitFile("file.txt", "content", "master");
+			GC gc = new GC(db);
+			gc.gc().get();
+			assertTrue(curs.getCommitGraph().isPresent());
 
-		db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
-				ConfigConstants.CONFIG_COMMIT_GRAPH, false);
+			db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+					ConfigConstants.CONFIG_COMMIT_GRAPH, false);
 
-		assertTrue(curs.getCommitGraph().isEmpty());
+			assertTrue(curs.getCommitGraph().isEmpty());
+		}
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java
index 24bdc4a..1f934ac 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java
@@ -13,6 +13,7 @@
 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.File;
@@ -25,6 +26,7 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
@@ -99,6 +101,39 @@ public void testIteratorMethodsContract() {
 		}
 	}
 
+	@Test
+	public void testIteratorMutableEntryCompareTo() {
+		Iterator<PackIndex.MutableEntry> iterA = smallIdx.iterator();
+		Iterator<PackIndex.MutableEntry> iterB = smallIdx.iterator();
+
+		MutableEntry aEntry = iterA.next();
+		iterB.next();
+		MutableEntry bEntry = iterB.next();
+		// b is one ahead
+		assertTrue(aEntry.compareBySha1To(bEntry) < 0);
+		assertTrue(bEntry.compareBySha1To(aEntry) > 0);
+
+		// advance a, now should be equal
+		assertEquals(0, iterA.next().compareBySha1To(bEntry));
+	}
+
+	@Test
+	public void testIteratorMutableEntryCopyTo() {
+		Iterator<PackIndex.MutableEntry> it = smallIdx.iterator();
+
+		MutableObjectId firstOidCopy = new MutableObjectId();
+		MutableEntry next = it.next();
+		next.copyOidTo(firstOidCopy);
+		ObjectId firstImmutable = next.toObjectId();
+
+		MutableEntry second = it.next();
+
+		// The copy has the right value after "next"
+		assertTrue(firstImmutable.equals(firstOidCopy));
+		assertFalse("iterator has moved",
+				second.toObjectId().equals(firstImmutable));
+	}
+
 	/**
 	 * Test results of iterator comparing to content of well-known (prepared)
 	 * small index.
@@ -106,22 +141,22 @@ public void testIteratorMethodsContract() {
 	@Test
 	public void testIteratorReturnedValues1() {
 		Iterator<PackIndex.MutableEntry> iter = smallIdx.iterator();
-		assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904", iter.next()
-				.name());
-		assertEquals("540a36d136cf413e4b064c2b0e0a4db60f77feab", iter.next()
-				.name());
-		assertEquals("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259", iter.next()
-				.name());
-		assertEquals("6ff87c4664981e4397625791c8ea3bbb5f2279a3", iter.next()
-				.name());
-		assertEquals("82c6b885ff600be425b4ea96dee75dca255b69e7", iter.next()
-				.name());
-		assertEquals("902d5476fa249b7abc9d84c611577a81381f0327", iter.next()
-				.name());
-		assertEquals("aabf2ffaec9b497f0950352b3e582d73035c2035", iter.next()
-				.name());
-		assertEquals("c59759f143fb1fe21c197981df75a7ee00290799", iter.next()
-				.name());
+		assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+				iter.next().name());
+		assertEquals("540a36d136cf413e4b064c2b0e0a4db60f77feab",
+				iter.next().name());
+		assertEquals("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259",
+				iter.next().name());
+		assertEquals("6ff87c4664981e4397625791c8ea3bbb5f2279a3",
+				iter.next().name());
+		assertEquals("82c6b885ff600be425b4ea96dee75dca255b69e7",
+				iter.next().name());
+		assertEquals("902d5476fa249b7abc9d84c611577a81381f0327",
+				iter.next().name());
+		assertEquals("aabf2ffaec9b497f0950352b3e582d73035c2035",
+				iter.next().name());
+		assertEquals("c59759f143fb1fe21c197981df75a7ee00290799",
+				iter.next().name());
 		assertFalse(iter.hasNext());
 	}
 
@@ -198,16 +233,16 @@ public void testCompareEntriesOffsetsWithGetOffsets() {
 	@Test
 	public void testIteratorReturnedValues2() {
 		Iterator<PackIndex.MutableEntry> iter = denseIdx.iterator();
-		while (!iter.next().name().equals(
-				"0a3d7772488b6b106fb62813c4d6d627918d9181")) {
+		while (!iter.next().name()
+				.equals("0a3d7772488b6b106fb62813c4d6d627918d9181")) {
 			// just iterating
 		}
-		assertEquals("1004d0d7ac26fbf63050a234c9b88a46075719d3", iter.next()
-				.name()); // same level-1
-		assertEquals("10da5895682013006950e7da534b705252b03be6", iter.next()
-				.name()); // same level-1
-		assertEquals("1203b03dc816ccbb67773f28b3c19318654b0bc8", iter.next()
-				.name());
+		assertEquals("1004d0d7ac26fbf63050a234c9b88a46075719d3",
+				iter.next().name()); // same level-1
+		assertEquals("10da5895682013006950e7da534b705252b03be6",
+				iter.next().name()); // same level-1
+		assertEquals("1203b03dc816ccbb67773f28b3c19318654b0bc8",
+				iter.next().name());
 	}
 
 	@Test
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 2bafde6..baa0182 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
@@ -90,25 +90,26 @@ public void refDirectorySetup() throws Exception {
 	@Test
 	public void testCreate() throws IOException {
 		// setUp above created the directory. We just have to test it.
-		File d = diskRepo.getDirectory();
+		File gitDir = diskRepo.getDirectory();
+		File commonDir = diskRepo.getCommonDirectory();
 		assertSame(diskRepo, refdir.getRepository());
 
-		assertTrue(new File(d, "refs").isDirectory());
-		assertTrue(new File(d, "logs").isDirectory());
-		assertTrue(new File(d, "logs/refs").isDirectory());
-		assertFalse(new File(d, "packed-refs").exists());
+		assertTrue(new File(commonDir, "refs").isDirectory());
+		assertTrue(new File(commonDir, "logs").isDirectory());
+		assertTrue(new File(commonDir, "logs/refs").isDirectory());
+		assertFalse(new File(commonDir, "packed-refs").exists());
 
-		assertTrue(new File(d, "refs/heads").isDirectory());
-		assertTrue(new File(d, "refs/tags").isDirectory());
-		assertEquals(2, new File(d, "refs").list().length);
-		assertEquals(0, new File(d, "refs/heads").list().length);
-		assertEquals(0, new File(d, "refs/tags").list().length);
+		assertTrue(new File(commonDir, "refs/heads").isDirectory());
+		assertTrue(new File(commonDir, "refs/tags").isDirectory());
+		assertEquals(2, new File(commonDir, "refs").list().length);
+		assertEquals(0, new File(commonDir, "refs/heads").list().length);
+		assertEquals(0, new File(commonDir, "refs/tags").list().length);
 
-		assertTrue(new File(d, "logs/refs/heads").isDirectory());
-		assertFalse(new File(d, "logs/HEAD").exists());
-		assertEquals(0, new File(d, "logs/refs/heads").list().length);
+		assertTrue(new File(commonDir, "logs/refs/heads").isDirectory());
+		assertFalse(new File(gitDir, "logs/HEAD").exists());
+		assertEquals(0, new File(commonDir, "logs/refs/heads").list().length);
 
-		assertEquals("ref: refs/heads/master\n", read(new File(d, HEAD)));
+		assertEquals("ref: refs/heads/master\n", read(new File(gitDir, HEAD)));
 	}
 
 	@Test(expected = UnsupportedOperationException.class)
@@ -1382,7 +1383,7 @@ private void writeLooseRef(String name, String content) throws IOException {
 	}
 
 	private void deleteLooseRef(String name) {
-		File path = new File(diskRepo.getDirectory(), name);
+		File path = new File(diskRepo.getCommonDirectory(), name);
 		assertTrue("deleted " + name, path.delete());
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
index cb977bd..acc36d7 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
@@ -40,6 +40,7 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 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;
@@ -111,16 +112,17 @@ public void testNoCacheObjectIdSubclass() throws IOException {
 		assertNotSame(newid, r.getObjectId());
 		assertSame(ObjectId.class, r.getObjectId().getClass());
 		assertEquals(newid, r.getObjectId());
-		List<ReflogEntry> reverseEntries1 = db
+		List<ReflogEntry> reverseEntries1 = db.getRefDatabase()
 				.getReflogReader("refs/heads/abc").getReverseEntries();
 		ReflogEntry entry1 = reverseEntries1.get(0);
 		assertEquals(1, reverseEntries1.size());
 		assertEquals(ObjectId.zeroId(), entry1.getOldId());
 		assertEquals(r.getObjectId(), entry1.getNewId());
-		assertEquals(new PersonIdent(db).toString(),  entry1.getWho().toString());
+		assertEquals(new PersonIdent(db).toString(),
+				entry1.getWho().toString());
 		assertEquals("", entry1.getComment());
-		List<ReflogEntry> reverseEntries2 = db.getReflogReader("HEAD")
-				.getReverseEntries();
+		List<ReflogEntry> reverseEntries2 = db.getRefDatabase()
+				.getReflogReader("HEAD").getReverseEntries();
 		assertEquals(0, reverseEntries2.size());
 	}
 
@@ -136,8 +138,11 @@ public void testNewNamespaceConflictWithLoosePrefixNameExists()
 		final RefUpdate ru2 = updateRef(newRef2);
 		Result update2 = ru2.update();
 		assertEquals(Result.LOCK_FAILURE, update2);
-		assertEquals(1, db.getReflogReader("refs/heads/z").getReverseEntries().size());
-		assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(1, refDb.getReflogReader("refs/heads/z")
+				.getReverseEntries().size());
+		assertEquals(0,
+				refDb.getReflogReader("HEAD").getReverseEntries().size());
 	}
 
 	@Test
@@ -147,8 +152,10 @@ public void testNewNamespaceConflictWithPackedPrefixNameExists()
 		final RefUpdate ru = updateRef(newRef);
 		Result update = ru.update();
 		assertEquals(Result.LOCK_FAILURE, update);
-		assertNull(db.getReflogReader("refs/heads/master/x"));
-		assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+		RefDatabase refDb = db.getRefDatabase();
+		assertNull(refDb.getReflogReader("refs/heads/master/x"));
+		assertEquals(0,
+				refDb.getReflogReader("HEAD").getReverseEntries().size());
 	}
 
 	@Test
@@ -163,9 +170,12 @@ public void testNewNamespaceConflictWithLoosePrefixOfExisting()
 		final RefUpdate ru2 = updateRef(newRef2);
 		Result update2 = ru2.update();
 		assertEquals(Result.LOCK_FAILURE, update2);
-		assertEquals(1, db.getReflogReader("refs/heads/z/a").getReverseEntries().size());
-		assertNull(db.getReflogReader("refs/heads/z"));
-		assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(1, refDb.getReflogReader("refs/heads/z/a")
+				.getReverseEntries().size());
+		assertNull(refDb.getReflogReader("refs/heads/z"));
+		assertEquals(0,
+				refDb.getReflogReader("HEAD").getReverseEntries().size());
 	}
 
 	@Test
@@ -175,8 +185,10 @@ public void testNewNamespaceConflictWithPackedPrefixOfExisting()
 		final RefUpdate ru = updateRef(newRef);
 		Result update = ru.update();
 		assertEquals(Result.LOCK_FAILURE, update);
-		assertNull(db.getReflogReader("refs/heads/prefix"));
-		assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+		RefDatabase refDb = db.getRefDatabase();
+		assertNull(refDb.getReflogReader("refs/heads/prefix"));
+		assertEquals(0,
+				refDb.getReflogReader("HEAD").getReverseEntries().size());
 	}
 
 	/**
@@ -197,8 +209,11 @@ public void testDeleteHEADreferencedRef() throws IOException {
 		Result delete = updateRef2.delete();
 		assertEquals(Result.REJECTED_CURRENT_BRANCH, delete);
 		assertEquals(pid, db.resolve("refs/heads/master"));
-		assertEquals(1,db.getReflogReader("refs/heads/master").getReverseEntries().size());
-		assertEquals(0,db.getReflogReader("HEAD").getReverseEntries().size());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(1, refDb.getReflogReader("refs/heads/master")
+				.getReverseEntries().size());
+		assertEquals(0,
+				refDb.getReflogReader("HEAD").getReverseEntries().size());
 	}
 
 	@Test
@@ -209,7 +224,8 @@ public void testWriteReflog() throws IOException {
 		updateRef.setForceUpdate(true);
 		Result update = updateRef.update();
 		assertEquals(Result.FORCED, update);
-		assertEquals(1,db.getReflogReader("refs/heads/master").getReverseEntries().size());
+		assertEquals(1, db.getRefDatabase().getReflogReader("refs/heads/master")
+				.getReverseEntries().size());
 	}
 
 	@Test
@@ -219,15 +235,18 @@ public void testLooseDelete() throws IOException {
 		ref.update(); // create loose ref
 		ref = updateRef(newRef); // refresh
 		delete(ref, Result.NO_CHANGE);
-		assertNull(db.getReflogReader("refs/heads/abc"));
+		assertNull(db.getRefDatabase().getReflogReader("refs/heads/abc"));
 	}
 
 	@Test
 	public void testDeleteHead() throws IOException {
 		final RefUpdate ref = updateRef(Constants.HEAD);
 		delete(ref, Result.REJECTED_CURRENT_BRANCH, true, false);
-		assertEquals(0, db.getReflogReader("refs/heads/master").getReverseEntries().size());
-		assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(0, refDb.getReflogReader("refs/heads/master")
+				.getReverseEntries().size());
+		assertEquals(0,
+				refDb.getReflogReader("HEAD").getReverseEntries().size());
 	}
 
 	@Test
@@ -423,7 +442,7 @@ public void testUpdateRefDetached() throws Exception {
 
 		// the branch HEAD referred to is left untouched
 		assertEquals(pid, db.resolve("refs/heads/master"));
-		ReflogReader reflogReader = db.getReflogReader("HEAD");
+		ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD");
 		ReflogEntry e = reflogReader.getReverseEntries().get(0);
 		assertEquals(pid, e.getOldId());
 		assertEquals(ppid, e.getNewId());
@@ -453,7 +472,7 @@ public void testUpdateRefDetachedUnbornHead() throws Exception {
 
 		// the branch HEAD referred to is left untouched
 		assertNull(db.resolve("refs/heads/unborn"));
-		ReflogReader reflogReader = db.getReflogReader("HEAD");
+		ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD");
 		ReflogEntry e = reflogReader.getReverseEntries().get(0);
 		assertEquals(ObjectId.zeroId(), e.getOldId());
 		assertEquals(ppid, e.getNewId());
@@ -691,9 +710,12 @@ public void testRenameBranchNoPreviousLog() throws IOException {
 		assertEquals(Result.RENAMED, result);
 		assertEquals(rb, db.resolve("refs/heads/new/name"));
 		assertNull(db.resolve("refs/heads/b"));
-		assertEquals(1, db.getReflogReader("new/name").getReverseEntries().size());
-		assertEquals("Branch: renamed b to new/name", db.getReflogReader("new/name")
-				.getLastEntry().getComment());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(1, refDb.getReflogReader("refs/heads/new/name")
+				.getReverseEntries().size());
+		assertEquals("Branch: renamed b to new/name",
+				refDb.getReflogReader("refs/heads/new/name").getLastEntry()
+						.getComment());
 		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
 		assertEquals(oldHead, db.resolve(Constants.HEAD)); // unchanged
 	}
@@ -713,11 +735,15 @@ public void testRenameBranchHasPreviousLog() throws IOException {
 		assertEquals(Result.RENAMED, result);
 		assertEquals(rb, db.resolve("refs/heads/new/name"));
 		assertNull(db.resolve("refs/heads/b"));
-		assertEquals(2, db.getReflogReader("new/name").getReverseEntries().size());
-		assertEquals("Branch: renamed b to new/name", db.getReflogReader("new/name")
-				.getLastEntry().getComment());
-		assertEquals("Just a message", db.getReflogReader("new/name")
-				.getReverseEntries().get(1).getComment());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(2, refDb.getReflogReader("refs/heads/new/name")
+				.getReverseEntries().size());
+		assertEquals("Branch: renamed b to new/name",
+				refDb.getReflogReader("refs/heads/new/name").getLastEntry()
+						.getComment());
+		assertEquals("Just a message",
+				refDb.getReflogReader("refs/heads/new/name").getReverseEntries()
+						.get(1).getComment());
 		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
 		assertEquals(oldHead, db.resolve(Constants.HEAD)); // unchanged
 	}
@@ -737,13 +763,20 @@ public void testRenameCurrentBranch() throws IOException {
 		assertEquals(Result.RENAMED, result);
 		assertEquals(rb, db.resolve("refs/heads/new/name"));
 		assertNull(db.resolve("refs/heads/b"));
-		assertEquals("Branch: renamed b to new/name", db.getReflogReader(
-				"new/name").getLastEntry().getComment());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals("Branch: renamed b to new/name",
+				refDb.getReflogReader("refs/heads/new/name").getLastEntry()
+						.getComment());
 		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
 		assertEquals(rb, db.resolve(Constants.HEAD));
-		assertEquals(2, db.getReflogReader("new/name").getReverseEntries().size());
-		assertEquals("Branch: renamed b to new/name", db.getReflogReader("new/name").getReverseEntries().get(0).getComment());
-		assertEquals("Just a message", db.getReflogReader("new/name").getReverseEntries().get(1).getComment());
+		assertEquals(2, refDb.getReflogReader("refs/heads/new/name")
+				.getReverseEntries().size());
+		assertEquals("Branch: renamed b to new/name",
+				refDb.getReflogReader("refs/heads/new/name").getReverseEntries()
+						.get(0).getComment());
+		assertEquals("Just a message",
+				refDb.getReflogReader("refs/heads/new/name").getReverseEntries()
+						.get(1).getComment());
 	}
 
 	@Test
@@ -766,11 +799,17 @@ public void testRenameBranchAlsoInPack() throws IOException {
 		assertEquals(Result.RENAMED, result);
 		assertEquals(rb2, db.resolve("refs/heads/new/name"));
 		assertNull(db.resolve("refs/heads/b"));
-		assertEquals("Branch: renamed b to new/name", db.getReflogReader(
-				"new/name").getLastEntry().getComment());
-		assertEquals(3, db.getReflogReader("refs/heads/new/name").getReverseEntries().size());
-		assertEquals("Branch: renamed b to new/name", db.getReflogReader("refs/heads/new/name").getReverseEntries().get(0).getComment());
-		assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals("Branch: renamed b to new/name",
+				refDb.getReflogReader("refs/heads/new/name").getLastEntry()
+						.getComment());
+		assertEquals(3, refDb.getReflogReader("refs/heads/new/name")
+				.getReverseEntries().size());
+		assertEquals("Branch: renamed b to new/name",
+				refDb.getReflogReader("refs/heads/new/name").getReverseEntries()
+						.get(0).getComment());
+		assertEquals(0,
+				refDb.getReflogReader("HEAD").getReverseEntries().size());
 		// make sure b's log file is gone too.
 		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
 
@@ -789,9 +828,10 @@ public void tryRenameWhenLocked(String toLock, String fromName,
 		ObjectId oldfromId = db.resolve(fromName);
 		ObjectId oldHeadId = db.resolve(Constants.HEAD);
 		writeReflog(db, oldfromId, "Just a message", fromName);
-		List<ReflogEntry> oldFromLog = db
+		RefDatabase refDb = db.getRefDatabase();
+		List<ReflogEntry> oldFromLog = refDb
 				.getReflogReader(fromName).getReverseEntries();
-		List<ReflogEntry> oldHeadLog = oldHeadId != null ? db
+		List<ReflogEntry> oldHeadLog = oldHeadId != null ? refDb
 				.getReflogReader(Constants.HEAD).getReverseEntries() : null;
 
 		assertTrue("internal check, we have a log", new File(db.getDirectory(),
@@ -818,10 +858,10 @@ public void tryRenameWhenLocked(String toLock, String fromName,
 			assertEquals(oldHeadId, db.resolve(Constants.HEAD));
 			assertEquals(oldfromId, db.resolve(fromName));
 			assertNull(db.resolve(toName));
-			assertEquals(oldFromLog.toString(), db.getReflogReader(fromName)
+			assertEquals(oldFromLog.toString(), refDb.getReflogReader(fromName)
 					.getReverseEntries().toString());
 			if (oldHeadId != null && oldHeadLog != null)
-				assertEquals(oldHeadLog.toString(), db.getReflogReader(
+				assertEquals(oldHeadLog.toString(), refDb.getReflogReader(
 						Constants.HEAD).getReverseEntries().toString());
 		} finally {
 			lockFile.unlock();
@@ -942,15 +982,18 @@ public void testRenameRefNameColission1avoided() throws IOException {
 		assertEquals(Result.RENAMED, result);
 		assertNull(db.resolve("refs/heads/a"));
 		assertEquals(rb, db.resolve("refs/heads/a/b"));
-		assertEquals(3, db.getReflogReader("a/b").getReverseEntries().size());
-		assertEquals("Branch: renamed a to a/b", db.getReflogReader("a/b")
-				.getReverseEntries().get(0).getComment());
-		assertEquals("Just a message", db.getReflogReader("a/b")
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(3, refDb.getReflogReader("refs/heads/a/b")
+				.getReverseEntries().size());
+		assertEquals("Branch: renamed a to a/b",
+				refDb.getReflogReader("refs/heads/a/b").getReverseEntries()
+						.get(0).getComment());
+		assertEquals("Just a message", refDb.getReflogReader("refs/heads/a/b")
 				.getReverseEntries().get(1).getComment());
-		assertEquals("Setup", db.getReflogReader("a/b").getReverseEntries()
-				.get(2).getComment());
+		assertEquals("Setup", refDb.getReflogReader("refs/heads/a/b")
+				.getReverseEntries().get(2).getComment());
 		// same thing was logged to HEAD
-		assertEquals("Branch: renamed a to a/b", db.getReflogReader("HEAD")
+		assertEquals("Branch: renamed a to a/b", refDb.getReflogReader("HEAD")
 				.getReverseEntries().get(0).getComment());
 	}
 
@@ -978,15 +1021,20 @@ public void testRenameRefNameColission2avoided() throws IOException {
 
 		assertNull(db.resolve("refs/heads/prefix/a"));
 		assertEquals(rb, db.resolve("refs/heads/prefix"));
-		assertEquals(3, db.getReflogReader("prefix").getReverseEntries().size());
-		assertEquals("Branch: renamed prefix/a to prefix", db.getReflogReader(
-				"prefix").getReverseEntries().get(0).getComment());
-		assertEquals("Just a message", db.getReflogReader("prefix")
-				.getReverseEntries().get(1).getComment());
-		assertEquals("Setup", db.getReflogReader("prefix").getReverseEntries()
-				.get(2).getComment());
-		assertEquals("Branch: renamed prefix/a to prefix", db.getReflogReader(
-				"HEAD").getReverseEntries().get(0).getComment());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(3, refDb.getReflogReader("refs/heads/prefix")
+				.getReverseEntries().size());
+		assertEquals("Branch: renamed prefix/a to prefix",
+				refDb.getReflogReader("refs/heads/prefix").getReverseEntries()
+						.get(0).getComment());
+		assertEquals("Just a message",
+				refDb.getReflogReader("refs/heads/prefix").getReverseEntries()
+						.get(1).getComment());
+		assertEquals("Setup", refDb.getReflogReader("refs/heads/prefix")
+				.getReverseEntries().get(2).getComment());
+		assertEquals("Branch: renamed prefix/a to prefix",
+				refDb.getReflogReader("HEAD").getReverseEntries().get(0)
+						.getComment());
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java
index dc0e749..16645cb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java
@@ -27,6 +27,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
@@ -154,18 +155,22 @@ public void testReadRightLog() throws Exception {
 		setupReflog("logs/refs/heads/a", aLine);
 		setupReflog("logs/refs/heads/master", masterLine);
 		setupReflog("logs/HEAD", headLine);
-		assertEquals("branch: change to master", db.getReflogReader("master")
-				.getLastEntry().getComment());
-		assertEquals("branch: change to a", db.getReflogReader("a")
-				.getLastEntry().getComment());
-		assertEquals("branch: change to HEAD", db.getReflogReader("HEAD")
-				.getLastEntry().getComment());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals("branch: change to master",
+				refDb.getReflogReader("refs/heads/master").getLastEntry()
+						.getComment());
+		assertEquals("branch: change to a",
+				refDb.getReflogReader("refs/heads/a").getLastEntry()
+						.getComment());
+		assertEquals("branch: change to HEAD",
+				refDb.getReflogReader("HEAD").getLastEntry().getComment());
 	}
 
 	@Test
 	public void testReadLineWithMissingComment() throws Exception {
 		setupReflog("logs/refs/heads/master", oneLineWithoutComment);
-		final ReflogReader reader = db.getReflogReader("master");
+		final ReflogReader reader = db.getRefDatabase()
+				.getReflogReader("refs/heads/master");
 		ReflogEntry e = reader.getLastEntry();
 		assertEquals(ObjectId
 				.fromString("da85355dfc525c9f6f3927b876f379f46ccf826e"), e
@@ -183,15 +188,18 @@ public void testReadLineWithMissingComment() throws Exception {
 
 	@Test
 	public void testNoLog() throws Exception {
-		assertEquals(0, db.getReflogReader("master").getReverseEntries().size());
-		assertNull(db.getReflogReader("master").getLastEntry());
+		RefDatabase refDb = db.getRefDatabase();
+		assertEquals(0,
+				refDb.getReflogReader("refs/heads/master").getReverseEntries()
+						.size());
+		assertNull(refDb.getReflogReader("refs/heads/master").getLastEntry());
 	}
 
 	@Test
 	public void testCheckout() throws Exception {
 		setupReflog("logs/HEAD", switchBranch);
-		List<ReflogEntry> entries = db.getReflogReader(Constants.HEAD)
-				.getReverseEntries();
+		List<ReflogEntry> entries = db.getRefDatabase()
+				.getReflogReader(Constants.HEAD).getReverseEntries();
 		assertEquals(1, entries.size());
 		ReflogEntry entry = entries.get(0);
 		CheckoutEntry checkout = entry.parseCheckout();
@@ -238,7 +246,7 @@ public void testSpecificEntryNumber() throws Exception {
 
 	private void setupReflog(String logName, byte[] data)
 			throws FileNotFoundException, IOException {
-		File logfile = new File(db.getDirectory(), logName);
+		File logfile = new File(db.getCommonDirectory(), logName);
 		if (!logfile.getParentFile().mkdirs()
 				&& !logfile.getParentFile().isDirectory()) {
 			throw new IOException(
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java
index 8d0e99d..a836333 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java
@@ -16,6 +16,8 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -32,7 +34,7 @@ public void shouldFilterLineFeedFromMessage() throws Exception {
 		ReflogWriter writer =
 				new ReflogWriter((RefDirectory) db.getRefDatabase());
 		PersonIdent ident = new PersonIdent("John Doe", "john@doe.com",
-				1243028200000L, 120);
+				Instant.ofEpochMilli(1243028200000L), ZoneOffset.ofHours(2));
 		ObjectId oldId = ObjectId
 				.fromString("da85355dfc525c9f6f3927b876f379f46ccf826e");
 		ObjectId newId = ObjectId
@@ -48,7 +50,7 @@ public void shouldFilterLineFeedFromMessage() throws Exception {
 
 	private void readReflog(byte[] buffer)
 			throws FileNotFoundException, IOException {
-		File logfile = new File(db.getDirectory(), "logs/refs/heads/master");
+		File logfile = new File(db.getCommonDirectory(), "logs/refs/heads/master");
 		if (!logfile.getParentFile().mkdirs()
 				&& !logfile.getParentFile().isDirectory()) {
 			throw new IOException(
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
index 49e8a7b..e067beb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
@@ -28,6 +28,7 @@
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.time.Instant;
+import java.time.ZoneOffset;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -374,8 +375,10 @@ public void test008_FailOnWrongVersion() throws IOException {
 	public void test009_CreateCommitOldFormat() throws IOException {
 		final ObjectId treeId = insertTree(new TreeFormatter());
 		final CommitBuilder c = new CommitBuilder();
-		c.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c.setMessage("A Commit\n");
 		c.setTreeId(treeId);
 		assertEquals(treeId, c.getTreeId());
@@ -411,7 +414,8 @@ public void test020_createBlobTag() throws IOException {
 		final TagBuilder t = new TagBuilder();
 		t.setObjectId(emptyId, Constants.OBJ_BLOB);
 		t.setTag("test020");
-		t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60));
+		t.setTagger(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		t.setMessage("test020 tagged\n");
 		ObjectId actid = insertTag(t);
 		assertEquals("6759556b09fbb4fd8ae5e315134481cc25d46954", actid.name());
@@ -419,8 +423,9 @@ public void test020_createBlobTag() throws IOException {
 		RevTag mapTag = parseTag(actid);
 		assertEquals(Constants.OBJ_BLOB, mapTag.getObject().getType());
 		assertEquals("test020 tagged\n", mapTag.getFullMessage());
-		assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag
-				.getTaggerIdent());
+		assertEquals(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)),
+				mapTag.getTaggerIdent());
 		assertEquals("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", mapTag
 				.getObject().getId().name());
 	}
@@ -434,7 +439,8 @@ public void test021_createTreeTag() throws IOException {
 		final TagBuilder t = new TagBuilder();
 		t.setObjectId(almostEmptyTreeId, Constants.OBJ_TREE);
 		t.setTag("test021");
-		t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60));
+		t.setTagger(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		t.setMessage("test021 tagged\n");
 		ObjectId actid = insertTag(t);
 		assertEquals("b0517bc8dbe2096b419d42424cd7030733f4abe5", actid.name());
@@ -442,8 +448,9 @@ public void test021_createTreeTag() throws IOException {
 		RevTag mapTag = parseTag(actid);
 		assertEquals(Constants.OBJ_TREE, mapTag.getObject().getType());
 		assertEquals("test021 tagged\n", mapTag.getFullMessage());
-		assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag
-				.getTaggerIdent());
+		assertEquals(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)),
+				mapTag.getTaggerIdent());
 		assertEquals("417c01c8795a35b8e835113a85a5c0c1c77f67fb", mapTag
 				.getObject().getId().name());
 	}
@@ -455,17 +462,18 @@ public void test022_createCommitTag() throws IOException {
 		almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
 		final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
 		final CommitBuilder almostEmptyCommit = new CommitBuilder();
-		almostEmptyCommit.setAuthor(new PersonIdent(author, 1154236443000L,
-				-2 * 60)); // not exactly the same
-		almostEmptyCommit.setCommitter(new PersonIdent(author, 1154236443000L,
-				-2 * 60));
+		almostEmptyCommit.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-2)));
+		almostEmptyCommit.setCommitter(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-2)));
 		almostEmptyCommit.setMessage("test022\n");
 		almostEmptyCommit.setTreeId(almostEmptyTreeId);
 		ObjectId almostEmptyCommitId = insertCommit(almostEmptyCommit);
 		final TagBuilder t = new TagBuilder();
 		t.setObjectId(almostEmptyCommitId, Constants.OBJ_COMMIT);
 		t.setTag("test022");
-		t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60));
+		t.setTagger(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		t.setMessage("test022 tagged\n");
 		ObjectId actid = insertTag(t);
 		assertEquals("0ce2ebdb36076ef0b38adbe077a07d43b43e3807", actid.name());
@@ -473,8 +481,9 @@ public void test022_createCommitTag() throws IOException {
 		RevTag mapTag = parseTag(actid);
 		assertEquals(Constants.OBJ_COMMIT, mapTag.getObject().getType());
 		assertEquals("test022 tagged\n", mapTag.getFullMessage());
-		assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag
-				.getTaggerIdent());
+		assertEquals(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)),
+				mapTag.getTaggerIdent());
 		assertEquals("b5d3b45a96b340441f5abb9080411705c51cc86c", mapTag
 				.getObject().getId().name());
 	}
@@ -488,9 +497,9 @@ public void test023_createCommitNonAnullii() throws IOException {
 		CommitBuilder commit = new CommitBuilder();
 		commit.setTreeId(almostEmptyTreeId);
 		commit.setAuthor(new PersonIdent("Joe H\u00e4cker", "joe@example.com",
-				4294967295000L, 60));
+				Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
 		commit.setCommitter(new PersonIdent("Joe Hacker", "joe2@example.com",
-				4294967295000L, 60));
+				Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
 		commit.setEncoding(UTF_8);
 		commit.setMessage("\u00dcbergeeks");
 		ObjectId cid = insertCommit(commit);
@@ -509,9 +518,9 @@ public void test024_createCommitNonAscii() throws IOException {
 		CommitBuilder commit = new CommitBuilder();
 		commit.setTreeId(almostEmptyTreeId);
 		commit.setAuthor(new PersonIdent("Joe H\u00e4cker", "joe@example.com",
-				4294967295000L, 60));
+				Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
 		commit.setCommitter(new PersonIdent("Joe Hacker", "joe2@example.com",
-				4294967295000L, 60));
+				Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
 		commit.setEncoding(ISO_8859_1);
 		commit.setMessage("\u00dcbergeeks");
 		ObjectId cid = insertCommit(commit);
@@ -544,8 +553,10 @@ public void test026_CreateCommitMultipleparents() throws IOException {
 				.fromString("00b1f73724f493096d1ffa0b0f1f1482dbb8c936"), treeId);
 
 		final CommitBuilder c1 = new CommitBuilder();
-		c1.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c1.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c1.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c1.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c1.setMessage("A Commit\n");
 		c1.setTreeId(treeId);
 		assertEquals(treeId, c1.getTreeId());
@@ -555,8 +566,10 @@ public void test026_CreateCommitMultipleparents() throws IOException {
 		assertEquals(cmtid1, actid1);
 
 		final CommitBuilder c2 = new CommitBuilder();
-		c2.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c2.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c2.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c2.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c2.setMessage("A Commit 2\n");
 		c2.setTreeId(treeId);
 		assertEquals(treeId, c2.getTreeId());
@@ -577,8 +590,10 @@ public void test026_CreateCommitMultipleparents() throws IOException {
 		assertEquals(actid1, rm2.getParent(0));
 
 		final CommitBuilder c3 = new CommitBuilder();
-		c3.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c3.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c3.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c3.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c3.setMessage("A Commit 3\n");
 		c3.setTreeId(treeId);
 		assertEquals(treeId, c3.getTreeId());
@@ -600,8 +615,10 @@ public void test026_CreateCommitMultipleparents() throws IOException {
 		assertEquals(actid2, rm3.getParent(1));
 
 		final CommitBuilder c4 = new CommitBuilder();
-		c4.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
-		c4.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+		c4.setAuthor(new PersonIdent(author,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+		c4.setCommitter(new PersonIdent(committer,
+				Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
 		c4.setMessage("A Commit 4\n");
 		c4.setTreeId(treeId);
 		assertEquals(treeId, c3.getTreeId());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java
new file mode 100644
index 0000000..82f3eb1
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.midx;
+
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_LARGEOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_REVINDEX;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.eclipse.jgit.junit.FakeIndexFactory.IndexObject;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.util.NB;
+import org.junit.Test;
+
+public class MultiPackIndexWriterTest {
+
+	@Test
+	public void write_allSmallOffsets() throws IOException {
+		PackIndex index1 = indexOf(
+				object("0000000000000000000000000000000000000001", 500),
+				object("0000000000000000000000000000000000000003", 1500),
+				object("0000000000000000000000000000000000000005", 3000));
+		PackIndex index2 = indexOf(
+				object("0000000000000000000000000000000000000002", 500),
+				object("0000000000000000000000000000000000000004", 1500),
+				object("0000000000000000000000000000000000000006", 3000));
+
+		Map<String, PackIndex> data = Map.of("packname1", index1, "packname2",
+				index2);
+
+		MultiPackIndexWriter writer = new MultiPackIndexWriter();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		writer.write(NullProgressMonitor.INSTANCE, out, data);
+		// header (12 bytes)
+		// + chunkHeader (6 * 12 bytes)
+		// + fanout table (256 * 4 bytes)
+		// + OIDs (6 * 20 bytes)
+		// + (pack, offset) pairs (6 * 8)
+		// + RIDX (6 * 4 bytes)
+		// + packfile names (2 * 10)
+		// + checksum (20)
+		assertEquals(1340, out.size());
+		List<Integer> chunkIds = readChunkIds(out);
+		assertEquals(5, chunkIds.size());
+		assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+		assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+		assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+		assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+		assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+	}
+
+	@Test
+	public void write_smallOffset_limit() throws IOException {
+		PackIndex index1 = indexOf(
+				object("0000000000000000000000000000000000000001", 500),
+				object("0000000000000000000000000000000000000003", 1500),
+				object("0000000000000000000000000000000000000005", (1L << 32) -1));
+		PackIndex index2 = indexOf(
+				object("0000000000000000000000000000000000000002", 500),
+				object("0000000000000000000000000000000000000004", 1500),
+				object("0000000000000000000000000000000000000006", 3000));
+		Map<String, PackIndex> data =
+				Map.of("packname1", index1, "packname2", index2);
+
+		MultiPackIndexWriter writer = new MultiPackIndexWriter();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		writer.write(NullProgressMonitor.INSTANCE, out, data);
+		// header (12 bytes)
+		// + chunkHeader (6 * 12 bytes)
+		// + fanout table (256 * 4 bytes)
+		// + OIDs (6 * 20 bytes)
+		// + (pack, offset) pairs (6 * 8)
+		// + RIDX (6 * 4 bytes)
+		// + packfile names (2 * 10)
+		// + checksum (20)
+		assertEquals(1340, out.size());
+		List<Integer> chunkIds = readChunkIds(out);
+		assertEquals(5, chunkIds.size());
+		assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+		assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+		assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+		assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+		assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+	}
+
+	@Test
+	public void write_largeOffset() throws IOException {
+		PackIndex index1 = indexOf(
+				object("0000000000000000000000000000000000000001", 500),
+				object("0000000000000000000000000000000000000003", 1500),
+				object("0000000000000000000000000000000000000005", 1L << 32));
+		PackIndex index2 = indexOf(
+				object("0000000000000000000000000000000000000002", 500),
+				object("0000000000000000000000000000000000000004", 1500),
+				object("0000000000000000000000000000000000000006", 3000));
+		Map<String, PackIndex> data =
+				Map.of("packname1", index1, "packname2", index2);
+
+		MultiPackIndexWriter writer = new MultiPackIndexWriter();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		writer.write(NullProgressMonitor.INSTANCE, out, data);
+		// header (12 bytes)
+		// + chunkHeader (7 * 12 bytes)
+		// + fanout table (256 * 4 bytes)
+		// + OIDs (6 * 20 bytes)
+		// + (pack, offset) pairs (6 * 8)
+		// + (large-offset) (1 * 8)
+		// + RIDX (6 * 4 bytes)
+		// + packfile names (2 * 10)
+		// + checksum (20)
+		assertEquals(1360, out.size());
+		List<Integer> chunkIds = readChunkIds(out);
+		assertEquals(6, chunkIds.size());
+		assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+		assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+		assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+		assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_LARGEOFFSETS));
+		assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+		assertEquals(5, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+	}
+
+	private List<Integer> readChunkIds(ByteArrayOutputStream out) {
+		List<Integer> chunkIds = new ArrayList<>();
+		byte[] raw = out.toByteArray();
+		int numChunks = raw[6];
+		int position = 12;
+		for (int i = 0; i < numChunks; i++) {
+			chunkIds.add(NB.decodeInt32(raw, position));
+			position += CHUNK_LOOKUP_WIDTH;
+		}
+		return chunkIds;
+	}
+
+	private static PackIndex indexOf(IndexObject... objs) {
+		return FakeIndexFactory.indexOf(Arrays.asList(objs));
+	}
+
+	private static IndexObject object(String name, long offset) {
+		return new IndexObject(name, offset);
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java
new file mode 100644
index 0000000..1d8bde0
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.midx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.eclipse.jgit.junit.FakeIndexFactory.IndexObject;
+import org.junit.Test;
+
+public class PackIndexMergerTest {
+
+	@Test
+	public void rawIterator_noDuplicates() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000001", 500),
+				oidOffset("0000000000000000000000000000000000000005", 12),
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndex idxTwo = indexOf(
+				oidOffset("0000000000000000000000000000000000000002", 501),
+				oidOffset("0000000000000000000000000000000000000003", 13),
+				oidOffset("0000000000000000000000000000000000000015", 1501));
+		PackIndex idxThree = indexOf(
+				oidOffset("0000000000000000000000000000000000000004", 502),
+				oidOffset("0000000000000000000000000000000000000007", 14),
+				oidOffset("0000000000000000000000000000000000000012", 1502));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree));
+		assertEquals(9, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.rawIterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 501);
+		assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 13);
+		assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 502);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 14);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000012", 2,
+				1502);
+		assertNextEntry(it, "0000000000000000000000000000000000000015", 1,
+				1501);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void rawIterator_allDuplicates() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000001", 500),
+				oidOffset("0000000000000000000000000000000000000005", 12),
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxOne, "p3", idxOne));
+		assertEquals(3, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.rawIterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 1, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 2, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 1, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 2, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 1,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 2,
+				1500);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void bySha1Iterator_noDuplicates() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000001", 500),
+				oidOffset("0000000000000000000000000000000000000005", 12),
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndex idxTwo = indexOf(
+				oidOffset("0000000000000000000000000000000000000002", 501),
+				oidOffset("0000000000000000000000000000000000000003", 13),
+				oidOffset("0000000000000000000000000000000000000015", 1501));
+		PackIndex idxThree = indexOf(
+				oidOffset("0000000000000000000000000000000000000004", 502),
+				oidOffset("0000000000000000000000000000000000000007", 14),
+				oidOffset("0000000000000000000000000000000000000012", 1502));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree));
+		assertEquals(9, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 501);
+		assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 13);
+		assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 502);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 14);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000012", 2,
+				1502);
+		assertNextEntry(it, "0000000000000000000000000000000000000015", 1,
+				1501);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void bySha1Iterator_allDuplicates() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000001", 500),
+				oidOffset("0000000000000000000000000000000000000005", 12),
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxOne, "p3", idxOne));
+		assertEquals(3, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void bySha1Iterator_differentIndexSizes() {
+		PackIndex idxOne = indexOf(
+				oidOffset("0000000000000000000000000000000000000010", 1500));
+		PackIndex idxTwo = indexOf(
+				oidOffset("0000000000000000000000000000000000000002", 500),
+				oidOffset("0000000000000000000000000000000000000003", 12));
+		PackIndex idxThree = indexOf(
+				oidOffset("0000000000000000000000000000000000000004", 500),
+				oidOffset("0000000000000000000000000000000000000007", 12),
+				oidOffset("0000000000000000000000000000000000000012", 1500));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree));
+		assertEquals(6, merger.getUniqueObjectCount());
+		assertEquals(3, merger.getPackCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator();
+		assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 500);
+		assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 12);
+		assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+				1500);
+		assertNextEntry(it, "0000000000000000000000000000000000000012", 2,
+				1500);
+		assertFalse(it.hasNext());
+	}
+
+	@Test
+	public void merger_noIndexes() {
+		PackIndexMerger merger = new PackIndexMerger(Map.of());
+		assertEquals(0, merger.getUniqueObjectCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		assertTrue(merger.getPackNames().isEmpty());
+		assertEquals(0, merger.getPackCount());
+		assertFalse(merger.bySha1Iterator().hasNext());
+	}
+
+	@Test
+	public void merger_emptyIndexes() {
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", indexOf(), "p2", indexOf()));
+		assertEquals(0, merger.getUniqueObjectCount());
+		assertFalse(merger.needsLargeOffsetsChunk());
+		assertEquals(2, merger.getPackNames().size());
+		assertEquals(2, merger.getPackCount());
+		assertFalse(merger.bySha1Iterator().hasNext());
+	}
+
+	@Test
+	public void bySha1Iterator_largeOffsets_needsChunk() {
+		PackIndex idx1 = indexOf(
+				oidOffset("0000000000000000000000000000000000000002", 1L << 32),
+				oidOffset("0000000000000000000000000000000000000004", 12));
+		PackIndex idx2 = indexOf(oidOffset(
+				"0000000000000000000000000000000000000003", (1L << 31) + 10));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idx1, "p2", idx2));
+		assertTrue(merger.needsLargeOffsetsChunk());
+		assertEquals(2, merger.getOffsetsOver31BitsCount());
+		assertEquals(3, merger.getUniqueObjectCount());
+	}
+
+	@Test
+	public void bySha1Iterator_largeOffsets_noChunk() {
+		// If no value is over 2^32-1, then we don't need large offset
+		PackIndex idx1 = indexOf(
+				oidOffset("0000000000000000000000000000000000000002",
+						(1L << 31) + 15),
+				oidOffset("0000000000000000000000000000000000000004", 12));
+		PackIndex idx2 = indexOf(oidOffset(
+				"0000000000000000000000000000000000000003", (1L << 31) + 10));
+		PackIndexMerger merger = new PackIndexMerger(
+				Map.of("p1", idx1, "p2", idx2));
+		assertFalse(merger.needsLargeOffsetsChunk());
+		assertEquals(2, merger.getOffsetsOver31BitsCount());
+		assertEquals(3, merger.getUniqueObjectCount());
+	}
+
+	private static void assertNextEntry(
+			Iterator<PackIndexMerger.MidxMutableEntry> it, String oid,
+			int packId, long offset) {
+		assertTrue(it.hasNext());
+		PackIndexMerger.MidxMutableEntry e = it.next();
+		assertEquals(oid, e.getObjectId().name());
+		assertEquals(packId, e.getPackId());
+		assertEquals(offset, e.getOffset());
+	}
+
+	private static IndexObject oidOffset(String oid, long offset) {
+		return new IndexObject(oid, offset);
+	}
+
+	private static PackIndex indexOf(IndexObject... objs) {
+		return FakeIndexFactory.indexOf(Arrays.asList(objs));
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java
new file mode 100644
index 0000000..917288a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.midx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.junit.Test;
+
+public class PackIndexPeekIteratorTest {
+    @Test
+    public void next() {
+        PackIndex index1 = indexOf(
+                object("0000000000000000000000000000000000000001", 500),
+                object("0000000000000000000000000000000000000003", 1500),
+                object("0000000000000000000000000000000000000005", 3000));
+        PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1);
+        assertEquals("0000000000000000000000000000000000000001", it.next().name());
+        assertEquals("0000000000000000000000000000000000000003", it.next().name());
+        assertEquals("0000000000000000000000000000000000000005", it.next().name());
+        assertNull(it.next());
+    }
+
+    @Test
+    public void peek_doesNotAdvance() {
+        PackIndex index1 = indexOf(
+                object("0000000000000000000000000000000000000001", 500),
+                object("0000000000000000000000000000000000000003", 1500),
+                object("0000000000000000000000000000000000000005", 3000));
+        PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1);
+        it.next();
+        assertEquals("0000000000000000000000000000000000000001", it.peek().name());
+        assertEquals("0000000000000000000000000000000000000001", it.peek().name());
+        it.next();
+        assertEquals("0000000000000000000000000000000000000003", it.peek().name());
+        assertEquals("0000000000000000000000000000000000000003", it.peek().name());
+        it.next();
+        assertEquals("0000000000000000000000000000000000000005", it.peek().name());
+        assertEquals("0000000000000000000000000000000000000005", it.peek().name());
+        it.next();
+        assertNull(it.peek());
+        assertNull(it.peek());
+    }
+
+    @Test
+    public void empty() {
+        PackIndex index1 = indexOf();
+        PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1);
+        assertNull(it.next());
+        assertNull(it.peek());
+    }
+
+    private static PackIndex indexOf(FakeIndexFactory.IndexObject... objs) {
+        return FakeIndexFactory.indexOf(Arrays.asList(objs));
+    }
+
+    private static FakeIndexFactory.IndexObject object(String name, long offset) {
+        return new FakeIndexFactory.IndexObject(name, offset);
+    }
+}
\ No newline at end of file
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 ea0d92a..a54002b 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
@@ -29,6 +29,8 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -175,7 +177,8 @@ public void hasObjMapRefsSmallTable() throws IOException {
 
 	@Test
 	public void hasObjLogs() throws IOException {
-		PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 		String msg = "test";
 		ReftableConfig cfg = new ReftableConfig();
 		cfg.setIndexObjects(false);
@@ -617,7 +620,8 @@ public void invalidReflogWriteOrderUpdateIndex() throws IOException {
 			.setMinUpdateIndex(1)
 			.setMaxUpdateIndex(2)
 			.begin();
-		PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 		String msg = "test";
 
 		writer.writeLog(MASTER, 1, who, ObjectId.zeroId(), id(1), msg);
@@ -633,7 +637,8 @@ public void invalidReflogWriteOrderName() throws IOException {
 			.setMinUpdateIndex(1)
 			.setMaxUpdateIndex(1)
 			.begin();
-		PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 		String msg = "test";
 
 		writer.writeLog(NEXT, 1, who, ObjectId.zeroId(), id(1), msg);
@@ -647,7 +652,8 @@ public void invalidReflogWriteOrderName() throws IOException {
 	public void withReflog() throws IOException {
 		Ref master = ref(MASTER, 1);
 		Ref next = ref(NEXT, 2);
-		PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 		String msg = "test";
 
 		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@@ -712,11 +718,14 @@ public void reflogReader() throws IOException {
 		writer.writeRef(master);
 		writer.writeRef(next);
 
-		PersonIdent who1 = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who1 = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 		writer.writeLog(MASTER, 3, who1, ObjectId.zeroId(), id(1), "1");
-		PersonIdent who2 = new PersonIdent("Log", "Ger", 1500079710, -8 * 60);
+		PersonIdent who2 = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 		writer.writeLog(MASTER, 2, who2, id(1), id(2), "2");
-		PersonIdent who3 = new PersonIdent("Log", "Ger", 1500079711, -8 * 60);
+		PersonIdent who3 = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 		writer.writeLog(MASTER, 1, who3, id(2), id(3), "3");
 
 		writer.finish();
@@ -753,7 +762,8 @@ public void allRefs() throws IOException {
 				.setMaxUpdateIndex(1)
 				.setConfig(cfg)
 				.begin();
-		PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 
 		// Fill out the 1st ref block.
 		List<String> names = new ArrayList<>();
@@ -782,7 +792,8 @@ public void allRefs() throws IOException {
 
 	@Test
 	public void reflogSeek() throws IOException {
-		PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who = new PersonIdent("Log", "Ger",
+				Instant.ofEpochSecond(1500079709), ZoneOffset.ofHours(-8));
 		String msg = "test";
 		String msgNext = "test next";
 
@@ -827,7 +838,8 @@ public void reflogSeek() throws IOException {
 
 	@Test
 	public void reflogSeekPrefix() throws IOException {
-		PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 
 		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
 		ReftableWriter writer = new ReftableWriter(buffer)
@@ -850,7 +862,8 @@ public void reflogSeekPrefix() throws IOException {
 
 	@Test
 	public void onlyReflog() throws IOException {
-		PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 		String msg = "test";
 
 		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@@ -916,7 +929,8 @@ public void logScan() throws IOException {
 			writer.writeRef(ref);
 		}
 
-		PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+		PersonIdent who = new PersonIdent("Log", "Ger",
+				Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
 		for (Ref ref : refs) {
 			writer.writeLog(ref.getName(), 1, who,
 					ObjectId.zeroId(), ref.getObjectId(),
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java
index 450b753..1581d49 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java
@@ -11,6 +11,7 @@
 package org.eclipse.jgit.junit;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.time.Instant.EPOCH;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -18,7 +19,6 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
-import java.util.Date;
 import java.util.regex.Pattern;
 
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -199,8 +199,8 @@ public void amendRef() throws Exception {
 		assertEquals(orig.getAuthorIdent(), amended.getAuthorIdent());
 
 		// Committer name/email is the same, but time was incremented.
-		assertEquals(new PersonIdent(orig.getCommitterIdent(), new Date(0)),
-				new PersonIdent(amended.getCommitterIdent(), new Date(0)));
+		assertEquals(new PersonIdent(orig.getCommitterIdent(), EPOCH),
+				new PersonIdent(amended.getCommitterIdent(), EPOCH));
 		assertTrue(orig.getCommitTime() < amended.getCommitTime());
 
 		assertEquals("foo contents", blobAsString(amended, "foo"));
@@ -275,9 +275,9 @@ public void cherryPick() throws Exception {
 		RevCommit toPick = tr.commit()
 				.parent(tr.commit().create()) // Can't cherry-pick root.
 				.author(new PersonIdent("Cherrypick Author", "cpa@example.com",
-						tr.getDate(), tr.getTimeZone()))
+						tr.getInstant(), tr.getTimeZoneId()))
 				.author(new PersonIdent("Cherrypick Committer", "cpc@example.com",
-						tr.getDate(), tr.getTimeZone()))
+						tr.getInstant(), tr.getTimeZoneId()))
 				.message("message to cherry-pick")
 				.add("bar", "bar contents\n")
 				.create();
@@ -294,8 +294,8 @@ public void cherryPick() throws Exception {
 		assertEquals(toPick.getAuthorIdent(), result.getAuthorIdent());
 
 		// Committer name/email matches default, and time was incremented.
-		assertEquals(new PersonIdent(head.getCommitterIdent(), new Date(0)),
-				new PersonIdent(result.getCommitterIdent(), new Date(0)));
+		assertEquals(new PersonIdent(head.getCommitterIdent(), EPOCH),
+				new PersonIdent(result.getCommitterIdent(), EPOCH));
 		assertTrue(toPick.getCommitTime() < result.getCommitTime());
 
 		assertEquals("message to cherry-pick", result.getFullMessage());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
index 31940a1..06fee8e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
@@ -1636,6 +1636,47 @@ public void testCoreCommitGraphConfig() {
 		assertFalse(config.get(CoreConfig.KEY).enableCommitGraph());
 	}
 
+	@Test
+	public void testGetNoDefaultBoolean() {
+		Config config = new Config();
+		assertNull(config.getBoolean("foo", "bar"));
+		assertNull(config.getBoolean("foo", "bar", "baz"));
+	}
+
+	@Test
+	public void testGetNoDefaultEnum() {
+		Config config = new Config();
+		assertNull(config.getEnum(new TestEnum[] { TestEnum.ONE_TWO }, "foo",
+				"bar", "baz"));
+	}
+
+	@Test
+	public void testGetNoDefaultInt() {
+		Config config = new Config();
+		assertNull(config.getInt("foo", "bar"));
+		assertNull(config.getInt("foo", "bar", "baz"));
+	}
+	@Test
+	public void testGetNoDefaultIntInRange() {
+		Config config = new Config();
+		assertNull(config.getIntInRange("foo", "bar", 1, 5));
+		assertNull(config.getIntInRange("foo", "bar", "baz", 1, 5));
+	}
+
+	@Test
+	public void testGetNoDefaultLong() {
+		Config config = new Config();
+		assertNull(config.getLong("foo", "bar"));
+		assertNull(config.getLong("foo", "bar", "baz"));
+	}
+
+	@Test
+	public void testGetNoDefaultTimeUnit() {
+		Config config = new Config();
+		assertNull(config.getTimeUnit("foo", "bar", "baz",
+				TimeUnit.SECONDS));
+	}
+
 	private static void assertValueRoundTrip(String value)
 			throws ConfigInvalidException {
 		assertValueRoundTrip(value, value);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java
index 32f6766..5c2b190 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java
@@ -96,6 +96,16 @@ public void testGetKeyFormat_x509() throws Exception {
 	}
 
 	@Test
+	public void testGetKeyFormat_ssh() throws Exception {
+		Config c = parse("" //
+				+ "[gpg]\n" //
+				+ "  format = ssh\n" //
+		);
+
+		assertEquals(GpgConfig.GpgFormat.SSH, new GpgConfig(c).getKeyFormat());
+	}
+
+	@Test
 	public void testGetSigningKey() throws Exception {
 		Config c = parse("" //
 				+ "[user]\n" //
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java
index 2b7b6ca..cd98606 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java
@@ -2,7 +2,7 @@
  * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
- * Copyright (C) 2013, Robin Stocker <robin@nibor.org> and others
+ * Copyright (C) 2013, 2025 Robin Stocker <robin@nibor.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
@@ -539,7 +539,7 @@ public void testAssumeUnchanged() throws Exception {
 			assertTrue(diff.getAssumeUnchanged().contains("file3"));
 			assertTrue(diff.getModified().contains("file"));
 
-			git.add().addFilepattern(".").call();
+			git.add().addFilepattern(".").setAll(false).call();
 
 			iterator = new FileTreeIterator(db);
 			diff = new IndexDiff(db, Constants.HEAD, iterator);
@@ -551,6 +551,18 @@ public void testAssumeUnchanged() throws Exception {
 			assertTrue(diff.getAssumeUnchanged().contains("file3"));
 			assertTrue(diff.getChanged().contains("file"));
 			assertEquals(Collections.EMPTY_SET, diff.getUntrackedFolders());
+
+			git.add().addFilepattern(".").call();
+
+			iterator = new FileTreeIterator(db);
+			diff = new IndexDiff(db, Constants.HEAD, iterator);
+			diff.diff();
+			assertEquals(1, diff.getAssumeUnchanged().size());
+			assertEquals(0, diff.getModified().size());
+			assertEquals(1, diff.getChanged().size());
+			assertTrue(diff.getAssumeUnchanged().contains("file2"));
+			assertTrue(diff.getChanged().contains("file"));
+			assertEquals(Collections.EMPTY_SET, diff.getUntrackedFolders());
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java
index 21032c3..d6f0b03 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java
@@ -16,6 +16,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import java.nio.ByteBuffer;
 import java.util.Locale;
 
 import org.eclipse.jgit.errors.InvalidObjectIdException;
@@ -153,4 +154,16 @@ public void testSetByte() {
 			assertEquals(ObjectId.fromRaw(exp).name(), id.name());
 		}
 	}
+
+	@Test
+	public void test_toFromByteBuffer_raw() {
+		ObjectId oid = ObjectId
+				.fromString("ff00eedd003713bb1bb26b808ec9312548e73946");
+		ByteBuffer anObject = ByteBuffer.allocate(Constants.OBJECT_ID_LENGTH);
+		oid.copyRawTo(anObject);
+		anObject.flip();
+
+		ObjectId actual = ObjectId.fromRaw(anObject);
+		assertEquals(oid.name(), actual.name());
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
index 97da175..943a68b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
@@ -55,7 +55,8 @@ public void testNewIdentInstant() {
 				p.getWhenAsInstant());
 		assertEquals("A U Thor <author@example.com> 1142878501 -0500",
 				p.toExternalString());
-		assertEquals(ZoneId.of("GMT-05:00"), p.getZoneId());
+		assertEquals(ZoneId.of("GMT-05:00").getRules().getOffset(
+				Instant.ofEpochMilli(1142878501000L)), p.getZoneOffset());
 	}
 
 	@Test
@@ -69,7 +70,8 @@ public void testNewIdentInstant2() {
 				p.getWhenAsInstant());
 		assertEquals("A U Thor <author@example.com> 1142878501 +0530",
 				p.toExternalString());
-		assertEquals(ZoneId.of("GMT+05:30"), p.getZoneId());
+		assertEquals(ZoneId.of("GMT+05:30").getRules().getOffset(
+				Instant.ofEpochMilli(1142878501000L)), p.getZoneOffset());
 	}
 
 	@SuppressWarnings("unused")
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java
index b02f245..85f9612 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java
@@ -71,6 +71,11 @@ public List<Ref> getAdditionalRefs() throws IOException {
 		}
 
 		@Override
+		public ReflogReader getReflogReader(Ref ref) throws IOException {
+			return null;
+		}
+
+		@Override
 		public void create() throws IOException {
 			// Not needed
 		}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java
index 854180e..a93937e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java
@@ -16,6 +16,9 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneOffset;
 
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -24,22 +27,23 @@
 public class ReflogConfigTest extends RepositoryTestCase {
 	@Test
 	public void testlogAllRefUpdates() throws Exception {
-		long commitTime = 1154236443000L;
-		int tz = -4 * 60;
+		Instant commitTime = Instant.ofEpochSecond(1154236443L);
+		ZoneOffset tz = ZoneOffset.ofHours(-4);
 
 		// check that there are no entries in the reflog and turn off writing
 		// reflogs
-		assertTrue(db.getReflogReader(Constants.HEAD).getReverseEntries()
+		RefDatabase refDb = db.getRefDatabase();
+		assertTrue(refDb.getReflogReader(Constants.HEAD).getReverseEntries()
 				.isEmpty());
-		final FileBasedConfig cfg = db.getConfig();
+		FileBasedConfig cfg = db.getConfig();
 		cfg.setBoolean("core", null, "logallrefupdates", false);
 		cfg.save();
 
 		// do one commit and check that reflog size is 0: no reflogs should be
 		// written
 		commit("A Commit\n", commitTime, tz);
-		commitTime += 60 * 1000;
-		assertTrue("Reflog for HEAD still contain no entry", db
+		commitTime = commitTime.plus(Duration.ofMinutes(1));
+		assertTrue("Reflog for HEAD still contain no entry", refDb
 				.getReflogReader(Constants.HEAD).getReverseEntries().isEmpty());
 
 		// set the logAllRefUpdates parameter to true and check it
@@ -52,10 +56,10 @@ public void testlogAllRefUpdates() throws Exception {
 
 		// do one commit and check that reflog size is increased to 1
 		commit("A Commit\n", commitTime, tz);
-		commitTime += 60 * 1000;
-		assertTrue(
-				"Reflog for HEAD should contain one entry",
-				db.getReflogReader(Constants.HEAD).getReverseEntries().size() == 1);
+		commitTime = commitTime.plus(Duration.ofMinutes(1));
+		assertTrue("Reflog for HEAD should contain one entry",
+				refDb.getReflogReader(Constants.HEAD).getReverseEntries()
+						.size() == 1);
 
 		// set the logAllRefUpdates parameter to false and check it
 		cfg.setBoolean("core", null, "logallrefupdates", false);
@@ -67,10 +71,10 @@ public void testlogAllRefUpdates() throws Exception {
 
 		// do one commit and check that reflog size is 2
 		commit("A Commit\n", commitTime, tz);
-		commitTime += 60 * 1000;
-		assertTrue(
-				"Reflog for HEAD should contain two entries",
-				db.getReflogReader(Constants.HEAD).getReverseEntries().size() == 2);
+		commitTime = commitTime.plus(Duration.ofMinutes(1));
+		assertTrue("Reflog for HEAD should contain two entries",
+				refDb.getReflogReader(Constants.HEAD).getReverseEntries()
+						.size() == 2);
 
 		// set the logAllRefUpdates parameter to false and check it
 		cfg.setEnum("core", null, "logallrefupdates",
@@ -84,13 +88,13 @@ public void testlogAllRefUpdates() throws Exception {
 		// do one commit and check that reflog size is 3
 		commit("A Commit\n", commitTime, tz);
 		assertTrue("Reflog for HEAD should contain three entries",
-				db.getReflogReader(Constants.HEAD).getReverseEntries()
+				refDb.getReflogReader(Constants.HEAD).getReverseEntries()
 						.size() == 3);
 	}
 
-	private void commit(String commitMsg, long commitTime, int tz)
+	private void commit(String commitMsg, Instant commitTime, ZoneOffset tz)
 			throws IOException {
-		final CommitBuilder commit = new CommitBuilder();
+		CommitBuilder commit = new CommitBuilder();
 		commit.setAuthor(new PersonIdent(author, commitTime, tz));
 		commit.setCommitter(new PersonIdent(committer, commitTime, tz));
 		commit.setMessage(commitMsg);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java
index ae811f8..8865ba9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java
@@ -15,6 +15,9 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import java.time.Instant;
+import java.time.ZoneOffset;
+
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -162,7 +165,8 @@ private static ObjectId commit(final ObjectInserter odi,
 			final ObjectId[] parentIds) throws Exception {
 		final CommitBuilder c = new CommitBuilder();
 		c.setTreeId(treeB.writeTree(odi));
-		c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0));
+		c.setAuthor(new PersonIdent("A U Thor", "a.u.thor",
+				Instant.ofEpochSecond(1), ZoneOffset.UTC));
 		c.setCommitter(c.getAuthor());
 		c.setParentIds(parentIds);
 		c.setMessage("Tree " + c.getTreeId().name());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java
index f410960..b1998f3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java
@@ -15,6 +15,8 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
 
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.dircache.DirCache;
@@ -357,7 +359,8 @@ private static ObjectId commit(ObjectInserter odi, DirCache treeB,
 			ObjectId[] parentIds) throws Exception {
 		CommitBuilder c = new CommitBuilder();
 		c.setTreeId(treeB.writeTree(odi));
-		c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0));
+		c.setAuthor(new PersonIdent("A U Thor", "a.u.thor",
+				Instant.ofEpochSecond(1), ZoneOffset.UTC));
 		c.setCommitter(c.getAuthor());
 		c.setParentIds(parentIds);
 		c.setMessage("Tree " + c.getTreeId().name());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java
new file mode 100644
index 0000000..3a8af7a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2024 Qualcomm Innovation Center, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.merge;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.lib.Constants;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public class MergeAlgorithmUnionTest {
+	MergeFormatter fmt = new MergeFormatter();
+
+	private final boolean newlineAtEnd;
+
+	@DataPoints
+	public static boolean[] newlineAtEndDataPoints = { false, true };
+
+	public MergeAlgorithmUnionTest(boolean newlineAtEnd) {
+		this.newlineAtEnd = newlineAtEnd;
+	}
+
+	/**
+	 * Check for a conflict where the second text was changed similar to the
+	 * first one, but the second texts modification covers one more line.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testTwoConflictingModifications() throws IOException {
+		assertEquals(t("abZZdefghij"),
+				merge("abcdefghij", "abZdefghij", "aZZdefghij"));
+	}
+
+	/**
+	 * Test a case where we have three consecutive chunks. The first text
+	 * modifies all three chunks. The second text modifies the first and the
+	 * last chunk. This should be reported as one conflicting region.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testOneAgainstTwoConflictingModifications() throws IOException {
+		assertEquals(t("aZZcZefghij"),
+				merge("abcdefghij", "aZZZefghij", "aZcZefghij"));
+	}
+
+	/**
+	 * Test a merge where only the second text contains modifications. Expect as
+	 * merge result the second text.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testNoAgainstOneModification() throws IOException {
+		assertEquals(t("aZcZefghij"),
+				merge("abcdefghij", "abcdefghij", "aZcZefghij"));
+	}
+
+	/**
+	 * Both texts contain modifications but not on the same chunks. Expect a
+	 * non-conflict merge result.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testTwoNonConflictingModifications() throws IOException {
+		assertEquals(t("YbZdefghij"),
+				merge("abcdefghij", "abZdefghij", "Ybcdefghij"));
+	}
+
+	/**
+	 * Merge two complicated modifications. The merge algorithm has to extend
+	 * and combine conflicting regions to get to the expected merge result.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testTwoComplicatedModifications() throws IOException {
+		assertEquals(t("aZZZZfZhZjbYdYYYYiY"),
+				merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY"));
+	}
+
+	/**
+	 * Merge two modifications with a shared delete at the end. The underlying
+	 * diff algorithm has to provide consistent edit results to get the expected
+	 * merge result.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testTwoModificationsWithSharedDelete() throws IOException {
+		assertEquals(t("Cb}n}"), merge("ab}n}n}", "ab}n}", "Cb}n}"));
+	}
+
+	/**
+	 * Merge modifications with a shared insert in the middle. The underlying
+	 * diff algorithm has to provide consistent edit results to get the expected
+	 * merge result.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testModificationsWithMiddleInsert() throws IOException {
+		assertEquals(t("aBcd123123uvwxPq"),
+				merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq"));
+	}
+
+	/**
+	 * Merge modifications with a shared delete in the middle. The underlying
+	 * diff algorithm has to provide consistent edit results to get the expected
+	 * merge result.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testModificationsWithMiddleDelete() throws IOException {
+		assertEquals(t("Abz}z123Q"),
+				merge("abz}z}z123q", "Abz}z123Q", "abz}z123q"));
+	}
+
+	@Test
+	public void testInsertionAfterDeletion() throws IOException {
+		assertEquals(t("abcd"), merge("abd", "ad", "abcd"));
+	}
+
+	@Test
+	public void testInsertionBeforeDeletion() throws IOException {
+		assertEquals(t("acbd"), merge("abd", "ad", "acbd"));
+	}
+
+	/**
+	 * Test a conflicting region at the very start of the text.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testConflictAtStart() throws IOException {
+		assertEquals(t("ZYbcdefghij"),
+				merge("abcdefghij", "Zbcdefghij", "Ybcdefghij"));
+	}
+
+	/**
+	 * Test a conflicting region at the very end of the text.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testConflictAtEnd() throws IOException {
+		assertEquals(t("abcdefghiZY"),
+				merge("abcdefghij", "abcdefghiZ", "abcdefghiY"));
+	}
+
+	/**
+	 * Check for a conflict where the second text was changed similar to the
+	 * first one, but the second texts modification covers one more line.
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testSameModification() throws IOException {
+		assertEquals(t("abZdefghij"),
+				merge("abcdefghij", "abZdefghij", "abZdefghij"));
+	}
+
+	/**
+	 * Check that a deleted vs. a modified line shows up as conflict (see Bug
+	 * 328551)
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testDeleteVsModify() throws IOException {
+		assertEquals(t("abZdefghij"),
+				merge("abcdefghij", "abdefghij", "abZdefghij"));
+	}
+
+	@Test
+	public void testInsertVsModify() throws IOException {
+		assertEquals(t("abZXY"), merge("ab", "abZ", "aXY"));
+	}
+
+	@Test
+	public void testAdjacentModifications() throws IOException {
+		assertEquals(t("aZcbYd"), merge("abcd", "aZcd", "abYd"));
+	}
+
+	@Test
+	public void testSeparateModifications() throws IOException {
+		assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe"));
+	}
+
+	@Test
+	public void testBlankLines() throws IOException {
+		assertEquals(t("aZc\nYe"), merge("abc\nde", "aZc\nde", "abc\nYe"));
+	}
+
+	/**
+	 * Test merging two contents which do one similar modification and one
+	 * insertion is only done by one side, in the middle. Between modification
+	 * and insertion is a block which is common between the two contents and the
+	 * common base
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testTwoSimilarModsAndOneInsert() throws IOException {
+		assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde"));
+
+		assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB"));
+
+		assertEquals(t("HIAAAJCAB"), merge("HiACAB", "HIACAB", "HIAAAJCAB"));
+
+		assertEquals(t("AGADEFHIAAAJCAB"),
+				merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB"));
+	}
+
+	/**
+	 * Test merging two contents which do one similar modification and one
+	 * insertion is only done by one side, at the end. Between modification and
+	 * insertion is a block which is common between the two contents and the
+	 * common base
+	 *
+	 * @throws java.io.IOException
+	 */
+	@Test
+	public void testTwoSimilarModsAndOneInsertAtEnd() throws IOException {
+		Assume.assumeTrue(newlineAtEnd);
+		assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ"));
+
+		assertEquals(t("IAJ"), merge("iA", "IA", "IAJ"));
+
+		assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ"));
+	}
+
+	@Test
+	public void testTwoSimilarModsAndOneInsertAtEndNoNewlineAtEnd()
+			throws IOException {
+		Assume.assumeFalse(newlineAtEnd);
+		assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAJ"));
+
+		assertEquals(t("IAAJ"), merge("iA", "IA", "IAJ"));
+
+		assertEquals(t("IAAAAJ"), merge("iA", "IA", "IAAAJ"));
+	}
+
+	// Test situations where (at least) one input value is the empty text
+
+	@Test
+	public void testEmptyTextModifiedAgainstDeletion() throws IOException {
+		// NOTE: git.git merge-file appends a '\n' to the end of the file even
+		// when the input files do not have a newline at the end. That appears
+		// to be a bug in git.git.
+		assertEquals(t("AB"), merge("A", "AB", ""));
+		assertEquals(t("AB"), merge("A", "", "AB"));
+	}
+
+	@Test
+	public void testEmptyTextUnmodifiedAgainstDeletion() throws IOException {
+		assertEquals(t(""), merge("AB", "AB", ""));
+
+		assertEquals(t(""), merge("AB", "", "AB"));
+	}
+
+	@Test
+	public void testEmptyTextDeletionAgainstDeletion() throws IOException {
+		assertEquals(t(""), merge("AB", "", ""));
+	}
+
+	private String merge(String commonBase, String ours, String theirs)
+			throws IOException {
+		MergeAlgorithm ma = new MergeAlgorithm();
+		ma.setContentMergeStrategy(ContentMergeStrategy.UNION);
+		MergeResult<RawText> r = ma.merge(RawTextComparator.DEFAULT,
+				T(commonBase), T(ours), T(theirs));
+		ByteArrayOutputStream bo = new ByteArrayOutputStream(50);
+		fmt.formatMerge(bo, r, "B", "O", "T", UTF_8);
+		return bo.toString(UTF_8);
+	}
+
+	public String t(String text) {
+		StringBuilder r = new StringBuilder();
+		for (int i = 0; i < text.length(); i++) {
+			char c = text.charAt(i);
+			switch (c) {
+			case '<':
+				r.append("<<<<<<< O\n");
+				break;
+			case '=':
+				r.append("=======\n");
+				break;
+			case '|':
+				r.append("||||||| B\n");
+				break;
+			case '>':
+				r.append(">>>>>>> T\n");
+				break;
+			default:
+				r.append(c);
+				if (newlineAtEnd || i < text.length() - 1)
+					r.append('\n');
+			}
+		}
+		return r.toString();
+	}
+
+	public RawText T(String text) {
+		return new RawText(Constants.encode(t(text)));
+	}
+}
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 3a036ac..c6a6321 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
@@ -1792,7 +1792,77 @@ public void checkModeMergeConflictInVirtualAncestor(MergeStrategy strategy) thro
 		// children
 		mergeResult = git.merge().include(commitC3S).call();
 		assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
+	}
 
+	/**
+	 * Merging two commits when binary files have equal content, but conflicting content in the
+	 * virtual ancestor.
+	 *
+	 * <p>
+	 * This test has the same set up as
+	 * {@code checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren}, only
+	 * with the content conflict in A1 and A2.
+	 */
+	@Theory
+	public void checkBinaryMergeConflictInVirtualAncestor(MergeStrategy strategy) throws Exception {
+		if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+			return;
+		}
+
+		Git git = Git.wrap(db);
+
+		// master
+		writeTrashFile("c", "initial file");
+		git.add().addFilepattern("c").call();
+		RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+		writeTrashFile("a", "\0\1\1\1\1\0");  // content in Ancestor 1
+		git.add().addFilepattern("a").call();
+		RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+		writeTrashFile("a", "\0\1\2\3\4\5\0");  // 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();
+		writeTrashFile("a", "\0\2\2\2\2\0");  // content in Ancestor 1
+		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", "\0\5\4\3\2\1\0");  // 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
+		writeTrashFile("a", "\0\3\3\3\3\0");  // 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 - set the same value as in resolution above
+		writeTrashFile("a", "\0\3\3\3\3\0");  // 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);
 	}
 
 	/**
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java
index 798aebe..0016adf 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java
@@ -16,6 +16,8 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -375,7 +377,8 @@ private static ObjectId commit(ObjectInserter odi, DirCache treeB,
 			ObjectId[] parentIds) throws Exception {
 		CommitBuilder c = new CommitBuilder();
 		c.setTreeId(treeB.writeTree(odi));
-		c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0));
+		c.setAuthor(new PersonIdent("A U Thor", "a.u.thor",
+				Instant.ofEpochMilli(1L), ZoneOffset.UTC));
 		c.setCommitter(c.getAuthor());
 		c.setParentIds(parentIds);
 		c.setMessage("Tree " + c.getTreeId().name());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
index 2aac15b..5507f85 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
@@ -48,8 +48,7 @@
 import org.junit.runners.Suite;
 
 @RunWith(Suite.class)
-@Suite.SuiteClasses({
- 		PatchApplierTest.WithWorktree. class, //
+@Suite.SuiteClasses({ PatchApplierTest.WithWorktree.class, //
 		PatchApplierTest.InCore.class, //
 })
 public class PatchApplierTest {
@@ -128,6 +127,20 @@ protected Result applyPatch() throws IOException {
 			}
 		}
 
+		protected Result applyPatchAllowConflicts() throws IOException {
+			InputStream patchStream = getTestResource(name + ".patch");
+			Patch patch = new Patch();
+			patch.parse(patchStream);
+			if (inCore) {
+				try (ObjectInserter oi = db.newObjectInserter()) {
+					return new PatchApplier(db, baseTip, oi).allowConflicts()
+							.applyPatch(patch);
+				}
+			}
+			return new PatchApplier(db).allowConflicts()
+					.applyPatch(patch);
+		}
+
 		protected static InputStream getTestResource(String patchFile) {
 			return PatchApplierTest.class.getClassLoader()
 					.getResourceAsStream("org/eclipse/jgit/diff/" + patchFile);
@@ -169,6 +182,13 @@ void verifyChange(Result result, String aName, boolean exists)
 			verifyContent(result, aName, exists);
 		}
 
+		void verifyChange(Result result, String aName, boolean exists,
+				int numConflicts) throws Exception {
+			assertEquals(numConflicts, result.getErrors().size());
+			assertEquals(1, result.getPaths().size());
+			verifyContent(result, aName, exists);
+		}
+
 		protected byte[] readBlob(ObjectId treeish, String path)
 				throws Exception {
 			try (TestRepository<?> tr = new TestRepository<>(db);
@@ -346,6 +366,44 @@ public void testCopyWithHunks() throws Exception {
 		}
 
 		@Test
+		public void testConflictMarkers() throws Exception {
+			init("allowconflict", true, true);
+
+			Result result = applyPatchAllowConflicts();
+
+			assertEquals(result.getErrors().size(), 1);
+			PatchApplier.Result.Error error = result.getErrors().get(0);
+			assertEquals("cannot apply hunk", error.msg);
+			assertEquals("allowconflict", error.oldFileName);
+			assertTrue(error.isGitConflict());
+			verifyChange(result, "allowconflict", true, 1);
+		}
+
+		@Test
+		public void testConflictMarkersOutOfBounds() throws Exception {
+			init("ConflictOutOfBounds", true, true);
+
+			Result result = applyPatchAllowConflicts();
+
+			assertEquals(result.getErrors().size(), 1);
+			PatchApplier.Result.Error error = result.getErrors().get(0);
+			assertEquals("cannot apply hunk", error.msg);
+			assertEquals("ConflictOutOfBounds", error.oldFileName);
+			assertTrue(error.isGitConflict());
+			verifyChange(result, "ConflictOutOfBounds", true, 1);
+		}
+
+		@Test
+		public void testConflictMarkersFileDeleted() throws Exception {
+			init("allowconflict_file_deleted", false, false);
+
+			Result result = applyPatchAllowConflicts();
+
+			assertEquals(1, result.getErrors().size());
+			assertEquals(0, result.getPaths().size());
+		}
+
+		@Test
 		public void testShiftUp() throws Exception {
 			init("ShiftUp");
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java
index 6872289..014ff92 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2009, Google Inc. and others
+ * Copyright (C) 2008, 2024 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
@@ -23,7 +23,9 @@
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.IllegalCharsetNameException;
 import java.nio.charset.UnsupportedCharsetException;
-import java.util.TimeZone;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -94,18 +96,17 @@ public void testParse_NoParents() throws Exception {
 		assertNotNull(cAuthor);
 		assertEquals(authorName, cAuthor.getName());
 		assertEquals(authorEmail, cAuthor.getEmailAddress());
-		assertEquals((long) authorTime * 1000, cAuthor.getWhen().getTime());
-		assertEquals(TimeZone.getTimeZone("GMT" + authorTimeZone),
-				cAuthor.getTimeZone());
+		assertEquals(Instant.ofEpochSecond(authorTime),
+				cAuthor.getWhenAsInstant());
+		assertEquals(ZoneId.of(authorTimeZone), cAuthor.getZoneId());
 
 		final PersonIdent cCommitter = c.getCommitterIdent();
 		assertNotNull(cCommitter);
 		assertEquals(committerName, cCommitter.getName());
 		assertEquals(committerEmail, cCommitter.getEmailAddress());
-		assertEquals((long) committerTime * 1000,
-				cCommitter.getWhen().getTime());
-		assertEquals(TimeZone.getTimeZone("GMT" + committerTimeZone),
-				cCommitter.getTimeZone());
+		assertEquals(Instant.ofEpochSecond(committerTime),
+				cCommitter.getWhenAsInstant());
+		assertEquals(ZoneId.of(committerTimeZone), cCommitter.getZoneId());
 	}
 
 	private RevCommit create(String msg) throws Exception {
@@ -153,9 +154,13 @@ public void testParse_incompleteAuthorAndCommitter() throws Exception {
 			c.parseCanonical(rw, b.toString().getBytes(UTF_8));
 		}
 		assertEquals(
-				new PersonIdent("", "a_u_thor@example.com", 1218123387000L, 7),
+				new PersonIdent("", "a_u_thor@example.com",
+						Instant.ofEpochMilli(1218123387000L),
+						ZoneOffset.ofHoursMinutes(0, 7)),
 				c.getAuthorIdent());
-		assertEquals(new PersonIdent("", "", 1218123390000L, -5),
+		assertEquals(
+				new PersonIdent("", "", Instant.ofEpochMilli(1218123390000L),
+						ZoneOffset.ofHoursMinutes(0, -5)),
 				c.getCommitterIdent());
 	}
 
@@ -408,6 +413,7 @@ public void testParse_NoMessage() throws Exception {
 		final RevCommit c = create(msg);
 		assertEquals(msg, c.getFullMessage());
 		assertEquals(msg, c.getShortMessage());
+		assertEquals(msg, c.getFirstMessageLine());
 	}
 
 	@Test
@@ -415,6 +421,7 @@ public void testParse_OnlyLFMessage() throws Exception {
 		final RevCommit c = create("\n");
 		assertEquals("\n", c.getFullMessage());
 		assertEquals("", c.getShortMessage());
+		assertEquals("", c.getFirstMessageLine());
 	}
 
 	@Test
@@ -423,6 +430,7 @@ public void testParse_ShortLineOnlyNoLF() throws Exception {
 		final RevCommit c = create(shortMsg);
 		assertEquals(shortMsg, c.getFullMessage());
 		assertEquals(shortMsg, c.getShortMessage());
+		assertEquals(shortMsg, c.getFirstMessageLine());
 	}
 
 	@Test
@@ -432,6 +440,7 @@ public void testParse_ShortLineOnlyEndLF() throws Exception {
 		final RevCommit c = create(fullMsg);
 		assertEquals(fullMsg, c.getFullMessage());
 		assertEquals(shortMsg, c.getShortMessage());
+		assertEquals(shortMsg, c.getFirstMessageLine());
 	}
 
 	@Test
@@ -441,6 +450,7 @@ public void testParse_ShortLineOnlyEmbeddedLF() throws Exception {
 		final RevCommit c = create(fullMsg);
 		assertEquals(fullMsg, c.getFullMessage());
 		assertEquals(shortMsg, c.getShortMessage());
+		assertEquals("This is a", c.getFirstMessageLine());
 	}
 
 	@Test
@@ -450,6 +460,7 @@ public void testParse_ShortLineOnlyEmbeddedAndEndingLF() throws Exception {
 		final RevCommit c = create(fullMsg);
 		assertEquals(fullMsg, c.getFullMessage());
 		assertEquals(shortMsg, c.getShortMessage());
+		assertEquals("This is a", c.getFirstMessageLine());
 	}
 
 	@Test
@@ -461,6 +472,7 @@ public void testParse_GitStyleMessage() throws Exception {
 		final RevCommit c = create(fullMsg);
 		assertEquals(fullMsg, c.getFullMessage());
 		assertEquals(shortMsg, c.getShortMessage());
+		assertEquals(shortMsg, c.getFirstMessageLine());
 	}
 
 	@Test
@@ -480,6 +492,7 @@ public void testParse_PublicParseMethod()
 		assertEquals(author, p.getAuthorIdent());
 		assertEquals(committer, p.getCommitterIdent());
 		assertEquals("Test commit", p.getShortMessage());
+		assertEquals("Test commit", p.getFirstMessageLine());
 		assertEquals(src.getMessage(), p.getFullMessage());
 	}
 
@@ -494,6 +507,7 @@ public void testParse_GitStyleMessageWithCRLF() throws Exception {
 		final RevCommit c = create(fullMsg);
 		assertEquals(fullMsg, c.getFullMessage());
 		assertEquals(shortMsg, c.getShortMessage());
+		assertEquals("This fixes a", c.getFirstMessageLine());
 	}
 
 	private static ObjectId id(String str) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java
index 81ff4a2..7fece66 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java
@@ -14,6 +14,7 @@
 import static org.junit.Assert.assertNull;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Date;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -217,14 +218,132 @@ public void testCommitTimeRevFilter() throws Exception {
 		final RevCommit b = commit(a);
 		tick(100);
 
-		Date since = getDate();
+		Instant since = getInstant();
 		final RevCommit c1 = commit(b);
 		tick(100);
 
 		final RevCommit c2 = commit(b);
 		tick(100);
 
-		Date until = getDate();
+		Instant until = getInstant();
+		final RevCommit d = commit(c1, c2);
+		tick(100);
+
+		final RevCommit e = commit(d);
+
+		{
+			RevFilter after = CommitTimeRevFilter.after(since);
+			assertNotNull(after);
+			rw.setRevFilter(after);
+			markStart(e);
+			assertCommit(e, rw.next());
+			assertCommit(d, rw.next());
+			assertCommit(c2, rw.next());
+			assertCommit(c1, rw.next());
+			assertNull(rw.next());
+		}
+
+		{
+			RevFilter before = CommitTimeRevFilter.before(until);
+			assertNotNull(before);
+			rw.reset();
+			rw.setRevFilter(before);
+			markStart(e);
+			assertCommit(c2, rw.next());
+			assertCommit(c1, rw.next());
+			assertCommit(b, rw.next());
+			assertCommit(a, rw.next());
+			assertNull(rw.next());
+		}
+
+		{
+			RevFilter between = CommitTimeRevFilter.between(since, until);
+			assertNotNull(between);
+			rw.reset();
+			rw.setRevFilter(between);
+			markStart(e);
+			assertCommit(c2, rw.next());
+			assertCommit(c1, rw.next());
+			assertNull(rw.next());
+		}
+	}
+
+	@Test
+	public void testCommitTimeRevFilter_date() throws Exception {
+		// Using deprecated Date api for the commit time rev filter.
+		// Delete this tests when method is removed.
+		final RevCommit a = commit();
+		tick(100);
+
+		final RevCommit b = commit(a);
+		tick(100);
+
+		Date since = Date.from(getInstant());
+		final RevCommit c1 = commit(b);
+		tick(100);
+
+		final RevCommit c2 = commit(b);
+		tick(100);
+
+		Date until = Date.from(getInstant());
+		final RevCommit d = commit(c1, c2);
+		tick(100);
+
+		final RevCommit e = commit(d);
+
+		{
+			RevFilter after = CommitTimeRevFilter.after(since);
+			assertNotNull(after);
+			rw.setRevFilter(after);
+			markStart(e);
+			assertCommit(e, rw.next());
+			assertCommit(d, rw.next());
+			assertCommit(c2, rw.next());
+			assertCommit(c1, rw.next());
+			assertNull(rw.next());
+		}
+
+		{
+			RevFilter before = CommitTimeRevFilter.before(until);
+			assertNotNull(before);
+			rw.reset();
+			rw.setRevFilter(before);
+			markStart(e);
+			assertCommit(c2, rw.next());
+			assertCommit(c1, rw.next());
+			assertCommit(b, rw.next());
+			assertCommit(a, rw.next());
+			assertNull(rw.next());
+		}
+
+		{
+			RevFilter between = CommitTimeRevFilter.between(since, until);
+			assertNotNull(between);
+			rw.reset();
+			rw.setRevFilter(between);
+			markStart(e);
+			assertCommit(c2, rw.next());
+			assertCommit(c1, rw.next());
+			assertNull(rw.next());
+		}
+	}
+
+	@Test
+	public void testCommitTimeRevFilter_long() throws Exception {
+		final RevCommit a = commit();
+		tick(100);
+
+		final RevCommit b = commit(a);
+		tick(100);
+
+		long since = getInstant().toEpochMilli();
+		final RevCommit c1 = commit(b);
+		tick(100);
+
+		final RevCommit c2 = commit(b);
+		tick(100);
+
+		long until = getInstant().toEpochMilli();
 		final RevCommit d = commit(c1, c2);
 		tick(100);
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java
index ec0c0e7..8fa6a83 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java
@@ -12,6 +12,7 @@
 
 import static org.junit.Assert.assertSame;
 
+import java.time.Instant;
 import java.util.Date;
 
 import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -38,8 +39,14 @@ protected RevWalk createRevWalk() {
 		return new RevWalk(db);
 	}
 
+	// Use getInstant() instead
+	@Deprecated
 	protected Date getDate() {
-		return util.getDate();
+		return Date.from(util.getInstant());
+	}
+
+	protected Instant getInstant() {
+		return util.getInstant();
 	}
 
 	protected void tick(int secDelta) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java
index 0a045c9..ffc7c96 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java
@@ -14,6 +14,7 @@
 import static org.junit.Assert.assertEquals;
 
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 
 import org.eclipse.jgit.api.Git;
@@ -121,7 +122,7 @@ private void assertContains(RevCommit commit, Collection<Ref> refsThatShouldCont
 		Collection<Ref> sortedRefs = RefComparator.sort(allRefs);
 		List<Ref> actual = RevWalkUtils.findBranchesReachableFrom(commit,
 				rw, sortedRefs);
-		assertEquals(refsThatShouldContainCommit, actual);
+		assertEquals(new HashSet<>(refsThatShouldContainCommit), new HashSet<>(actual));
 	}
 
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java
index 300c869..4306975 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java
@@ -114,6 +114,13 @@ public void addSubmodule() throws Exception {
 				try (Repository subModRepo = generator.getRepository()) {
 					assertNotNull(subModRepo);
 					assertEquals(subCommit, commit);
+					String worktreeDir = subModRepo.getConfig().getString(
+							ConfigConstants.CONFIG_CORE_SECTION, null,
+							ConfigConstants.CONFIG_KEY_WORKTREE);
+					assertEquals("../../../sub", worktreeDir);
+					String gitdir = read(new File(subModRepo.getWorkTree(),
+							Constants.DOT_GIT));
+					assertEquals("gitdir: ../.git/modules/sub", gitdir);
 				}
 			}
 			Status status = Git.wrap(db).status().call();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java
index b10bd73..d541170 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java
@@ -17,21 +17,25 @@
 import java.io.IOException;
 import java.util.Collection;
 
+import org.eclipse.jgit.api.CheckoutCommand;
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.InitCommand;
+import org.eclipse.jgit.api.SubmoduleAddCommand;
 import org.eclipse.jgit.api.SubmoduleUpdateCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheEditor;
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
 import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.junit.Test;
 
@@ -40,6 +44,91 @@
  */
 public class SubmoduleUpdateTest extends RepositoryTestCase {
 
+	private Repository submoduleRepo;
+
+	private Git git;
+
+	private AnyObjectId subRepoCommit2;
+
+	private void createSubmoduleRepo() throws IOException, GitAPIException {
+		File directory = createTempDirectory("submodule_repo");
+		InitCommand init = Git.init();
+		init.setDirectory(directory);
+		init.call();
+		submoduleRepo = Git.open(directory).getRepository();
+		try (Git sub = Git.wrap(submoduleRepo)) {
+			// commit something
+			JGitTestUtil.writeTrashFile(submoduleRepo, "commit1.txt",
+					"commit 1");
+			sub.add().addFilepattern("commit1.txt").call();
+			sub.commit().setMessage("commit 1").call().getId();
+
+			JGitTestUtil.writeTrashFile(submoduleRepo, "commit2.txt",
+					"commit 2");
+			sub.add().addFilepattern("commit2.txt").call();
+			subRepoCommit2 = sub.commit().setMessage("commit 2").call().getId();
+		}
+	}
+
+	private void addSubmodule(String path) throws GitAPIException {
+		SubmoduleAddCommand command = new SubmoduleAddCommand(db);
+		command.setPath(path);
+		String uri = submoduleRepo.getDirectory().toURI().toString();
+		command.setURI(uri);
+		try (Repository repo = command.call()) {
+			assertNotNull(repo);
+		}
+		git.add().addFilepattern(path).addFilepattern(Constants.DOT_GIT_MODULES)
+				.call();
+		git.commit().setMessage("adding submodule").call();
+		recursiveDelete(new File(git.getRepository().getWorkTree(), path));
+		recursiveDelete(
+				new File(new File(git.getRepository().getCommonDirectory(),
+						Constants.MODULES), path));
+	}
+
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		createSubmoduleRepo();
+
+		git = Git.wrap(db);
+		// commit something
+		writeTrashFile("initial.txt", "initial");
+		git.add().addFilepattern("initial.txt").call();
+		git.commit().setMessage("initial commit").call();
+	}
+
+	public void updateModeClonedRestoredSubmoduleTemplate(String mode)
+			throws Exception {
+		String path = "sub";
+		addSubmodule(path);
+
+		StoredConfig cfg = git.getRepository().getConfig();
+		if (mode != null) {
+			cfg.load();
+			cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+					ConfigConstants.CONFIG_KEY_UPDATE, mode);
+			cfg.save();
+		}
+		SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(db);
+		update.call();
+		try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+			update.call();
+			assertEquals(subRepoCommit2.getName(),
+					subGit.getRepository().getBranch());
+		}
+
+		recursiveDelete(new File(db.getWorkTree(), path));
+
+		update.call();
+		try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+			update.call();
+			assertEquals(subRepoCommit2.getName(),
+					subGit.getRepository().getBranch());
+		}
+	}
+
 	@Test
 	public void repositoryWithNoSubmodules() throws GitAPIException {
 		SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db);
@@ -50,35 +139,9 @@ public void repositoryWithNoSubmodules() throws GitAPIException {
 
 	@Test
 	public void repositoryWithSubmodule() throws Exception {
-		writeTrashFile("file.txt", "content");
-		Git git = Git.wrap(db);
-		git.add().addFilepattern("file.txt").call();
-		final RevCommit commit = git.commit().setMessage("create file").call();
 
 		final String path = "sub";
-		DirCache cache = db.lockDirCache();
-		DirCacheEditor editor = cache.editor();
-		editor.add(new PathEdit(path) {
-
-			@Override
-			public void apply(DirCacheEntry ent) {
-				ent.setFileMode(FileMode.GITLINK);
-				ent.setObjectId(commit);
-			}
-		});
-		editor.commit();
-
-		StoredConfig config = db.getConfig();
-		config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
-				ConfigConstants.CONFIG_KEY_URL, db.getDirectory().toURI()
-						.toString());
-		config.save();
-
-		FileBasedConfig modulesConfig = new FileBasedConfig(new File(
-				db.getWorkTree(), Constants.DOT_GIT_MODULES), db.getFS());
-		modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
-				ConfigConstants.CONFIG_KEY_PATH, path);
-		modulesConfig.save();
+		addSubmodule(path);
 
 		SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db);
 		Collection<String> updated = command.call();
@@ -90,14 +153,22 @@ public void apply(DirCacheEntry ent) {
 			assertTrue(generator.next());
 			try (Repository subRepo = generator.getRepository()) {
 				assertNotNull(subRepo);
-				assertEquals(commit, subRepo.resolve(Constants.HEAD));
+				assertEquals(subRepoCommit2, subRepo.resolve(Constants.HEAD));
+				String worktreeDir = subRepo.getConfig().getString(
+						ConfigConstants.CONFIG_CORE_SECTION, null,
+						ConfigConstants.CONFIG_KEY_WORKTREE);
+				assertEquals("../../../sub", worktreeDir);
+				String gitdir = read(
+						new File(subRepo.getWorkTree(), Constants.DOT_GIT));
+				assertEquals("gitdir: ../.git/modules/sub", gitdir);
+
 			}
 		}
 	}
 
 	@Test
-	public void repositoryWithUnconfiguredSubmodule() throws IOException,
-			GitAPIException {
+	public void repositoryWithUnconfiguredSubmodule()
+			throws IOException, GitAPIException {
 		final ObjectId id = ObjectId
 				.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
 		final String path = "sub";
@@ -113,16 +184,14 @@ public void apply(DirCacheEntry ent) {
 		});
 		editor.commit();
 
-		FileBasedConfig modulesConfig = new FileBasedConfig(new File(
-				db.getWorkTree(), Constants.DOT_GIT_MODULES), db.getFS());
+		FileBasedConfig modulesConfig = new FileBasedConfig(
+				new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
+				db.getFS());
 		modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
 				ConfigConstants.CONFIG_KEY_PATH, path);
 		String url = "git://server/repo.git";
 		modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
 				ConfigConstants.CONFIG_KEY_URL, url);
-		String update = "rebase";
-		modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
-				ConfigConstants.CONFIG_KEY_UPDATE, update);
 		modulesConfig.save();
 
 		SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db);
@@ -132,8 +201,8 @@ public void apply(DirCacheEntry ent) {
 	}
 
 	@Test
-	public void repositoryWithInitializedSubmodule() throws IOException,
-			GitAPIException {
+	public void repositoryWithInitializedSubmodule()
+			throws IOException, GitAPIException {
 		final ObjectId id = ObjectId
 				.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
 		final String path = "sub";
@@ -160,4 +229,77 @@ public void apply(DirCacheEntry ent) {
 		assertNotNull(updated);
 		assertTrue(updated.isEmpty());
 	}
+
+	@Test
+	public void updateModeMergeClonedRestoredSubmodule() throws Exception {
+		updateModeClonedRestoredSubmoduleTemplate(
+				ConfigConstants.CONFIG_KEY_MERGE);
+	}
+
+	@Test
+	public void updateModeRebaseClonedRestoredSubmodule() throws Exception {
+		updateModeClonedRestoredSubmoduleTemplate(
+				ConfigConstants.CONFIG_KEY_REBASE);
+	}
+
+	@Test
+	public void updateModeCheckoutClonedRestoredSubmodule() throws Exception {
+		updateModeClonedRestoredSubmoduleTemplate(
+				ConfigConstants.CONFIG_KEY_CHECKOUT);
+	}
+
+	@Test
+	public void updateModeMissingClonedRestoredSubmodule() throws Exception {
+		updateModeClonedRestoredSubmoduleTemplate(null);
+	}
+
+	@Test
+	public void updateMode() throws Exception {
+		String path = "sub";
+		addSubmodule(path);
+
+		StoredConfig cfg = git.getRepository().getConfig();
+		cfg.load();
+		cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+				ConfigConstants.CONFIG_KEY_UPDATE,
+				ConfigConstants.CONFIG_KEY_REBASE);
+		cfg.save();
+
+		SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(db);
+		update.call();
+		try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+			CheckoutCommand checkout = subGit.checkout();
+			checkout.setName("master");
+			checkout.call();
+			update.call();
+			assertEquals("master", subGit.getRepository().getBranch());
+		}
+
+		cfg.load();
+		cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+				ConfigConstants.CONFIG_KEY_UPDATE,
+				ConfigConstants.CONFIG_KEY_CHECKOUT);
+		cfg.save();
+
+		update.call();
+		try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+			assertEquals(subRepoCommit2.getName(),
+					subGit.getRepository().getBranch());
+		}
+
+		cfg.load();
+		cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+				ConfigConstants.CONFIG_KEY_UPDATE,
+				ConfigConstants.CONFIG_KEY_MERGE);
+		cfg.save();
+
+		update.call();
+		try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+			CheckoutCommand checkout = subGit.checkout();
+			checkout.setName("master");
+			checkout.call();
+			update.call();
+			assertEquals("master", subGit.getRepository().getBranch());
+		}
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
index c47e591..0ba8926 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
@@ -25,10 +25,6 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
-import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
-import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -46,16 +42,8 @@ public class AtomicPushTest {
 	public void setUp() throws Exception {
 		server = newRepo("server");
 		client = newRepo("client");
-		testProtocol = new TestProtocol<>(
-				null,
-				new ReceivePackFactory<Object>() {
-					@Override
-					public ReceivePack create(Object req, Repository db)
-							throws ServiceNotEnabledException,
-							ServiceNotAuthorizedException {
-						return new ReceivePack(db);
-					}
-				});
+		testProtocol = new TestProtocol<>(null,
+				(req, db) -> new ReceivePack(db));
 		uri = testProtocol.register(ctx, server);
 
 		try (TestRepository<?> clientRepo = new TestRepository<>(client)) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java
index cee023d..6290b79 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java
@@ -138,6 +138,51 @@ public void incompleteCasesMatchPersonIdent() throws Exception {
 				"Me <me@example.com>");
 	}
 
+	@Test
+	public void timezoneRange_hours() {
+		int HOUR_TO_MS = 60 * 60 * 1000;
+
+		// java.util.TimeZone: Hours must be between 0 to 23
+		PushCertificateIdent hourLimit = PushCertificateIdent
+				.parse("A U. Thor <a_u_thor@example.com> 1218123387 +2300");
+		assertEquals(1380, hourLimit.getTimeZoneOffset());
+		assertEquals(23 * HOUR_TO_MS,
+				hourLimit.getTimeZone().getOffset(1218123387));
+
+		PushCertificateIdent hourDubious = PushCertificateIdent
+				.parse("A U. Thor <a_u_thor@example.com> 1218123387 +2400");
+		assertEquals(1440, hourDubious.getTimeZoneOffset());
+		assertEquals(0, hourDubious.getTimeZone().getOffset(1218123387));
+	}
+
+	@Test
+	public void timezoneRange_minutes() {
+		PushCertificateIdent hourLimit = PushCertificateIdent
+				.parse("A U. Thor <a_u_thor@example.com> 1218123387 +0059");
+		assertEquals(59, hourLimit.getTimeZoneOffset());
+		assertEquals(59 * 60 * 1000,
+				hourLimit.getTimeZone().getOffset(1218123387));
+
+		// This becomes one hour and one minute (!)
+		PushCertificateIdent hourDubious = PushCertificateIdent
+				.parse("A U. Thor <a_u_thor@example.com> 1218123387 +0061");
+		assertEquals(61, hourDubious.getTimeZoneOffset());
+		assertEquals(61 * 60 * 1000,
+				hourDubious.getTimeZone().getOffset(1218123387));
+
+		PushCertificateIdent weirdCase = PushCertificateIdent
+				.parse("A U. Thor <a_u_thor@example.com> 1218123387 +0099");
+		assertEquals(99, weirdCase.getTimeZoneOffset());
+		assertEquals(99 * 60 * 1000,
+				weirdCase.getTimeZone().getOffset(1218123387));
+
+		PushCertificateIdent weirdCase2 = PushCertificateIdent
+				.parse("A U. Thor <a_u_thor@example.com> 1218123387 +0199");
+		assertEquals(60 + 99, weirdCase2.getTimeZoneOffset());
+		assertEquals((60 + 99) * 60 * 1000,
+				weirdCase2.getTimeZone().getOffset(1218123387));
+	}
+
 	private static void assertMatchesPersonIdent(String raw,
 			PersonIdent expectedPersonIdent, String expectedUserId) {
 		PushCertificateIdent certIdent = PushCertificateIdent.parse(raw);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java
index 4f01e4d..a03222b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java
@@ -23,6 +23,8 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -318,8 +320,8 @@ public void putMatchingWithSomeMatchingRefs() throws Exception {
 	}
 
 	private PersonIdent newIdent() {
-		return new PersonIdent(
-				"A U. Thor", "author@example.com", ts.getAndIncrement(), 0);
+		return new PersonIdent("A U. Thor", "author@example.com",
+				Instant.ofEpochMilli(ts.getAndIncrement()), ZoneOffset.UTC);
 	}
 
 	private PushCertificateStore newStore() {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java
index 029b45e..96d3a58 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java
@@ -14,10 +14,10 @@
 import java.io.File;
 import java.io.IOException;
 import java.net.HttpCookie;
+import java.time.Duration;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Date;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -101,7 +101,7 @@ public void testProcessResponseCookies() throws IOException {
 						.singletonList("cookie2=some value; Max-Age=1234; Path=/"));
 
 		try (TransportHttp transportHttp = new TransportHttp(db, uri)) {
-			Date creationDate = new Date();
+			Instant creationDate = Instant.now();
 			transportHttp.processResponseCookies(connection);
 
 			// evaluate written cookie file
@@ -112,8 +112,9 @@ public void testProcessResponseCookies() throws IOException {
 			cookie.setPath("/u/2/");
 
 			cookie.setMaxAge(
-					(Instant.parse("2100-01-01T11:00:00.000Z").toEpochMilli()
-							- creationDate.getTime()) / 1000);
+					Duration.between(creationDate,
+							Instant.parse("2100-01-01T11:00:00.000Z"))
+							.getSeconds());
 			cookie.setSecure(true);
 			cookie.setHttpOnly(true);
 			expectedCookies.add(cookie);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
index d403624..6792002 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
@@ -82,6 +82,43 @@ public void testWindowsFile2() throws Exception {
 	}
 
 	@Test
+	public void testBrokenFilePath() throws Exception {
+		String str = "D:\\\\my\\\\x";
+		URIish u = new URIish(str);
+		assertNull(u.getScheme());
+		assertFalse(u.isRemote());
+		assertEquals(str, u.getPath());
+		assertEquals(u, new URIish(str));
+	}
+
+	@Test
+	public void testStackOverflow() throws Exception {
+		StringBuilder b = new StringBuilder("D:\\");
+		for (int i = 0; i < 4000; i++) {
+			b.append("x\\");
+		}
+		String str = b.toString();
+		URIish u = new URIish(str);
+		assertNull(u.getScheme());
+		assertFalse(u.isRemote());
+		assertEquals(str, u.getPath());
+	}
+
+	@Test
+	public void testStackOverflow2() throws Exception {
+		StringBuilder b = new StringBuilder("D:\\");
+		for (int i = 0; i < 4000; i++) {
+			b.append("x\\");
+		}
+		b.append('y');
+		String str = b.toString();
+		URIish u = new URIish(str);
+		assertNull(u.getScheme());
+		assertFalse(u.isRemote());
+		assertEquals(str, u.getPath());
+	}
+
+	@Test
 	public void testRelativePath() throws Exception {
 		final String str = "../../foo/bar";
 		URIish u = new URIish(str);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java
index 2711762..a5507c8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java
@@ -27,9 +27,6 @@
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.UploadPack.RequestPolicy;
-import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
-import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
-import org.eclipse.jgit.transport.resolver.UploadPackFactory;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -264,15 +261,10 @@ private void generateBitmaps(InMemoryRepository repo) throws Exception {
 	}
 
 	private static TestProtocol<Object> generateReachableCommitUploadPackProtocol() {
-		return new TestProtocol<>(new UploadPackFactory<Object>() {
-			@Override
-			public UploadPack create(Object req, Repository db)
-					throws ServiceNotEnabledException,
-					ServiceNotAuthorizedException {
-				UploadPack up = new UploadPack(db);
-				up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT);
-				return up;
-			}
+		return new TestProtocol<>((req, db) -> {
+			UploadPack up = new UploadPack(db);
+			up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT);
+			return up;
 		}, null);
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
index def73ac..5c2f0e5 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
@@ -1,5 +1,6 @@
 package org.eclipse.jgit.transport;
 
+import static java.time.ZoneOffset.UTC;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
@@ -11,12 +12,14 @@
 import static org.junit.Assert.assertNotNull;
 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.IOException;
 import java.io.InputStream;
 import java.io.StringWriter;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -501,15 +504,15 @@ public void testV2Capabilities() throws Exception {
 		assertThat(hook.capabilitiesRequest, notNullValue());
 		assertThat(pckIn.readString(), is("version 2"));
 		assertThat(
-				Arrays.asList(pckIn.readString(), pckIn.readString(),
-						pckIn.readString()),
+				Arrays.asList(pckIn.readString(),pckIn.readString(),
+						pckIn.readString(), pckIn.readString()),
 				// TODO(jonathantanmy) This check is written this way
 				// to make it simple to see that we expect this list of
 				// capabilities, but probably should be loosened to
 				// allow additional commands to be added to the list,
 				// and additional capabilities to be added to existing
 				// commands without requiring test changes.
-				hasItems("ls-refs", "fetch=shallow", "server-option"));
+				hasItems("agent=" + UserAgent.get() ,"ls-refs", "fetch=shallow", "server-option"));
 		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
 	}
 
@@ -535,7 +538,7 @@ private void checkAdvertisedIfAllowed(String configSection, String configName,
 				lines.add(line);
 			}
 		}
-		assertThat(lines, containsInAnyOrder("ls-refs", "fetch", "server-option"));
+		assertThat(lines, containsInAnyOrder("ls-refs", "fetch", "server-option", "agent=" + UserAgent.get()));
 	}
 
 	private void checkUnadvertisedIfUnallowed(String configSection,
@@ -564,6 +567,47 @@ private void checkUnadvertisedIfUnallowed(String configSection,
 	}
 
 	@Test
+	public void testV0CapabilitiesAllowAnySha1InWant() throws Exception {
+		checkAvertisedCapabilityProtocolV0IfAllowed("uploadpack",
+				"allowanysha1inwant", "allow-reachable-sha1-in-want",
+				"allow-tip-sha1-in-want");
+	}
+
+	@Test
+	public void testV0CapabilitiesAllowReachableSha1InWant() throws Exception {
+		checkAvertisedCapabilityProtocolV0IfAllowed("uploadpack",
+				"allowreachablesha1inwant", "allow-reachable-sha1-in-want");
+	}
+
+	@Test
+	public void testV0CapabilitiesAllowTipSha1InWant() throws Exception {
+		checkAvertisedCapabilityProtocolV0IfAllowed("uploadpack",
+				"allowtipsha1inwant", "allow-tip-sha1-in-want");
+	}
+
+	private void checkAvertisedCapabilityProtocolV0IfAllowed(
+			String configSection, String configName, String... capabilities)
+			throws Exception {
+		server.getConfig().setBoolean(configSection, null, configName, true);
+		ByteArrayInputStream recvStream = uploadPackSetup(
+				TransferConfig.ProtocolVersion.V0.version(), null,
+				PacketLineIn.end());
+		PacketLineIn pckIn = new PacketLineIn(recvStream);
+
+		String line;
+		while (!PacketLineIn.isEnd((line = pckIn.readString()))) {
+			if (line.contains("capabilities")) {
+				List<String> linesCapabilities = Arrays.asList(line.substring(
+						line.indexOf(" ", line.indexOf("capabilities")) + 1)
+						.split(" "));
+				assertThat(linesCapabilities, hasItems(capabilities));
+				return;
+			}
+		}
+		fail("Server side protocol did not contain any capabilities'");
+	}
+
+	@Test
 	public void testV2CapabilitiesAllowFilter() throws Exception {
 		checkAdvertisedIfAllowed("uploadpack", "allowfilter", "filter");
 		checkUnadvertisedIfUnallowed("uploadpack", "allowfilter", "filter");
@@ -601,9 +645,9 @@ public void testV2CapabilitiesRefInWantNotAdvertisedIfAdvertisingForbidden() thr
 
 		assertThat(pckIn.readString(), is("version 2"));
 		assertThat(
-				Arrays.asList(pckIn.readString(), pckIn.readString(),
+				Arrays.asList(pckIn.readString(),pckIn.readString(), pckIn.readString(),
 						pckIn.readString()),
-				hasItems("ls-refs", "fetch=shallow", "server-option"));
+				hasItems("agent="+ UserAgent.get(),"ls-refs", "fetch=shallow", "server-option"));
 		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
 	}
 
@@ -1464,14 +1508,19 @@ public void testV2FetchDeepenWithoutDone() throws Exception {
 	public void testV2FetchShallowSince() throws Exception {
 		PersonIdent person = new PersonIdent(remote.getRepository());
 
-		RevCommit beyondBoundary = remote.commit()
-			.committer(new PersonIdent(person, 1510000000, 0)).create();
-		RevCommit boundary = remote.commit().parent(beyondBoundary)
-			.committer(new PersonIdent(person, 1520000000, 0)).create();
-		RevCommit tooOld = remote.commit()
-			.committer(new PersonIdent(person, 1500000000, 0)).create();
+		RevCommit beyondBoundary = remote.commit().committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1510000), UTC))
+				.create();
+		RevCommit boundary = remote.commit().parent(beyondBoundary).committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1520000), UTC))
+				.create();
+		RevCommit tooOld = remote.commit().committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC))
+				.create();
 		RevCommit merge = remote.commit().parent(boundary).parent(tooOld)
-			.committer(new PersonIdent(person, 1530000000, 0)).create();
+				.committer(new PersonIdent(person,
+						Instant.ofEpochSecond(1530000), UTC))
+				.create();
 
 		remote.update("branch1", merge);
 
@@ -1517,12 +1566,15 @@ public void testV2FetchShallowSince() throws Exception {
 	public void testV2FetchShallowSince_excludedParentWithMultipleChildren() throws Exception {
 		PersonIdent person = new PersonIdent(remote.getRepository());
 
-		RevCommit base = remote.commit()
-			.committer(new PersonIdent(person, 1500000000, 0)).create();
-		RevCommit child1 = remote.commit().parent(base)
-			.committer(new PersonIdent(person, 1510000000, 0)).create();
-		RevCommit child2 = remote.commit().parent(base)
-			.committer(new PersonIdent(person, 1520000000, 0)).create();
+		RevCommit base = remote.commit().committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC))
+				.create();
+		RevCommit child1 = remote.commit().parent(base).committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1510000), UTC))
+				.create();
+		RevCommit child2 = remote.commit().parent(base).committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1520000), UTC))
+				.create();
 
 		remote.update("branch1", child1);
 		remote.update("branch2", child2);
@@ -1559,8 +1611,9 @@ public void testV2FetchShallowSince_excludedParentWithMultipleChildren() throws
 	public void testV2FetchShallowSince_noCommitsSelected() throws Exception {
 		PersonIdent person = new PersonIdent(remote.getRepository());
 
-		RevCommit tooOld = remote.commit()
-				.committer(new PersonIdent(person, 1500000000, 0)).create();
+		RevCommit tooOld = remote.commit().committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC))
+				.create();
 
 		remote.update("branch1", tooOld);
 
@@ -1684,12 +1737,15 @@ public void testV2FetchDeepenNot_supportAnnotatedTags() throws Exception {
 	public void testV2FetchDeepenNot_excludedParentWithMultipleChildren() throws Exception {
 		PersonIdent person = new PersonIdent(remote.getRepository());
 
-		RevCommit base = remote.commit()
-			.committer(new PersonIdent(person, 1500000000, 0)).create();
-		RevCommit child1 = remote.commit().parent(base)
-			.committer(new PersonIdent(person, 1510000000, 0)).create();
-		RevCommit child2 = remote.commit().parent(base)
-			.committer(new PersonIdent(person, 1520000000, 0)).create();
+		RevCommit base = remote.commit().committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC))
+				.create();
+		RevCommit child1 = remote.commit().parent(base).committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1510000), UTC))
+				.create();
+		RevCommit child2 = remote.commit().parent(base).committer(
+				new PersonIdent(person, Instant.ofEpochSecond(1520000), UTC))
+				.create();
 
 		remote.update("base", base);
 		remote.update("branch1", child1);
@@ -2820,7 +2876,7 @@ public void testSingleBranchCloneTagChain() throws Exception {
 		RevTag heavyTag2 = remote.tag("middleTagRing", heavyTag1);
 		remote.lightweightTag("refTagRing", heavyTag2);
 
-		UploadPack uploadPack = new UploadPack(remote.getRepository());
+		try (UploadPack uploadPack = new UploadPack(remote.getRepository())) {
 
 		ByteArrayOutputStream cli = new ByteArrayOutputStream();
 		PacketLineOut clientWant = new PacketLineOut(cli);
@@ -2830,7 +2886,6 @@ public void testSingleBranchCloneTagChain() throws Exception {
 		clientWant.writeString("done\n");
 
 		try (ByteArrayOutputStream serverResponse = new ByteArrayOutputStream()) {
-
 			uploadPack.setPreUploadHook(new PreUploadHook() {
 				@Override
 				public void onBeginNegotiateRound(UploadPack up,
@@ -2883,6 +2938,7 @@ public void onSendPack(UploadPack up,
 			assertTrue(objDb.has(heavyTag2.toObjectId()));
 		}
 	}
+}
 
 	@Test
 	public void testSingleBranchShallowCloneTagChainWithReflessTag() throws Exception {
@@ -2894,7 +2950,7 @@ public void testSingleBranchShallowCloneTagChainWithReflessTag() throws Exceptio
 		RevTag tag3 = remote.tag("t3", tag2);
 		remote.lightweightTag("t3", tag3);
 
-		UploadPack uploadPack = new UploadPack(remote.getRepository());
+		try (UploadPack uploadPack = new UploadPack(remote.getRepository())) {
 
 		ByteArrayOutputStream cli = new ByteArrayOutputStream();
 		PacketLineOut clientWant = new PacketLineOut(cli);
@@ -2904,7 +2960,6 @@ public void testSingleBranchShallowCloneTagChainWithReflessTag() throws Exceptio
 		clientWant.writeString("done\n");
 
 		try (ByteArrayOutputStream serverResponse = new ByteArrayOutputStream()) {
-
 			uploadPack.setPreUploadHook(new PreUploadHook() {
 				@Override
 				public void onBeginNegotiateRound(UploadPack up,
@@ -2952,6 +3007,7 @@ public void onSendPack(UploadPack up,
 			assertTrue(objDb.has(one.toObjectId()));
 		}
 	}
+}
 
 	@Test
 	public void testSafeToClearRefsInFetchV0() throws Exception {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
index e463e90..7b9e70d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
@@ -25,8 +25,9 @@
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.dircache.Checkout;
 import org.eclipse.jgit.dircache.DirCache;
-import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
 import org.eclipse.jgit.dircache.DirCacheEditor;
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
 import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -38,6 +39,7 @@
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -303,11 +305,12 @@ public void testIsModifiedSymlinkAsFile() throws Exception {
 		DirCacheEntry dce = db.readDirCache().getEntry("symlink");
 		dce.setFileMode(FileMode.SYMLINK);
 		try (ObjectReader objectReader = db.newObjectReader()) {
+			Checkout checkout = new Checkout(db).setRecursiveDeletion(false);
+			checkout.checkout(dce,
+					new CheckoutMetadata(EolStreamType.DIRECT, null),
+					objectReader, null);
 			WorkingTreeOptions options = db.getConfig()
 					.get(WorkingTreeOptions.KEY);
-			DirCacheCheckout.checkoutEntry(db, dce, objectReader, false, null,
-					options);
-
 			FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(),
 					options);
 			while (!fti.getEntryPathString().equals("symlink")) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java
index 3265249..44e8632 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java
@@ -12,7 +12,9 @@
 
 import static org.junit.Assert.assertEquals;
 
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
 
 import org.eclipse.jgit.junit.MockSystemReader;
 import org.eclipse.jgit.lib.ObjectId;
@@ -53,9 +55,9 @@ public class ChangeIdUtilTest {
 
 	MockSystemReader mockSystemReader = new MockSystemReader();
 
-	final long when = mockSystemReader.getCurrentTime();
+	Instant when = mockSystemReader.now();
 
-	final int tz = new MockSystemReader().getTimezone(when);
+	ZoneId tz = new MockSystemReader().getTimeZoneAt(when);
 
 	PersonIdent author = new PersonIdent("J. Author", "ja@example.com");
 	{
@@ -218,23 +220,23 @@ public void testACommitWithSubject_NonFooterAndBugAndSob() throws Exception {
 	@Test
 	public void testACommitWithSubjectBodyBugBrackersAndSob() throws Exception {
 		assertEquals(
-				"a commit with subject body, bug. brackers and sob\n\nText\n\nBug: 33\nChange-Id: I90ecb589bef766302532c3e00915e10114b00f62\n[bracket]\nSigned-off-by: me@you.too\n",
-				call("a commit with subject body, bug. brackers and sob\n\nText\n\nBug: 33\n[bracket]\nSigned-off-by: me@you.too\n\n"));
+				"a commit with subject body, bug, brackers and sob\n\nText\n\nBug: 33\n[bracket]\nChange-Id: I94dc6ed919a4baaa7c1bf8712717b888c6b90363\nSigned-off-by: me@you.too\n",
+				call("a commit with subject body, bug, brackers and sob\n\nText\n\nBug: 33\n[bracket]\nSigned-off-by: me@you.too\n\n"));
 	}
 
 	@Test
 	public void testACommitWithSubjectBodyBugLineWithASpaceAndSob()
 			throws Exception {
 		assertEquals(
-				"a commit with subject body, bug. line with a space and sob\n\nText\n\nBug: 33\nChange-Id: I864e2218bdee033c8ce9a7f923af9e0d5dc16863\n \nSigned-off-by: me@you.too\n",
-				call("a commit with subject body, bug. line with a space and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n"));
+				"a commit with subject body, bug, line with a space and sob\n\nText\n\nBug: 33\n \nChange-Id: I126b472d2e0e64ad8187d61857f0169f9ccdae86\nSigned-off-by: me@you.too\n",
+				call("a commit with subject body, bug, line with a space and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n"));
 	}
 
 	@Test
 	public void testACommitWithSubjectBodyBugEmptyLineAndSob() throws Exception {
 		assertEquals(
-				"a commit with subject body, bug. empty line and sob\n\nText\n\nBug: 33\nChange-Id: I33f119f533313883e6ada3df600c4f0d4db23a76\n \nSigned-off-by: me@you.too\n",
-				call("a commit with subject body, bug. empty line and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n"));
+				"a commit with subject body, bug, empty line and sob\n\nText\n\nBug: 33\n\nChange-Id: Ic3b61b6e39a0815669b65302e9e75e6a5a019a26\nSigned-off-by: me@you.too\n",
+				call("a commit with subject body, bug, empty line and sob\n\nText\n\nBug: 33\n\nSigned-off-by: me@you.too\n\n"));
 	}
 
 	@Test
@@ -342,9 +344,7 @@ public void testTimeAltersId() throws Exception {
 
 	/** Increment the {@link #author} and {@link #committer} times. */
 	protected void tick() {
-		final long delta = TimeUnit.MILLISECONDS.convert(5 * 60,
-				TimeUnit.SECONDS);
-		final long now = author.getWhen().getTime() + delta;
+		Instant now = author.getWhenAsInstant().plus(Duration.ofMinutes(5));
 
 		author = new PersonIdent(author, now, tz);
 		committer = new PersonIdent(committer, now, tz);
@@ -528,7 +528,7 @@ public void testKernelStyleFooter() throws Exception {
 	}
 
 	@Test
-	public void testChangeIdAfterBugOrIssue() throws Exception {
+	public void testChangeIdAfterOtherFooters() throws Exception {
 		assertEquals("a\n" + //
 				"\n" + //
 				"Bug: 42\n" + //
@@ -541,6 +541,18 @@ public void testChangeIdAfterBugOrIssue() throws Exception {
 
 		assertEquals("a\n" + //
 				"\n" + //
+				"Bug: 42\n" + //
+				"     multi-line Bug footer\n" + //
+				"Change-Id: Icc953ef35f1a4ee5eb945132aefd603ae3d9dd9f\n" + //
+				SOB1,//
+				call("a\n" + //
+						"\n" + //
+						"Bug: 42\n" + //
+						"     multi-line Bug footer\n" + //
+						SOB1));
+
+		assertEquals("a\n" + //
+				"\n" + //
 				"Issue: 42\n" + //
 				"Change-Id: Ie66e07d89ae5b114c0975b49cf326e90331dd822\n" + //
 				SOB1,//
@@ -548,6 +560,14 @@ public void testChangeIdAfterBugOrIssue() throws Exception {
 						"\n" + //
 						"Issue: 42\n" + //
 						SOB1));
+
+		assertEquals("a\n" + //
+				"\n" + //
+				"Other: none\n" + //
+				"Change-Id: Ide70e625dea61854206378a377dd12e462ae720f\n",//
+				call("a\n" + //
+						"\n" + //
+						"Other: none\n"));
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java
index 6a531fe..7ef386f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java
@@ -89,7 +89,6 @@ public void RAW() {
 	@Test
 	public void LOCALE() {
 		String date = new GitDateFormatter(Format.LOCALE).formatDate(ident);
-		System.out.println(date);
 		assertTrue("Sep 20, 2011 7:09:25 PM -0400".equals(date)
 				|| "Sep 20, 2011, 7:09:25 PM -0400".equals(date) // JDK-8206961
 				|| "Sep 20, 2011, 7:09:25\u202FPM -0400".equals(date)); // JDK-8304925
@@ -99,7 +98,6 @@ public void LOCALE() {
 	public void LOCALELOCAL() {
 		String date = new GitDateFormatter(Format.LOCALELOCAL)
 				.formatDate(ident);
-		System.out.println(date);
 		assertTrue("Sep 20, 2011 7:39:25 PM".equals(date)
 				|| "Sep 20, 2011, 7:39:25 PM".equals(date) // JDK-8206961
 				|| "Sep 20, 2011, 7:39:25\u202FPM".equals(date)); // JDK-8304925
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java
new file mode 100644
index 0000000..a59d7bc
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012, Christian Halstrick 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.assertThrows;
+
+import java.text.ParseException;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests which assert that unparseable Strings lead to ParseExceptions
+ */
+@RunWith(Theories.class)
+public class GitTimeParserBadlyFormattedTest {
+	private String dateStr;
+
+	@Before
+	public void setUp() {
+		MockSystemReader mockSystemReader = new MockSystemReader();
+		SystemReader.setInstance(mockSystemReader);
+	}
+
+	@After
+	public void tearDown() {
+		SystemReader.setInstance(null);
+	}
+
+	public GitTimeParserBadlyFormattedTest(String dateStr) {
+		this.dateStr = dateStr;
+	}
+
+	@DataPoints
+	public static String[] getDataPoints() {
+		return new String[] { "", ".", "...", "1970", "3000.3000.3000", "3 yesterday ago",
+				"now yesterday ago", "yesterdays", "3.day. 2.week.ago",
+				"day ago", "Gra Feb 21 15:35:00 2007 +0100",
+				"Sun Feb 21 15:35:00 2007 +0100",
+				"Wed Feb 21 15:35:00 Grand +0100" };
+	}
+
+	@Theory
+	public void badlyFormattedWithoutRef() {
+		assertThrows(
+				"The expected ParseException while parsing '" + dateStr
+						+ "' did not occur.",
+				ParseException.class, () -> GitTimeParser.parse(dateStr));
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java
new file mode 100644
index 0000000..0e5eb28
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2024, Christian Halstrick 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.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Period;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GitTimeParserTest {
+	MockSystemReader mockSystemReader;
+
+	@Before
+	public void setUp() {
+		mockSystemReader = new MockSystemReader();
+		SystemReader.setInstance(mockSystemReader);
+	}
+
+	@After
+	public void tearDown() {
+		SystemReader.setInstance(null);
+	}
+
+	@Test
+	public void yesterday() throws ParseException {
+		LocalDateTime parse = GitTimeParser.parse("yesterday");
+
+		LocalDateTime now = SystemReader.getInstance().civilNow();
+		assertEquals(Period.between(parse.toLocalDate(), now.toLocalDate()),
+				Period.ofDays(1));
+	}
+
+	@Test
+	public void never() throws ParseException {
+		LocalDateTime parse = GitTimeParser.parse("never");
+		assertEquals(LocalDateTime.MAX, parse);
+	}
+
+	@Test
+	public void now_pointInTime() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+		LocalDateTime parsedNow = GitTimeParser.parse("now", aTime);
+
+		assertEquals(aTime, parsedNow);
+	}
+
+	@Test
+	public void now_systemTime() throws ParseException {
+		LocalDateTime firstNow = GitTimeParser.parse("now");
+		assertEquals(SystemReader.getInstance().civilNow(), firstNow);
+		mockSystemReader.tick(10);
+		LocalDateTime secondNow = GitTimeParser.parse("now");
+		assertTrue(secondNow.isAfter(firstNow));
+	}
+
+	@Test
+	public void weeksAgo() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+		LocalDateTime parse = GitTimeParser.parse("2 weeks ago", aTime);
+		assertEquals(asLocalDateTime("2007-02-07 15:35:00 +0100"), parse);
+	}
+
+	@Test
+	public void daysAndWeeksAgo() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+		LocalDateTime twoWeeksAgoActual = GitTimeParser.parse("2 weeks ago",
+				aTime);
+
+		LocalDateTime twoWeeksAgoExpected = asLocalDateTime(
+				"2007-02-07 15:35:00 +0100");
+		assertEquals(twoWeeksAgoExpected, twoWeeksAgoActual);
+
+		LocalDateTime combinedWhitespace = GitTimeParser
+				.parse("3 days 2 weeks ago", aTime);
+		LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
+				"2007-02-04 15:35:00 +0100");
+		assertEquals(combinedWhitespaceExpected, combinedWhitespace);
+
+		LocalDateTime combinedDots = GitTimeParser.parse("3.day.2.week.ago",
+				aTime);
+		LocalDateTime combinedDotsExpected = asLocalDateTime(
+				"2007-02-04 15:35:00 +0100");
+		assertEquals(combinedDotsExpected, combinedDots);
+	}
+
+	@Test
+	public void hoursAgo() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:00 +0100");
+
+		LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
+				aTime);
+
+		LocalDateTime twoHoursAgoExpected = asLocalDateTime(
+				"2007-02-21 15:35:00 +0100");
+		assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
+	}
+
+	@Test
+	public void hoursAgo_acrossDay() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:00 +0100");
+
+		LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
+				aTime);
+
+		LocalDateTime twoHoursAgoExpected = asLocalDateTime(
+				"2007-02-20 22:35:00 +0100");
+		assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
+	}
+
+	@Test
+	public void minutesHoursAgoCombined() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-04 15:35:00 +0100");
+
+		LocalDateTime combinedWhitespace = GitTimeParser
+				.parse("3 hours 2 minutes ago", aTime);
+		LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
+				"2007-02-04 12:33:00 +0100");
+		assertEquals(combinedWhitespaceExpected, combinedWhitespace);
+
+		LocalDateTime combinedDots = GitTimeParser
+				.parse("3.hours.2.minutes.ago", aTime);
+		LocalDateTime combinedDotsExpected = asLocalDateTime(
+				"2007-02-04 12:33:00 +0100");
+		assertEquals(combinedDotsExpected, combinedDots);
+	}
+
+	@Test
+	public void minutesAgo() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:10 +0100");
+
+		LocalDateTime twoMinutesAgo = GitTimeParser.parse("2 minutes ago",
+				aTime);
+
+		LocalDateTime twoMinutesAgoExpected = asLocalDateTime(
+				"2007-02-21 17:33:10 +0100");
+		assertEquals(twoMinutesAgoExpected, twoMinutesAgo);
+	}
+
+	@Test
+	public void minutesAgo_acrossDay() throws ParseException {
+		LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:10 +0100");
+
+		LocalDateTime minutesAgoActual = GitTimeParser.parse("40 minutes ago",
+				aTime);
+
+		LocalDateTime minutesAgoExpected = asLocalDateTime(
+				"2007-02-20 23:55:10 +0100");
+		assertEquals(minutesAgoExpected, minutesAgoActual);
+	}
+
+	@Test
+	public void iso() throws ParseException {
+		String dateStr = "2007-02-21 15:35:00 +0100";
+
+		LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+		LocalDateTime expected = asLocalDateTime(dateStr);
+		assertEquals(expected, actual);
+	}
+
+	@Test
+	public void rfc() throws ParseException {
+		String dateStr = "Wed, 21 Feb 2007 15:35:00 +0100";
+
+		LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+		LocalDateTime expected = asLocalDateTime(dateStr,
+				"EEE, dd MMM yyyy HH:mm:ss Z");
+		assertEquals(expected, actual);
+	}
+
+	@Test
+	public void shortFmt() throws ParseException {
+		assertParsing("2007-02-21", "yyyy-MM-dd");
+	}
+
+	@Test
+	public void shortWithDots() throws ParseException {
+		assertParsing("2007.02.21", "yyyy.MM.dd");
+	}
+
+	@Test
+	public void shortWithSlash() throws ParseException {
+		assertParsing("02/21/2007", "MM/dd/yyyy");
+	}
+
+	@Test
+	public void shortWithDotsReverse() throws ParseException {
+		assertParsing("21.02.2007", "dd.MM.yyyy");
+	}
+
+	@Test
+	public void defaultFmt() throws ParseException {
+		assertParsing("Wed Feb 21 15:35:00 2007 +0100",
+				"EEE MMM dd HH:mm:ss yyyy Z");
+	}
+
+	@Test
+	public void local() throws ParseException {
+		assertParsing("Wed Feb 21 15:35:00 2007", "EEE MMM dd HH:mm:ss yyyy");
+	}
+
+	private static void assertParsing(String dateStr, String format)
+			throws ParseException {
+		LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+		LocalDateTime expected = asLocalDateTime(dateStr, format);
+		assertEquals(expected, actual);
+	}
+
+	private static LocalDateTime asLocalDateTime(String dateStr) {
+		return asLocalDateTime(dateStr, "yyyy-MM-dd HH:mm:ss Z");
+	}
+
+	private static LocalDateTime asLocalDateTime(String dateStr,
+			String pattern) {
+		DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern);
+		TemporalAccessor ta = fmt
+				.withZone(SystemReader.getInstance().getTimeZoneId())
+				.withLocale(SystemReader.getInstance().getLocale())
+				.parse(dateStr);
+		return ta.isSupported(ChronoField.HOUR_OF_DAY) ? LocalDateTime.from(ta)
+				: LocalDate.from(ta).atStartOfDay();
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java
index 355bbba..6d23db8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java
@@ -10,10 +10,13 @@
 
 package org.eclipse.jgit.util;
 
+import static java.time.Instant.EPOCH;
+import static java.time.ZoneOffset.UTC;
 import static org.junit.Assert.assertEquals;
 
-import java.util.Date;
-import java.util.TimeZone;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Test;
@@ -22,8 +25,8 @@ public class RawParseUtils_ParsePersonIdentTest {
 
 	@Test
 	public void testParsePersonIdent_legalCases() {
-		final Date when = new Date(1234567890000L);
-		final TimeZone tz = TimeZone.getTimeZone("GMT-7");
+		Instant when = Instant.ofEpochMilli(1234567890000L);
+		ZoneId tz = ZoneOffset.ofHours(-7);
 
 		assertPersonIdent("Me <me@example.com> 1234567890 -0700",
 				new PersonIdent("Me", "me@example.com", when, tz));
@@ -50,8 +53,8 @@ public void testParsePersonIdent_legalCases() {
 
 	@Test
 	public void testParsePersonIdent_fuzzyCases() {
-		final Date when = new Date(1234567890000L);
-		final TimeZone tz = TimeZone.getTimeZone("GMT-7");
+		Instant when = Instant.ofEpochMilli(1234567890000L);
+		ZoneId tz = ZoneOffset.ofHours(-7);
 
 		assertPersonIdent(
 				"A U Thor <author@example.com>,  C O. Miter <comiter@example.com> 1234567890 -0700",
@@ -64,8 +67,8 @@ public void testParsePersonIdent_fuzzyCases() {
 
 	@Test
 	public void testParsePersonIdent_incompleteCases() {
-		final Date when = new Date(1234567890000L);
-		final TimeZone tz = TimeZone.getTimeZone("GMT-7");
+		Instant when = Instant.ofEpochMilli(1234567890000L);
+		ZoneId tz = ZoneOffset.ofHours(-7);
 
 		assertPersonIdent("Me <> 1234567890 -0700", new PersonIdent("Me", "",
 				when, tz));
@@ -76,26 +79,26 @@ public void testParsePersonIdent_incompleteCases() {
 		assertPersonIdent(" <> 1234567890 -0700", new PersonIdent("", "", when,
 				tz));
 
-		assertPersonIdent("<>", new PersonIdent("", "", 0, 0));
+		assertPersonIdent("<>", new PersonIdent("", "", EPOCH, UTC));
 
-		assertPersonIdent(" <>", new PersonIdent("", "", 0, 0));
+		assertPersonIdent(" <>", new PersonIdent("", "", EPOCH, UTC));
 
 		assertPersonIdent("<me@example.com>", new PersonIdent("",
-				"me@example.com", 0, 0));
+				"me@example.com", EPOCH, UTC));
 
 		assertPersonIdent(" <me@example.com>", new PersonIdent("",
-				"me@example.com", 0, 0));
+				"me@example.com", EPOCH, UTC));
 
-		assertPersonIdent("Me <>", new PersonIdent("Me", "", 0, 0));
+		assertPersonIdent("Me <>", new PersonIdent("Me", "", EPOCH, UTC));
 
 		assertPersonIdent("Me <me@example.com>", new PersonIdent("Me",
-				"me@example.com", 0, 0));
+				"me@example.com", EPOCH, UTC));
 
 		assertPersonIdent("Me <me@example.com> 1234567890", new PersonIdent(
-				"Me", "me@example.com", 0, 0));
+				"Me", "me@example.com", EPOCH, UTC));
 
 		assertPersonIdent("Me <me@example.com> 1234567890 ", new PersonIdent(
-				"Me", "me@example.com", 0, 0));
+				"Me", "me@example.com", EPOCH, UTC));
 	}
 
 	@Test
@@ -104,6 +107,21 @@ public void testParsePersonIdent_malformedCases() {
 		assertPersonIdent("Me <me@example.com 1234567890 -0700", null);
 	}
 
+	@Test
+	public void testParsePersonIdent_badTz() {
+		PersonIdent tooBig = RawParseUtils
+				.parsePersonIdent("Me <me@example.com> 1234567890 +8315");
+		assertEquals(tooBig.getZoneOffset().getTotalSeconds(), 0);
+
+		PersonIdent tooSmall = RawParseUtils
+				.parsePersonIdent("Me <me@example.com> 1234567890 -8315");
+		assertEquals(tooSmall.getZoneOffset().getTotalSeconds(), 0);
+
+		PersonIdent notATime = RawParseUtils
+				.parsePersonIdent("Me <me@example.com> 1234567890 -0370");
+		assertEquals(notATime.getZoneOffset().getTotalSeconds(), 0);
+	}
+
 	private static void assertPersonIdent(String line, PersonIdent expected) {
 		PersonIdent actual = RawParseUtils.parsePersonIdent(line);
 		assertEquals(expected, actual);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java
index 214bbca..a927d8d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java
@@ -16,7 +16,7 @@
 import static org.eclipse.jgit.util.RelativeDateFormatter.YEAR_IN_MILLIS;
 import static org.junit.Assert.assertEquals;
 
-import java.util.Date;
+import java.time.Instant;
 
 import org.eclipse.jgit.junit.MockSystemReader;
 import org.junit.After;
@@ -37,9 +37,9 @@ public void tearDown() {
 
 	private static void assertFormat(long ageFromNow, long timeUnit,
 			String expectedFormat) {
-		Date d = new Date(SystemReader.getInstance().getCurrentTime()
-				- ageFromNow * timeUnit);
-		String s = RelativeDateFormatter.format(d);
+		long millis = ageFromNow * timeUnit;
+		Instant aTime = SystemReader.getInstance().now().minusMillis(millis);
+		String s = RelativeDateFormatter.format(aTime);
 		assertEquals(expectedFormat, s);
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java
index 015da16..9a1c710 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java
@@ -12,6 +12,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
@@ -172,4 +173,22 @@ public void testCommonPrefix() {
 		assertEquals("foo bar ",
 				StringUtils.commonPrefix("foo bar 42", "foo bar 24"));
 	}
+
+	@Test
+	public void testTrim() {
+		assertEquals("a", StringUtils.trim("a", '/'));
+		assertEquals("aaaa", StringUtils.trim("aaaa", '/'));
+		assertEquals("aaa", StringUtils.trim("/aaa", '/'));
+		assertEquals("aaa", StringUtils.trim("aaa/", '/'));
+		assertEquals("aaa", StringUtils.trim("/aaa/", '/'));
+		assertEquals("aa/aa", StringUtils.trim("/aa/aa/", '/'));
+		assertEquals("aa/aa", StringUtils.trim("aa/aa", '/'));
+
+		assertEquals("", StringUtils.trim("", '/'));
+		assertEquals("", StringUtils.trim("/", '/'));
+		assertEquals("", StringUtils.trim("//", '/'));
+		assertEquals("", StringUtils.trim("///", '/'));
+
+		assertNull(StringUtils.trim(null, '/'));
+	}
 }
diff --git a/org.eclipse.jgit.ui/META-INF/MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
index 04bda44..e0b049c 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: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Vendor: %Bundle-Vendor
 Bundle-RequiredExecutionEnvironment: JavaSE-17
-Export-Package: org.eclipse.jgit.awtui;version="7.0.0"
-Import-Package: org.eclipse.jgit.errors;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.lib;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.nls;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revplot;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.transport;version="[7.0.0,7.1.0)",
- org.eclipse.jgit.util;version="[7.0.0,7.1.0)"
+Export-Package: org.eclipse.jgit.awtui;version="7.3.0"
+Import-Package: org.eclipse.jgit.errors;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.lib;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.nls;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revplot;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.revwalk;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.transport;version="[7.3.0,7.4.0)",
+ org.eclipse.jgit.util;version="[7.3.0,7.4.0)"
diff --git a/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/SOURCE-MANIFEST.MF
index 8170fc6..66617ca 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.ui;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.ui;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.ui/pom.xml b/org.eclipse.jgit.ui/pom.xml
index 6d1e267..3d8d2a1 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-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 caefce3..877a488 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,10 +1,62 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <component id="org.eclipse.jgit" version="2">
-    <resource path="src/org/eclipse/jgit/lib/GpgSignatureVerifier.java" type="org.eclipse.jgit.lib.GpgSignatureVerifier">
-        <filter id="404000815">
+    <resource path="src/org/eclipse/jgit/lib/RefDatabase.java" type="org.eclipse.jgit.lib.RefDatabase">
+        <filter id="336695337">
             <message_arguments>
-                <message_argument value="org.eclipse.jgit.lib.GpgSignatureVerifier"/>
-                <message_argument value="verify(GpgConfig, byte[], byte[])"/>
+                <message_argument value="org.eclipse.jgit.lib.RefDatabase"/>
+                <message_argument value="getReflogReader(Ref)"/>
+            </message_arguments>
+        </filter>
+        <filter id="336695337">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.RefDatabase"/>
+                <message_argument value="getReflogReader(String)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/lib/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter">
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getBoolean(Config, String, String, String, Boolean)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getInt(Config, String, String, String, Integer)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getIntInRange(Config, String, String, String, Integer, Integer, Integer)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getIntInRange(Config, String, String, String, int, int, Integer)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getLong(Config, String, String, String, Long)"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getTimeUnit(Config, String, String, String, Long, TimeUnit)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/revwalk/RevWalk.java" type="org.eclipse.jgit.revwalk.RevWalk">
+        <filter id="1142947843">
+            <message_arguments>
+                <message_argument value="6.10.1"/>
+                <message_argument value="isMergedIntoAnyCommit(RevCommit, Collection&lt;RevCommit&gt;)"/>
             </message_arguments>
         </filter>
     </resource>
diff --git a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs
index c4dc76f..ef3d8ec 100644
--- a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs
+++ b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs
@@ -40,7 +40,7 @@
 org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=error
 org.eclipse.jdt.core.compiler.problem.invalidJavadoc=error
 org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled
-org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=disabled
 org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled
 org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=private
 org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 2cdfa99..5f851ec 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -3,14 +3,14 @@
 Bundle-Name: %Bundle-Name
 Automatic-Module-Name: org.eclipse.jgit
 Bundle-SymbolicName: org.eclipse.jgit
-Bundle-Version: 7.0.0.qualifier
+Bundle-Version: 7.3.0.qualifier
 Bundle-Localization: OSGI-INF/l10n/plugin
 Bundle-Vendor: %Bundle-Vendor
 Bundle-ActivationPolicy: lazy
 Service-Component: OSGI-INF/org.eclipse.jgit.internal.util.CleanupService.xml
 Eclipse-ExtensibleAPI: true
-Export-Package: org.eclipse.jgit.annotations;version="7.0.0",
- org.eclipse.jgit.api;version="7.0.0";
+Export-Package: org.eclipse.jgit.annotations;version="7.3.0",
+ org.eclipse.jgit.api;version="7.3.0";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.notes,
    org.eclipse.jgit.dircache,
@@ -25,72 +25,77 @@
    org.eclipse.jgit.revwalk.filter,
    org.eclipse.jgit.blame,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.api.errors;version="7.0.0";
+ org.eclipse.jgit.api.errors;version="7.3.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.errors",
- org.eclipse.jgit.attributes;version="7.0.0";
+ org.eclipse.jgit.attributes;version="7.3.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.blame;version="7.0.0";
+ org.eclipse.jgit.blame;version="7.3.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
-   org.eclipse.jgit.treewalk.filter,
-   org.eclipse.jgit.diff",
- org.eclipse.jgit.diff;version="7.0.0";
+   org.eclipse.jgit.blame.cache,
+   org.eclipse.jgit.diff,
+   org.eclipse.jgit.treewalk.filter",
+ org.eclipse.jgit.blame.cache;version="7.3.0";
+  uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.diff;version="7.3.0";
   uses:="org.eclipse.jgit.lib,
-   org.eclipse.jgit.attributes,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.patch,
+   org.eclipse.jgit.attributes,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util",
- org.eclipse.jgit.dircache;version="7.0.0";
+ org.eclipse.jgit.dircache;version="7.3.0";
   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="7.0.0";
+ org.eclipse.jgit.errors;version="7.3.0";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.dircache,
-   org.eclipse.jgit.lib,
-   org.eclipse.jgit.internal.storage.pack",
- org.eclipse.jgit.events;version="7.0.0";
+   org.eclipse.jgit.lib",
+ org.eclipse.jgit.events;version="7.3.0";
   uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.fnmatch;version="7.0.0",
- org.eclipse.jgit.gitrepo;version="7.0.0";
+ org.eclipse.jgit.fnmatch;version="7.3.0",
+ org.eclipse.jgit.gitrepo;version="7.3.0";
   uses:="org.xml.sax.helpers,
    org.eclipse.jgit.api,
+   org.eclipse.jgit.api.errors,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.xml.sax",
- org.eclipse.jgit.gitrepo.internal;version="7.0.0";x-internal:=true,
- org.eclipse.jgit.hooks;version="7.0.0";uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.ignore;version="7.0.0",
- org.eclipse.jgit.ignore.internal;version="7.0.0";
+ org.eclipse.jgit.gitrepo.internal;version="7.3.0";x-internal:=true,
+ org.eclipse.jgit.hooks;version="7.3.0";
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.util",
+ org.eclipse.jgit.ignore;version="7.3.0",
+ org.eclipse.jgit.ignore.internal;version="7.3.0";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal;version="7.0.0";
+ org.eclipse.jgit.internal;version="7.3.0";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.http.test",
- org.eclipse.jgit.internal.diff;version="7.0.0";
+ org.eclipse.jgit.internal.diff;version="7.3.0";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.diffmergetool;version="7.0.0";
+ org.eclipse.jgit.internal.diffmergetool;version="7.3.0";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.pgm,
    org.eclipse.egit.ui",
- org.eclipse.jgit.internal.fsck;version="7.0.0";
+ org.eclipse.jgit.internal.fsck;version="7.3.0";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.revwalk;version="7.0.0";
+ org.eclipse.jgit.internal.revwalk;version="7.3.0";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.storage.commitgraph;version="7.0.0";
+ org.eclipse.jgit.internal.storage.commitgraph;version="7.3.0";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.storage.dfs;version="7.0.0";
+ org.eclipse.jgit.internal.storage.dfs;version="7.3.0";
   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="7.0.0";
+ org.eclipse.jgit.internal.storage.file;version="7.3.0";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.junit.http,
@@ -99,41 +104,43 @@
    org.eclipse.jgit.pgm,
    org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.ssh.apache",
- org.eclipse.jgit.internal.storage.io;version="7.0.0";
+ org.eclipse.jgit.internal.storage.io;version="7.3.0";
   x-friends:="org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.memory;version="7.0.0";
+ org.eclipse.jgit.internal.storage.memory;version="7.3.0";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.storage.pack;version="7.0.0";
+ org.eclipse.jgit.internal.storage.midx;version="7.3.0";x-internal:=true,
+ org.eclipse.jgit.internal.storage.pack;version="7.3.0";
   x-friends:="org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.reftable;version="7.0.0";
+ org.eclipse.jgit.internal.storage.reftable;version="7.3.0";
   x-friends:="org.eclipse.jgit.http.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.submodule;version="7.0.0";x-internal:=true,
- org.eclipse.jgit.internal.transport.connectivity;version="7.0.0";
+ org.eclipse.jgit.internal.submodule;version="7.3.0";x-internal:=true,
+ org.eclipse.jgit.internal.transport.connectivity;version="7.3.0";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.http;version="7.0.0";
+ org.eclipse.jgit.internal.transport.http;version="7.3.0";
   x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.parser;version="7.0.0";
+ org.eclipse.jgit.internal.transport.parser;version="7.3.0";
   x-friends:="org.eclipse.jgit.http.server,
    org.eclipse.jgit.test",
- org.eclipse.jgit.internal.transport.ssh;version="7.0.0";
+ org.eclipse.jgit.internal.transport.ssh;version="7.3.0";
   x-friends:="org.eclipse.jgit.ssh.apache,
    org.eclipse.jgit.ssh.jsch,
    org.eclipse.jgit.test",
- org.eclipse.jgit.internal.util;version="7.0.0";
-  x-friends:=" org.eclipse.jgit.junit",
- org.eclipse.jgit.lib;version="7.0.0";
+ org.eclipse.jgit.internal.util;version="7.3.0";
+  x-friends:="org.eclipse.jgit.junit",
+ org.eclipse.jgit.lib;version="7.3.0";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.util.sha1,
    org.eclipse.jgit.dircache,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.internal.storage.file,
+   org.eclipse.jgit.api,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.events,
    com.googlecode.javaewah,
@@ -142,12 +149,12 @@
    org.eclipse.jgit.util,
    org.eclipse.jgit.submodule,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.lib.internal;version="7.0.0";
+ org.eclipse.jgit.lib.internal;version="7.3.0";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.pgm,
    org.eclipse.egit.ui",
- org.eclipse.jgit.logging;version="7.0.0",
- org.eclipse.jgit.merge;version="7.0.0";
+ org.eclipse.jgit.logging;version="7.3.0",
+ org.eclipse.jgit.merge;version="7.3.0";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
@@ -156,67 +163,69 @@
    org.eclipse.jgit.util,
    org.eclipse.jgit.api,
    org.eclipse.jgit.attributes",
- org.eclipse.jgit.nls;version="7.0.0",
- org.eclipse.jgit.notes;version="7.0.0";
+ org.eclipse.jgit.nls;version="7.3.0",
+ org.eclipse.jgit.notes;version="7.3.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.patch;version="7.0.0";
+ org.eclipse.jgit.patch;version="7.3.0";
   uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.revwalk,
    org.eclipse.jgit.diff",
- org.eclipse.jgit.revplot;version="7.0.0";
-  uses:="org.eclipse.jgit.lib,
-   org.eclipse.jgit.revwalk",
- org.eclipse.jgit.revwalk;version="7.0.0";
-  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="7.0.0";
+ org.eclipse.jgit.revplot;version="7.3.0";
   uses:="org.eclipse.jgit.revwalk,
-   org.eclipse.jgit.lib,
-   org.eclipse.jgit.util",
- org.eclipse.jgit.storage.file;version="7.0.0";
+   org.eclipse.jgit.lib",
+ org.eclipse.jgit.revwalk;version="7.3.0";
   uses:="org.eclipse.jgit.lib,
-   org.eclipse.jgit.util",
- org.eclipse.jgit.storage.pack;version="7.0.0";
-  uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.submodule;version="7.0.0";
-  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.revwalk.filter,
    org.eclipse.jgit.diff,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.treewalk,
+   org.eclipse.jgit.internal.storage.commitgraph",
+ org.eclipse.jgit.revwalk.filter;version="7.3.0";
+  uses:="org.eclipse.jgit.revwalk,
+   org.eclipse.jgit.lib,
    org.eclipse.jgit.util",
- org.eclipse.jgit.transport;version="7.0.0";
+ org.eclipse.jgit.storage.file;version="7.3.0";
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.util",
+ org.eclipse.jgit.storage.pack;version="7.3.0";
+  uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.submodule;version="7.3.0";
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.treewalk.filter,
+   org.eclipse.jgit.diff,
+   org.eclipse.jgit.treewalk,
+   org.eclipse.jgit.util",
+ org.eclipse.jgit.transport;version="7.3.0";
   uses:="javax.crypto,
+   org.eclipse.jgit.hooks,
    org.eclipse.jgit.util.io,
    org.eclipse.jgit.lib,
-   org.eclipse.jgit.revwalk,
    org.eclipse.jgit.transport.http,
-   org.eclipse.jgit.internal.storage.file,
+   org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util,
    org.eclipse.jgit.internal.storage.pack,
    org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.storage.pack,
    org.eclipse.jgit.errors",
- org.eclipse.jgit.transport.http;version="7.0.0";
+ org.eclipse.jgit.transport.http;version="7.3.0";
   uses:="javax.net.ssl",
- org.eclipse.jgit.transport.resolver;version="7.0.0";
+ org.eclipse.jgit.transport.resolver;version="7.3.0";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.lib",
- org.eclipse.jgit.treewalk;version="7.0.0";
+ org.eclipse.jgit.treewalk;version="7.3.0";
   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="7.0.0";
+ org.eclipse.jgit.treewalk.filter;version="7.3.0";
   uses:="org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util;version="7.0.0";
+ org.eclipse.jgit.util;version="7.3.0";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.hooks,
    org.eclipse.jgit.revwalk,
@@ -229,18 +238,18 @@
    org.eclipse.jgit.treewalk,
    javax.net.ssl,
    org.eclipse.jgit.util.time",
- org.eclipse.jgit.util.io;version="7.0.0";
+ org.eclipse.jgit.util.io;version="7.3.0";
   uses:="org.eclipse.jgit.attributes,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util.sha1;version="7.0.0",
- org.eclipse.jgit.util.time;version="7.0.0"
+ org.eclipse.jgit.util.sha1;version="7.3.0",
+ org.eclipse.jgit.util.time;version="7.3.0"
 Bundle-RequiredExecutionEnvironment: JavaSE-17
 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
  javax.crypto,
  javax.management,
  javax.net.ssl,
- org.apache.commons.codec.digest;version="1.15.0",
+ org.apache.commons.codec.digest;version="[1.15.0,2.0.0)",
  org.slf4j;version="[1.7.0,3.0.0)",
  org.xml.sax,
  org.xml.sax.helpers
diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
index 7531018..a9e669b 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: 7.0.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit;version="7.0.0.qualifier";roots="."
+Bundle-Version: 7.3.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit;version="7.3.0.qualifier";roots="."
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index 9397674..c65a440 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>7.0.0-SNAPSHOT</version>
+    <version>7.3.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit</artifactId>
@@ -49,7 +49,6 @@
     <dependency>
       <groupId>commons-codec</groupId>
       <artifactId>commons-codec</artifactId>
-      <version>1.16.0</version>
     </dependency>
 
   </dependencies>
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 19c9008..27270a1 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -64,8 +64,6 @@
 binaryHunkInvalidLength=Binary hunk, line {0}: input corrupt; expected length byte, got 0x{1}
 binaryHunkLineTooShort=Binary hunk, line {0}: input ended prematurely
 binaryHunkMissingNewline=Binary hunk, line {0}: input line not terminated by newline
-bitmapAccessErrorForPackfile=Error whilst trying to access bitmap file for {}
-bitmapFailedToGet=Failed to get bitmap index file {}
 bitmapMissingObject=Bitmap at {0} is missing {1}.
 bitmapsMustBePrepared=Bitmaps must be prepared before they may be written.
 bitmapUseNoopNoListener=Use NOOP instance for no listener
@@ -78,6 +76,7 @@
 buildingBitmaps=Building bitmaps
 cachedPacksPreventsIndexCreation=Using cached packs prevents index creation
 cachedPacksPreventsListingObjects=Using cached packs prevents listing objects
+cacheRegionAllOrNoneNull=expected all null or none: {0}, {1}
 cannotAccessLastModifiedForSafeDeletion=Unable to access lastModifiedTime of file {0}, skip deletion since we cannot safely avoid race condition
 cannotBeCombined=Cannot be combined.
 cannotBeRecursiveWhenTreesAreIncluded=TreeWalk shouldn't be recursive when tree objects are included.
@@ -267,6 +266,7 @@
 deletingNotSupported=Deleting {0} not supported.
 depthMustBeAt1=Depth must be >= 1
 depthWithUnshallow=Depth and unshallow can\'t be used together
+deprecatedTrustFolderStat=Option core.trustFolderStat is deprecated, replace it by core.trustStat.
 destinationIsNotAWildcard=Destination is not a wildcard.
 detachedHeadDetected=HEAD is detached
 diffToolNotGivenError=No diff tool provided and no defaults configured.
@@ -285,6 +285,9 @@
 downloadCancelled=Download cancelled
 downloadCancelledDuringIndexing=Download cancelled during indexing
 duplicateAdvertisementsOf=duplicate advertisements of {0}
+duplicateCacheTablesGiven=Duplicate cache tables given
+duplicatePackExtensionsForCacheTables=Duplicate pack extension {0} in cache tables
+duplicatePackExtensionsSet=Attempting to configure duplicate pack extensions: {0}.{1}.{2} contains {3}
 duplicateRef=Duplicate ref: {0}
 duplicateRefAttribute=Duplicate ref attribute: {0}
 duplicateRemoteRefUpdateIsIllegal=Duplicate remote ref update is illegal. Affected remote name: {0}
@@ -461,6 +464,7 @@
 invalidTimeUnitValue2=Invalid time unit value: {0}.{1}={2}
 invalidTimeUnitValue3=Invalid time unit value: {0}.{1}.{2}={3}
 invalidTreeZeroLengthName=Cannot append a tree entry with zero-length name
+invalidTrustStat=core.trustStat must not be set to TrustStat.INHERIT, falling back to TrustStat.ALWAYS.
 invalidURL=Invalid URL {0}
 invalidWildcards=Invalid wildcards {0}
 invalidRefSpec=Invalid refspec {0}
@@ -499,7 +503,7 @@
 mergeStrategyAlreadyExistsAsDefault=Merge strategy "{0}" already exists as a default strategy
 mergeStrategyDoesNotSupportHeads=merge strategy {0} does not support {1} heads to be merged into HEAD
 mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4}
-mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1}
+mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1}\nFailing paths: {2}
 mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n  count {3}"
 mergeToolNotGivenError=No merge tool provided and no defaults configured.
 mergeToolNullError=Parameter for merge tool cannot be null.
@@ -524,6 +528,8 @@
 month=month
 months=months
 monthsAgo={0} months ago
+multiPackIndexUnexpectedSize=MultiPack index: expected %d bytes but out has %d bytes
+multiPackIndexWritingCancelled=Multipack index writing was canceled
 multipleMergeBasesFor=Multiple merge bases for:\n  {0}\n  {1} found:\n  {2}\n  {3}
 nameMustNotBeNullOrEmpty=Ref name must not be null or empty.
 need2Arguments=Need 2 arguments
@@ -539,6 +545,8 @@
 noMergeHeadSpecified=No merge head specified
 nonBareLinkFilesNotSupported=Link files are not supported with nonbare repos
 nonCommitToHeads=Cannot point a branch to a non-commit object
+noPackExtConfigurationGiven=No PackExt configuration given
+noPackExtGivenForConfiguration=No PackExt given for configuration
 noPathAttributesFound=No Attributes found for {0}.
 noSuchRef=no such ref
 noSuchRefKnown=no such ref: {0}
@@ -571,7 +579,6 @@
 oldIdMustNotBeNull=Expected old ID must not be null
 onlyOneFetchSupported=Only one fetch supported
 onlyOneOperationCallPerConnectionIsSupported=Only one operation call per connection is supported.
-onlyOpenPgpSupportedForSigning=OpenPGP is the only supported signing option with JGit at this time (gpg.format must be set to openpgp).
 openFilesMustBeAtLeast1=Open files must be >= 1
 openingConnection=Opening connection
 operationCanceled=Operation {0} was canceled
@@ -593,6 +600,8 @@
 packingCancelledDuringObjectsWriting=Packing cancelled during objects writing
 packObjectCountMismatch=Pack object count mismatch: pack {0} index {1}: {2}
 packRefs=Pack refs
+packRefsFailed=Packing refs failed
+packRefsSuccessful=Packed refs successfully
 packSizeNotSetYet=Pack size not yet set since it has not yet been received
 packTooLargeForIndexVersion1=Pack too large for index version 1
 packWasDeleted=Pack file {0} was deleted, removing it from pack list
@@ -609,6 +618,7 @@
 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}.
+precedenceTrustConfig=Both core.trustFolderStat and core.trustStat are set, ignoring trustFolderStat since trustStat takes precedence. Remove core.trustFolderStat from your configuration.
 prefixRemote=remote:
 problemWithResolvingPushRefSpecsLocally=Problem with resolving push ref specs locally: {0}
 progressMonUploading=Uploading {0}
@@ -636,8 +646,6 @@
 readerIsRequired=Reader is required
 readingObjectsFromLocalRepositoryFailed=reading objects from local repository failed: {0}
 readLastModifiedFailed=Reading lastModified of {0} failed
-readPipeIsNotAllowed=FS.readPipe() isn't allowed for command ''{0}''. Working directory: ''{1}''.
-readPipeIsNotAllowedRequiredPermission=FS.readPipe() isn't allowed for command ''{0}''. Working directory: ''{1}''. Required permission: {2}.
 readTimedOut=Read timed out after {0} ms
 receivePackObjectTooLarge1=Object too large, rejecting the pack. Max object size limit is {0} bytes.
 receivePackObjectTooLarge2=Object too large ({0} bytes), rejecting the pack. Max object size limit is {1} bytes.
@@ -718,6 +726,8 @@
 shutdownCleanup=Cleanup {} during JVM shutdown
 shutdownCleanupFailed=Cleanup during JVM shutdown failed
 shutdownCleanupListenerFailed=Cleanup of {0} during JVM shutdown failed
+signatureServiceConflict={0} conflict for type {1}. Already registered is {2}; additional factory {3} is ignored.
+signatureTypeUnknown=No signer for {0} signatures. Use another signature type for git config gpg.format, or do not sign.
 signatureVerificationError=Signature verification failed
 signatureVerificationUnavailable=No signature verifier registered
 signedTagMessageNoLf=A non-empty message of a signed tag must end in LF.
@@ -803,6 +813,7 @@
 tSizeMustBeGreaterOrEqual1=tSize must be >= 1
 unableToCheckConnectivity=Unable to check connectivity.
 unableToCreateNewObject=Unable to create new object: {0}
+unableToReadFullArray=Unable to read an array with {0} elements from the stream
 unableToReadFullInt=Unable to read a full int from the stream
 unableToReadPackfile=Unable to read packfile {0}
 unableToRemovePath=Unable to remove path ''{0}''
@@ -829,6 +840,7 @@
 unknownObjectInIndex=unknown object {0} found in index but not in pack file
 unknownObjectType=Unknown object type {0}.
 unknownObjectType2=unknown
+unknownPackExtension=Unknown pack extension: {0}.{1}.{2}={3}
 unknownPositionEncoding=Unknown position encoding %s
 unknownRefStorageFormat=Unknown ref storage format "{0}"
 unknownRepositoryFormat=Unknown repository format
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
index c895dc9..b4d1cab 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
- * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com> and others
+ * Copyright (C) 2010, 2025 Stefan Lay <stefan.lay@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
@@ -17,6 +17,7 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.text.MessageFormat;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
@@ -59,8 +60,15 @@ public class AddCommand extends GitCommand<DirCache> {
 
 	private WorkingTreeIterator workingTreeIterator;
 
+	// Update only known index entries, don't add new ones. If there's no file
+	// for an index entry, remove it: stage deletions.
 	private boolean update = false;
 
+	// If TRUE, also stage deletions, otherwise only update and add index
+	// entries.
+	// If not set explicitly
+	private Boolean all;
+
 	// This defaults to true because it's what JGit has been doing
 	// traditionally. The C git default would be false.
 	private boolean renormalize = true;
@@ -82,6 +90,17 @@ public AddCommand(Repository repo) {
 	 * A directory name (e.g. <code>dir</code> to add <code>dir/file1</code> and
 	 * <code>dir/file2</code>) can also be given to add all files in the
 	 * directory, recursively. Fileglobs (e.g. *.c) are not yet supported.
+	 * </p>
+	 * <p>
+	 * If a pattern {@code "."} is added, all changes in the git repository's
+	 * working tree will be added.
+	 * </p>
+	 * <p>
+	 * File patterns are required unless {@code isUpdate() == true} or
+	 * {@link #setAll(boolean)} is called. If so and no file patterns are given,
+	 * all changes will be added (i.e., a file pattern of {@code "."} is
+	 * implied).
+	 * </p>
 	 *
 	 * @param filepattern
 	 *            repository-relative path of file/directory to add (with
@@ -113,15 +132,41 @@ public AddCommand setWorkingTreeIterator(WorkingTreeIterator f) {
 	 * Executes the {@code Add} command. Each instance of this class should only
 	 * be used for one invocation of the command. Don't call this method twice
 	 * on an instance.
+	 * </p>
+	 *
+	 * @throws JGitInternalException
+	 *             on errors, but also if {@code isUpdate() == true} _and_
+	 *             {@link #setAll(boolean)} had been called
+	 * @throws NoFilepatternException
+	 *             if no file patterns are given if {@code isUpdate() == false}
+	 *             and {@link #setAll(boolean)} was not called
 	 */
 	@Override
 	public DirCache call() throws GitAPIException, NoFilepatternException {
-
-		if (filepatterns.isEmpty())
-			throw new NoFilepatternException(JGitText.get().atLeastOnePatternIsRequired);
 		checkCallable();
+
+		if (update && all != null) {
+			throw new JGitInternalException(MessageFormat.format(
+					JGitText.get().illegalCombinationOfArguments,
+					"--update", "--all/--no-all")); //$NON-NLS-1$ //$NON-NLS-2$
+		}
+		boolean addAll;
+		if (filepatterns.isEmpty()) {
+			if (update || all != null) {
+				addAll = true;
+			} else {
+				throw new NoFilepatternException(
+						JGitText.get().atLeastOnePatternIsRequired);
+			}
+		} else {
+			addAll = filepatterns.contains("."); //$NON-NLS-1$
+			if (all == null && !update) {
+				all = Boolean.TRUE;
+			}
+		}
+		boolean stageDeletions = update || (all != null && all.booleanValue());
+
 		DirCache dc = null;
-		boolean addAll = filepatterns.contains("."); //$NON-NLS-1$
 
 		try (ObjectInserter inserter = repo.newObjectInserter();
 				NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) {
@@ -181,7 +226,8 @@ public DirCache call() throws GitAPIException, NoFilepatternException {
 
 				if (f == null) { // working tree file does not exist
 					if (entry != null
-							&& (!update || GITLINK == entry.getFileMode())) {
+							&& (!stageDeletions
+									|| GITLINK == entry.getFileMode())) {
 						builder.add(entry);
 					}
 					continue;
@@ -252,7 +298,8 @@ public DirCache call() throws GitAPIException, NoFilepatternException {
 	}
 
 	/**
-	 * Set whether to only match against already tracked files
+	 * Set whether to only match against already tracked files. If
+	 * {@code update == true}, re-sets a previous {@link #setAll(boolean)}.
 	 *
 	 * @param update
 	 *            If set to true, the command only matches {@code filepattern}
@@ -314,4 +361,32 @@ public AddCommand setRenormalize(boolean renormalize) {
 	public boolean isRenormalize() {
 		return renormalize;
 	}
+
+	/**
+	 * Defines whether the command will use '--all' mode: update existing index
+	 * entries, add new entries, and remove index entries for which there is no
+	 * file. (In other words: also stage deletions.)
+	 * <p>
+	 * The setting is independent of {@link #setUpdate(boolean)}.
+	 * </p>
+	 *
+	 * @param all
+	 *            whether to enable '--all' mode
+	 * @return {@code this}
+	 * @since 7.2
+	 */
+	public AddCommand setAll(boolean all) {
+		this.all = Boolean.valueOf(all);
+		return this;
+	}
+
+	/**
+	 * Tells whether '--all' has been set for this command.
+	 *
+	 * @return {@code true} if it was set; {@code false} otherwise
+	 * @since 7.2
+	 */
+	public boolean isAll() {
+		return all != null && all.booleanValue();
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
index c133219..32c242f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
@@ -644,24 +644,6 @@ public CheckoutCommand setOrphan(boolean orphan) {
 	/**
 	 * Specify to force the ref update in case of a branch switch.
 	 *
-	 * @param force
-	 *            if <code>true</code> and the branch with the given name
-	 *            already exists, the start-point of an existing branch will be
-	 *            set to a new start-point; if false, the existing branch will
-	 *            not be changed
-	 * @return this instance
-	 * @deprecated this method was badly named comparing its semantics to native
-	 *             git's checkout --force option, use
-	 *             {@link #setForceRefUpdate(boolean)} instead
-	 */
-	@Deprecated
-	public CheckoutCommand setForce(boolean force) {
-		return setForceRefUpdate(force);
-	}
-
-	/**
-	 * Specify to force the ref update in case of a branch switch.
-	 *
 	 * In releases prior to 5.2 this method was called setForce() but this name
 	 * was misunderstood to implement native git's --force option, which is not
 	 * 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 3e034f1..4a536b9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
@@ -67,6 +67,8 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> {
 
 	private boolean bare;
 
+	private boolean relativePaths;
+
 	private FS fs;
 
 	private String remote = Constants.DEFAULT_REMOTE_NAME;
@@ -264,6 +266,7 @@ void verifyDirectories(URIish u) {
 	private Repository init() throws GitAPIException {
 		InitCommand command = Git.init();
 		command.setBare(bare);
+		command.setRelativeDirs(relativePaths);
 		if (fs != null) {
 			command.setFs(fs);
 		}
@@ -555,6 +558,20 @@ public CloneCommand setBare(boolean bare) throws IllegalStateException {
 	}
 
 	/**
+	 * Set whether the cloned repository shall use relative paths for GIT_DIR
+	 * and GIT_WORK_TREE
+	 *
+	 * @param relativePaths
+	 *            if true, use relative paths for GIT_DIR and GIT_WORK_TREE
+	 * @return this instance
+	 * @since 7.2
+	 */
+	public CloneCommand setRelativePaths(boolean relativePaths) {
+		this.relativePaths = relativePaths;
+		return this;
+	}
+
+	/**
 	 * Set the file system abstraction to be used for repositories created by
 	 * this command.
 	 *
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 a1a2cc0..a7d409c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -51,9 +51,6 @@
 import org.eclipse.jgit.lib.Constants;
 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;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -62,6 +59,8 @@
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.lib.Signers;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
@@ -129,7 +128,7 @@ public class CommitCommand extends GitCommand<RevCommit> {
 
 	private String signingKey;
 
-	private GpgSigner gpgSigner;
+	private Signer signer;
 
 	private GpgConfig gpgConfig;
 
@@ -319,30 +318,22 @@ private void checkIfEmpty(RevWalk rw, ObjectId headId, ObjectId indexTreeId)
 		}
 	}
 
-	private void sign(CommitBuilder commit) throws ServiceUnavailableException,
-			CanceledException, UnsupportedSigningFormatException {
-		if (gpgSigner == null) {
-			gpgSigner = GpgSigner.getDefault();
-			if (gpgSigner == null) {
-				throw new ServiceUnavailableException(
-						JGitText.get().signingServiceUnavailable);
+	private void sign(CommitBuilder commit)
+			throws CanceledException, IOException,
+			UnsupportedSigningFormatException {
+		if (signer == null) {
+			signer = Signers.get(gpgConfig.getKeyFormat());
+			if (signer == null) {
+				throw new UnsupportedSigningFormatException(MessageFormat
+						.format(JGitText.get().signatureTypeUnknown,
+								gpgConfig.getKeyFormat().toConfigValue()));
 			}
 		}
 		if (signingKey == null) {
 			signingKey = gpgConfig.getSigningKey();
 		}
-		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);
-		}
+		signer.signObject(repo, gpgConfig, commit, committer, signingKey,
+				credentialsProvider);
 	}
 
 	private void updateRef(RepositoryState state, ObjectId headId,
@@ -1097,22 +1088,22 @@ public CommitCommand setSign(Boolean sign) {
 	}
 
 	/**
-	 * Sets the {@link GpgSigner} to use if the commit is to be signed.
+	 * Sets the {@link Signer} 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
+	 * @since 7.0
 	 */
-	public CommitCommand setGpgSigner(GpgSigner signer) {
+	public CommitCommand setSigner(Signer signer) {
 		checkCallable();
-		this.gpgSigner = signer;
+		this.signer = signer;
 		return this;
 	}
 
 	/**
 	 * Sets an external {@link GpgConfig} to use. Whether it will be used is at
-	 * the discretion of the {@link #setGpgSigner(GpgSigner)}.
+	 * the discretion of the {@link #setSigner(Signer)}.
 	 *
 	 * @param config
 	 *            to set; if {@code null}, the config will be loaded from the
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
index 805a886..d252628 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
@@ -15,11 +15,11 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -76,6 +76,11 @@ public class DescribeCommand extends GitCommand<String> {
 	private List<FileNameMatcher> matchers = new ArrayList<>();
 
 	/**
+	 * Pattern matchers to be applied to tags for exclusion.
+	 */
+	private List<FileNameMatcher> excludeMatchers = new ArrayList<>();
+
+	/**
 	 * Whether to use all refs in the refs/ namespace
 	 */
 	private boolean useAll;
@@ -263,6 +268,27 @@ public DescribeCommand setMatch(String... patterns) throws InvalidPatternExcepti
 		return this;
 	}
 
+	/**
+	 * Sets one or more {@code glob(7)} patterns that tags must not match to be
+	 * considered. If multiple patterns are provided, they will all be applied.
+	 *
+	 * @param patterns
+	 *            the {@code glob(7)} pattern or patterns
+	 * @return {@code this}
+	 * @throws org.eclipse.jgit.errors.InvalidPatternException
+	 *             if the pattern passed in was invalid.
+	 * @see <a href=
+	 *      "https://www.kernel.org/pub/software/scm/git/docs/git-describe.html"
+	 *      >Git documentation about describe</a>
+	 * @since 7.2
+	 */
+	public DescribeCommand setExclude(String... patterns) throws InvalidPatternException {
+		for (String p : patterns) {
+			excludeMatchers.add(new FileNameMatcher(p, null));
+		}
+		return this;
+	}
+
 	private final Comparator<Ref> TAG_TIE_BREAKER = new Comparator<>() {
 
 		@Override
@@ -274,25 +300,28 @@ public int compare(Ref o1, Ref o2) {
 			}
 		}
 
-		private Date tagDate(Ref tag) throws IOException {
+		private Instant tagDate(Ref tag) throws IOException {
 			RevTag t = w.parseTag(tag.getObjectId());
 			w.parseBody(t);
-			return t.getTaggerIdent().getWhen();
+			return t.getTaggerIdent().getWhenAsInstant();
 		}
 	};
 
 	private Optional<Ref> getBestMatch(List<Ref> tags) {
 		if (tags == null || tags.isEmpty()) {
 			return Optional.empty();
-		} else if (matchers.isEmpty()) {
+		} else if (matchers.isEmpty() && excludeMatchers.isEmpty()) {
 			Collections.sort(tags, TAG_TIE_BREAKER);
 			return Optional.of(tags.get(0));
-		} else {
+		}
+
+		Stream<Ref> matchingTags;
+		if (!matchers.isEmpty()) {
 			// Find the first tag that matches in the stream of all tags
 			// filtered by matchers ordered by tie break order
-			Stream<Ref> matchingTags = Stream.empty();
+			matchingTags = Stream.empty();
 			for (FileNameMatcher matcher : matchers) {
-				Stream<Ref> m = tags.stream().filter(
+				Stream<Ref> m = tags.stream().filter( //
 						tag -> {
 							matcher.append(formatRefName(tag.getName()));
 							boolean result = matcher.isMatch();
@@ -301,8 +330,22 @@ private Optional<Ref> getBestMatch(List<Ref> tags) {
 						});
 				matchingTags = Stream.of(matchingTags, m).flatMap(i -> i);
 			}
-			return matchingTags.sorted(TAG_TIE_BREAKER).findFirst();
+		} else {
+			// If there are no matchers, there are only excluders
+			// Assume all tags match for now before applying excluders
+			matchingTags = tags.stream();
 		}
+
+		for (FileNameMatcher matcher : excludeMatchers) {
+			matchingTags = matchingTags.filter( //
+					tag -> {
+						matcher.append(formatRefName(tag.getName()));
+						boolean result = matcher.isMatch();
+						matcher.reset();
+						return !result;
+					});
+		}
+		return matchingTags.sorted(TAG_TIE_BREAKER).findFirst();
 	}
 
 	private ObjectId getObjectIdFromRef(Ref r) throws JGitInternalException {
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 0713c38..f24127b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
@@ -124,7 +124,7 @@ private FetchRecurseSubmodulesMode getRecurseMode(String path) {
 		FetchRecurseSubmodulesMode mode = repo.getConfig().getEnum(
 				FetchRecurseSubmodulesMode.values(),
 				ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
-				ConfigConstants.CONFIG_KEY_FETCH_RECURSE_SUBMODULES, null);
+				ConfigConstants.CONFIG_KEY_FETCH_RECURSE_SUBMODULES);
 		if (mode != null) {
 			return mode;
 		}
@@ -132,7 +132,7 @@ private FetchRecurseSubmodulesMode getRecurseMode(String path) {
 		// Fall back to fetch.recurseSubmodules, if set
 		mode = repo.getConfig().getEnum(FetchRecurseSubmodulesMode.values(),
 				ConfigConstants.CONFIG_FETCH_SECTION, null,
-				ConfigConstants.CONFIG_KEY_RECURSE_SUBMODULES, null);
+				ConfigConstants.CONFIG_KEY_RECURSE_SUBMODULES);
 		if (mode != null) {
 			return mode;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java
index 88d7e91..f6935e1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java
@@ -12,6 +12,7 @@
 import java.io.IOException;
 import java.text.MessageFormat;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.Date;
 import java.util.Properties;
 import java.util.concurrent.ExecutionException;
@@ -59,7 +60,7 @@ public class GarbageCollectCommand extends GitCommand<Properties> {
 
 	private ProgressMonitor monitor;
 
-	private Date expire;
+	private Instant expire;
 
 	private PackConfig pconfig;
 
@@ -98,8 +99,29 @@ public GarbageCollectCommand setProgressMonitor(ProgressMonitor monitor) {
 	 * @param expire
 	 *            minimal age of objects to be pruned.
 	 * @return this instance
+	 * @deprecated use {@link #setExpire(Instant)} instead
 	 */
+	@Deprecated(since = "7.2")
 	public GarbageCollectCommand setExpire(Date expire) {
+		if (expire != null) {
+			this.expire = expire.toInstant();
+		}
+		return this;
+	}
+
+	/**
+	 * During gc() or prune() each unreferenced, loose object which has been
+	 * created or modified after <code>expire</code> will not be pruned. Only
+	 * older objects may be pruned. If set to null then every object is a
+	 * candidate for pruning. Use {@link org.eclipse.jgit.util.GitTimeParser} to
+	 * parse time formats used by git gc.
+	 *
+	 * @param expire
+	 *            minimal age of objects to be pruned.
+	 * @return this instance
+	 * @since 7.2
+	 */
+	public GarbageCollectCommand setExpire(Instant expire) {
 		this.expire = expire;
 		return this;
 	}
@@ -108,8 +130,8 @@ public GarbageCollectCommand setExpire(Date expire) {
 	 * Whether to use aggressive mode or not. If set to true JGit behaves more
 	 * similar to native git's "git gc --aggressive". If set to
 	 * <code>true</code> compressed objects found in old packs are not reused
-	 * but every object is compressed again. Configuration variables
-	 * pack.window and pack.depth are set to 250 for this GC.
+	 * but every object is compressed again. Configuration variables pack.window
+	 * and pack.depth are set to 250 for this GC.
 	 *
 	 * @since 3.6
 	 * @param aggressive
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 3dc53ec..5bc035a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -714,6 +714,16 @@ public GarbageCollectCommand gc() {
 	}
 
 	/**
+	 * Return a command object to execute a {@code PackRefs} command
+	 *
+	 * @return a {@link org.eclipse.jgit.api.PackRefsCommand}
+	 * @since 7.1
+	 */
+	public PackRefsCommand packRefs() {
+		return new PackRefsCommand(repo);
+	}
+
+	/**
 	 * Return a command object to find human-readable names of revisions.
 	 *
 	 * @return a {@link org.eclipse.jgit.api.NameRevCommand}.
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 240290f..1da71aa 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/InitCommand.java
@@ -19,6 +19,7 @@
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
@@ -44,6 +45,8 @@ public class InitCommand implements Callable<Git> {
 
 	private String initialBranch;
 
+	private boolean relativePaths;
+
 	/**
 	 * {@inheritDoc}
 	 * <p>
@@ -100,7 +103,11 @@ public Git call() throws GitAPIException {
 					: initialBranch);
 			Repository repository = builder.build();
 			if (!repository.getObjectDatabase().exists())
-				repository.create(bare);
+				if (repository instanceof FileRepository) {
+					((FileRepository) repository).create(bare, relativePaths);
+				} else {
+					repository.create(bare);
+				}
 			return new Git(repository, true);
 		} catch (IOException | ConfigInvalidException e) {
 			throw new JGitInternalException(e.getMessage(), e);
@@ -214,4 +221,18 @@ public InitCommand setInitialBranch(String branch)
 		this.initialBranch = branch;
 		return this;
 	}
+
+	/**
+	 * * Set whether the repository shall use relative paths for GIT_DIR and
+	 * GIT_WORK_TREE
+	 *
+	 * @param relativePaths
+	 *            if true, use relative paths for GIT_DIR and GIT_WORK_TREE
+	 * @return {@code this}
+	 * @since 7.2
+	 */
+	public InitCommand setRelativeDirs(boolean relativePaths) {
+		this.relativePaths = relativePaths;
+		return this;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java
new file mode 100644
index 0000000..29a69c5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2024 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
+ * 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 org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Optimize storage of references.
+ *
+ * @since 7.1
+ */
+public class PackRefsCommand extends GitCommand<String> {
+	private ProgressMonitor monitor;
+
+	private boolean all;
+
+	/**
+	 * Creates a new {@link PackRefsCommand} instance with default values.
+	 *
+	 * @param repo
+	 * 		the repository this command will be used on
+	 */
+	public PackRefsCommand(Repository repo) {
+		super(repo);
+		this.monitor = NullProgressMonitor.INSTANCE;
+	}
+
+	/**
+	 * Set progress monitor
+	 *
+	 * @param monitor
+	 * 		a progress monitor
+	 * @return this instance
+	 */
+	public PackRefsCommand setProgressMonitor(ProgressMonitor monitor) {
+		this.monitor = monitor;
+		return this;
+	}
+
+	/**
+	 * Specify whether to pack all the references.
+	 *
+	 * @param all
+	 * 		if <code>true</code> all the loose refs will be packed
+	 * @return this instance
+	 */
+	public PackRefsCommand setAll(boolean all) {
+		this.all = all;
+		return this;
+	}
+
+	/**
+	 * Whether to pack all the references
+	 *
+	 * @return whether to pack all the references
+	 */
+	public boolean isAll() {
+		return all;
+	}
+
+	@Override
+	public String call() throws GitAPIException {
+		checkCallable();
+		try {
+			repo.getRefDatabase().packRefs(monitor, this);
+			return JGitText.get().packRefsSuccessful;
+		} catch (IOException e) {
+			throw new JGitInternalException(JGitText.get().packRefsFailed, e);
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
index 83ae0fc..4b2cee4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
@@ -533,9 +533,9 @@ public static BranchRebaseMode getRebaseMode(String branchName,
 			Config config) {
 		BranchRebaseMode mode = config.getEnum(BranchRebaseMode.values(),
 				ConfigConstants.CONFIG_BRANCH_SECTION,
-				branchName, ConfigConstants.CONFIG_KEY_REBASE, null);
+				branchName, ConfigConstants.CONFIG_KEY_REBASE);
 		if (mode == null) {
-			mode = config.getEnum(BranchRebaseMode.values(),
+			mode = config.getEnum(
 					ConfigConstants.CONFIG_PULL_SECTION, null,
 					ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE);
 		}
@@ -549,7 +549,7 @@ private FastForwardMode getFastForwardMode() {
 		Config config = repo.getConfig();
 		Merge ffMode = config.getEnum(Merge.values(),
 				ConfigConstants.CONFIG_PULL_SECTION, null,
-				ConfigConstants.CONFIG_KEY_FF, null);
+				ConfigConstants.CONFIG_KEY_FF);
 		return ffMode != null ? FastForwardMode.valueOf(ffMode) : null;
 	}
 }
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 858bd96..3ae7a6c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -18,6 +18,8 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -1835,23 +1837,26 @@ PersonIdent parseAuthor(byte[] raw) {
 
 		// the time is saved as <seconds since 1970> <timezone offset>
 		int timeStart = 0;
-		if (time.startsWith("@")) //$NON-NLS-1$
+		if (time.startsWith("@")) { //$NON-NLS-1$
 			timeStart = 1;
-		else
+		} else {
 			timeStart = 0;
-		long when = Long
-				.parseLong(time.substring(timeStart, time.indexOf(' '))) * 1000;
+		}
+		Instant when = Instant.ofEpochSecond(
+				Long.parseLong(time.substring(timeStart, time.indexOf(' '))));
 		String tzOffsetString = time.substring(time.indexOf(' ') + 1);
 		int multiplier = -1;
-		if (tzOffsetString.charAt(0) == '+')
+		if (tzOffsetString.charAt(0) == '+') {
 			multiplier = 1;
+		}
 		int hours = Integer.parseInt(tzOffsetString.substring(1, 3));
 		int minutes = Integer.parseInt(tzOffsetString.substring(3, 5));
 		// this is in format (+/-)HHMM (hours and minutes)
-		// we need to convert into minutes
-		int tz = (hours * 60 + minutes) * multiplier;
-		if (name != null && email != null)
+		ZoneOffset tz = ZoneOffset.ofHoursMinutes(hours * multiplier,
+				minutes * multiplier);
+		if (name != null && email != null) {
 			return new PersonIdent(name, email, when, tz);
+		}
 		return null;
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java
index dead274..a149649 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java
@@ -68,7 +68,7 @@ public Collection<ReflogEntry> call() throws GitAPIException,
 		checkCallable();
 
 		try {
-			ReflogReader reader = repo.getReflogReader(ref);
+			ReflogReader reader = repo.getRefDatabase().getReflogReader(ref);
 			if (reader == null)
 				throw new RefNotFoundException(MessageFormat.format(
 						JGitText.get().refNotResolved, ref));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteRemoveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteRemoveCommand.java
index 553fc2e..ad553f0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteRemoveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteRemoveCommand.java
@@ -49,18 +49,6 @@ protected RemoteRemoveCommand(Repository repo) {
 	/**
 	 * The name of the remote to remove.
 	 *
-	 * @param name
-	 *            a remote name
-	 * @deprecated use {@link #setRemoteName} instead
-	 */
-	@Deprecated
-	public void setName(String name) {
-		this.remoteName = name;
-	}
-
-	/**
-	 * The name of the remote to remove.
-	 *
 	 * @param remoteName
 	 *            a remote name
 	 * @return {@code this}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteSetUrlCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteSetUrlCommand.java
index e3d0186..68ddce3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteSetUrlCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RemoteSetUrlCommand.java
@@ -71,18 +71,6 @@ protected RemoteSetUrlCommand(Repository repo) {
 	/**
 	 * The name of the remote to change the URL for.
 	 *
-	 * @param name
-	 *            a remote name
-	 * @deprecated use {@link #setRemoteName} instead
-	 */
-	@Deprecated
-	public void setName(String name) {
-		this.remoteName = name;
-	}
-
-	/**
-	 * The name of the remote to change the URL for.
-	 *
 	 * @param remoteName
 	 *            a remote remoteName
 	 * @return {@code this}
@@ -96,18 +84,6 @@ public RemoteSetUrlCommand setRemoteName(String remoteName) {
 	/**
 	 * The new URL for the remote.
 	 *
-	 * @param uri
-	 *            an URL for the remote
-	 * @deprecated use {@link #setRemoteUri} instead
-	 */
-	@Deprecated
-	public void setUri(URIish uri) {
-		this.remoteUri = uri;
-	}
-
-	/**
-	 * The new URL for the remote.
-	 *
 	 * @param remoteUri
 	 *            an URL for the remote
 	 * @return {@code this}
@@ -121,23 +97,6 @@ public RemoteSetUrlCommand setRemoteUri(URIish remoteUri) {
 	/**
 	 * Whether to change the push URL of the remote instead of the fetch URL.
 	 *
-	 * @param push
-	 *            <code>true</code> to set the push url, <code>false</code> to
-	 *            set the fetch url
-	 * @deprecated use {@link #setUriType} instead
-	 */
-	@Deprecated
-	public void setPush(boolean push) {
-		if (push) {
-			setUriType(UriType.PUSH);
-		} else {
-			setUriType(UriType.FETCH);
-		}
-	}
-
-	/**
-	 * Whether to change the push URL of the remote instead of the fetch URL.
-	 *
 	 * @param type
 	 *            the <code>UriType</code> value to set
 	 * @return {@code this}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
index 855c3b1..6643c83 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2010, 2024 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
@@ -143,8 +143,8 @@ public RevCommit call() throws NoMessageException, UnmergedPathsException,
 				merger.setCommitNames(new String[] {
 						"BASE", ourName, revertName }); //$NON-NLS-1$
 
-				String shortMessage = "Revert \"" + srcCommit.getShortMessage() //$NON-NLS-1$
-						+ "\""; //$NON-NLS-1$
+				String shortMessage = "Revert \"" //$NON-NLS-1$
+						+ srcCommit.getFirstMessageLine() + '"';
 				String newMessage = shortMessage + "\n\n" //$NON-NLS-1$
 						+ "This reverts commit " + srcCommit.getId().getName() //$NON-NLS-1$
 						+ ".\n"; //$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
index e415728..b0b715e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java
@@ -263,18 +263,6 @@ public ObjectId call() throws GitAPIException,
 	/**
 	 * Whether to restore the index state
 	 *
-	 * @param applyIndex
-	 *            true (default) if the command should restore the index state
-	 * @deprecated use {@link #setRestoreIndex} instead
-	 */
-	@Deprecated
-	public void setApplyIndex(boolean applyIndex) {
-		this.restoreIndex = applyIndex;
-	}
-
-	/**
-	 * Whether to restore the index state
-	 *
 	 * @param restoreIndex
 	 *            true (default) if the command should restore the index state
 	 * @return {@code this}
@@ -319,19 +307,6 @@ public StashApplyCommand setContentMergeStrategy(
 	/**
 	 * Whether the command should restore untracked files
 	 *
-	 * @param applyUntracked
-	 *            true (default) if the command should restore untracked files
-	 * @since 3.4
-	 * @deprecated use {@link #setRestoreUntracked} instead
-	 */
-	@Deprecated
-	public void setApplyUntracked(boolean applyUntracked) {
-		this.restoreUntracked = applyUntracked;
-	}
-
-	/**
-	 * Whether the command should restore untracked files
-	 *
 	 * @param restoreUntracked
 	 *            true (default) if the command should restore untracked files
 	 * @return {@code this}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java
index 23fbe01..2dba0ef 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java
@@ -165,7 +165,8 @@ public ObjectId call() throws GitAPIException {
 
 		List<ReflogEntry> entries;
 		try {
-			ReflogReader reader = repo.getReflogReader(R_STASH);
+			ReflogReader reader = repo.getRefDatabase()
+					.getReflogReader(R_STASH);
 			if (reader == null) {
 				throw new RefNotFoundException(MessageFormat
 						.format(JGitText.get().refNotResolved, stashRef));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java
index 8fb5d60..5105dfc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java
@@ -176,8 +176,9 @@ public Repository call() throws GitAPIException {
 		CloneCommand clone = Git.cloneRepository();
 		configure(clone);
 		clone.setDirectory(moduleDirectory);
-		clone.setGitDir(new File(new File(repo.getDirectory(),
-				Constants.MODULES), path));
+		clone.setGitDir(new File(
+				new File(repo.getCommonDirectory(), Constants.MODULES), path));
+		clone.setRelativePaths(true);
 		clone.setURI(resolvedUri);
 		if (monitor != null)
 			clone.setProgressMonitor(monitor);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
index df73164..5e4b2ee 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
@@ -28,6 +28,7 @@
 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
 import org.eclipse.jgit.dircache.DirCacheCheckout;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -39,6 +40,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.submodule.SubmoduleWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.util.FileUtils;
 
 /**
  * A class used to execute a submodule update command.
@@ -62,6 +64,8 @@ public class SubmoduleUpdateCommand extends
 
 	private boolean fetch = false;
 
+	private boolean clonedRestored;
+
 	/**
 	 * <p>
 	 * Constructor for SubmoduleUpdateCommand.
@@ -116,25 +120,77 @@ public SubmoduleUpdateCommand addPath(String path) {
 		return this;
 	}
 
+	private static boolean submoduleExists(File gitDir) {
+		if (gitDir != null && gitDir.isDirectory()) {
+			File[] files = gitDir.listFiles();
+			return files != null && files.length != 0;
+		}
+		return false;
+	}
+
+	private static void restoreSubmodule(File gitDir, File workingTree)
+			throws IOException {
+		LockFile dotGitLock = new LockFile(
+				new File(workingTree, Constants.DOT_GIT));
+		if (dotGitLock.lock()) {
+			String content = Constants.GITDIR
+					+ getRelativePath(gitDir, workingTree);
+			dotGitLock.write(Constants.encode(content));
+			dotGitLock.commit();
+		}
+	}
+
+	private static String getRelativePath(File gitDir, File workingTree) {
+		File relPath;
+		try {
+			relPath = workingTree.toPath().relativize(gitDir.toPath())
+					.toFile();
+		} catch (IllegalArgumentException e) {
+			relPath = gitDir;
+		}
+		return FileUtils.pathToString(relPath);
+	}
+
+	private String determineUpdateMode(String mode) {
+		if (clonedRestored) {
+			return ConfigConstants.CONFIG_KEY_CHECKOUT;
+		}
+		return mode;
+	}
+
 	private Repository getOrCloneSubmodule(SubmoduleWalk generator, String url)
 			throws IOException, GitAPIException {
 		Repository repository = generator.getRepository();
+		boolean restored = false;
+		boolean cloned = false;
 		if (repository == null) {
-			if (callback != null) {
-				callback.cloningSubmodule(generator.getPath());
+			File gitDir = new File(
+					new File(repo.getCommonDirectory(), Constants.MODULES),
+					generator.getPath());
+			if (submoduleExists(gitDir)) {
+				restoreSubmodule(gitDir, generator.getDirectory());
+				restored = true;
+				clonedRestored = true;
+				repository = generator.getRepository();
+			} else {
+				if (callback != null) {
+					callback.cloningSubmodule(generator.getPath());
+				}
+				CloneCommand clone = Git.cloneRepository();
+				configure(clone);
+				clone.setURI(url);
+				clone.setDirectory(generator.getDirectory());
+				clone.setGitDir(gitDir);
+				clone.setRelativePaths(true);
+				if (monitor != null) {
+					clone.setProgressMonitor(monitor);
+				}
+				repository = clone.call().getRepository();
+				cloned = true;
+				clonedRestored = true;
 			}
-			CloneCommand clone = Git.cloneRepository();
-			configure(clone);
-			clone.setURI(url);
-			clone.setDirectory(generator.getDirectory());
-			clone.setGitDir(
-					new File(new File(repo.getDirectory(), Constants.MODULES),
-							generator.getPath()));
-			if (monitor != null) {
-				clone.setProgressMonitor(monitor);
-			}
-			repository = clone.call().getRepository();
-		} else if (this.fetch) {
+		}
+		if ((this.fetch || restored) && !cloned) {
 			if (fetchCallback != null) {
 				fetchCallback.fetchingSubmodule(generator.getPath());
 			}
@@ -171,15 +227,17 @@ public Collection<String> call() throws InvalidConfigurationException,
 					continue;
 				// Skip submodules not registered in parent repository's config
 				String url = generator.getConfigUrl();
-				if (url == null)
+				if (url == null) {
 					continue;
-
+				}
+				clonedRestored = false;
 				try (Repository submoduleRepo = getOrCloneSubmodule(generator,
 						url); RevWalk walk = new RevWalk(submoduleRepo)) {
 					RevCommit commit = walk
 							.parseCommit(generator.getObjectId());
 
-					String update = generator.getConfigUpdate();
+					String update = determineUpdateMode(
+							generator.getConfigUpdate());
 					if (ConfigConstants.CONFIG_KEY_MERGE.equals(update)) {
 						MergeCommand merge = new MergeCommand(submoduleRepo);
 						merge.include(commit);
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 3edaf5e..cc8589f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java
@@ -18,14 +18,11 @@
 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;
@@ -33,6 +30,8 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.lib.Signers;
 import org.eclipse.jgit.lib.TagBuilder;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -79,7 +78,7 @@ public class TagCommand extends GitCommand<Ref> {
 
 	private GpgConfig gpgConfig;
 
-	private GpgObjectSigner gpgSigner;
+	private Signer signer;
 
 	private CredentialsProvider credentialsProvider;
 
@@ -133,9 +132,9 @@ public Ref call() throws GitAPIException, ConcurrentRefUpdateException,
 			newTag.setTagger(tagger);
 			newTag.setObjectId(id);
 
-			if (gpgSigner != null) {
-				gpgSigner.signObject(newTag, signingKey, tagger,
-						credentialsProvider, gpgConfig);
+			if (signer != null) {
+				signer.signObject(repo, gpgConfig, newTag, tagger, signingKey,
+						credentialsProvider);
 			}
 
 			// write the tag object
@@ -196,15 +195,12 @@ private Ref updateTagRef(ObjectId tagId, RevWalk revWalk,
 	 *
 	 * @throws InvalidTagNameException
 	 *             if the tag name is null or invalid
-	 * @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()
-			throws InvalidTagNameException, ServiceUnavailableException,
-			UnsupportedSigningFormatException {
+			throws InvalidTagNameException, UnsupportedSigningFormatException {
 		if (name == null
 				|| !Repository.isValidRefName(Constants.R_TAGS + name)) {
 			throw new InvalidTagNameException(
@@ -230,16 +226,15 @@ private void processOptions()
 					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);
+					if (signer == null) {
+						signer = Signers.get(gpgConfig.getKeyFormat());
+						if (signer == null) {
+							throw new UnsupportedSigningFormatException(
+									MessageFormat.format(
+											JGitText.get().signatureTypeUnknown,
+											gpgConfig.getKeyFormat()
+													.toConfigValue()));
 						}
-						gpgSigner = (GpgObjectSigner) signer;
 					}
 					// The message of a signed tag must end in a newline because
 					// the signature will be appended.
@@ -326,22 +321,22 @@ public TagCommand setSigned(boolean signed) {
 	}
 
 	/**
-	 * Sets the {@link GpgSigner} to use if the commit is to be signed.
+	 * Sets the {@link Signer} 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
+	 * @since 7.0
 	 */
-	public TagCommand setGpgSigner(GpgObjectSigner signer) {
+	public TagCommand setSigner(Signer signer) {
 		checkCallable();
-		this.gpgSigner = signer;
+		this.signer = signer;
 		return this;
 	}
 
 	/**
 	 * Sets an external {@link GpgConfig} to use. Whether it will be used is at
-	 * the discretion of the {@link #setGpgSigner(GpgObjectSigner)}.
+	 * the discretion of the {@link #setSigner(Signer)}.
 	 *
 	 * @param config
 	 *            to set; if {@code null}, the config will be loaded from the
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java
index 21cddf7..f5f4b06 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerificationResult.java
@@ -9,7 +9,7 @@
  */
 package org.eclipse.jgit.api;
 
-import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.SignatureVerifier;
 import org.eclipse.jgit.revwalk.RevObject;
 
 /**
@@ -34,8 +34,9 @@ public interface VerificationResult {
 	 * Retrieves the signature verification result.
 	 *
 	 * @return the result, or {@code null} if none was computed
+	 * @since 7.0
 	 */
-	GpgSignatureVerifier.SignatureVerification getVerification();
+	SignatureVerifier.SignatureVerification getVerification();
 
 	/**
 	 * Retrieves the git object of which the signature was verified.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java
index 6a2a44e..487ff04 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/VerifySignatureCommand.java
@@ -25,11 +25,10 @@
 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.lib.SignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.SignatureVerifiers;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 
@@ -65,12 +64,8 @@ public enum VerifyMode {
 
 	private VerifyMode mode = VerifyMode.ANY;
 
-	private GpgSignatureVerifier verifier;
-
 	private GpgConfig config;
 
-	private boolean ownVerifier;
-
 	/**
 	 * Creates a new {@link VerifySignatureCommand} for the given {@link Repository}.
 	 *
@@ -140,22 +135,7 @@ public VerifySignatureCommand setMode(@NonNull VerifyMode mode) {
 	}
 
 	/**
-	 * 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)}.
+	 * Sets an external {@link GpgConfig} to use.
 	 *
 	 * @param config
 	 *            to set; if {@code null}, the config will be loaded from the
@@ -170,16 +150,6 @@ public VerifySignatureCommand setGpgConfig(GpgConfig config) {
 	}
 
 	/**
-	 * 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.
@@ -193,9 +163,6 @@ public GpgSignatureVerifier getVerifier() {
 	 *
 	 * @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
@@ -207,16 +174,6 @@ public Map<String, VerificationResult> call()
 		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());
 		}
@@ -239,10 +196,6 @@ public Map<String, VerificationResult> call()
 		} catch (IOException e) {
 			throw new JGitInternalException(
 					JGitText.get().signatureVerificationError, e);
-		} finally {
-			if (ownVerifier) {
-				verifier.clear();
-			}
 		}
 		return result;
 	}
@@ -258,8 +211,8 @@ private VerificationResult verifyOne(RevObject object)
 		}
 		if (type == Constants.OBJ_COMMIT || type == Constants.OBJ_TAG) {
 			try {
-				GpgSignatureVerifier.SignatureVerification verification = verifier
-						.verifySignature(object, config);
+				SignatureVerification verification = SignatureVerifiers
+						.verify(repo, config, object);
 				if (verification == null) {
 					// Not signed
 					return null;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java
index fe3e22a..9c4d870 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java
@@ -9,6 +9,8 @@
  */
 package org.eclipse.jgit.attributes;
 
+import org.eclipse.jgit.annotations.Nullable;
+
 /**
  * Represents an attribute.
  * <p>
@@ -139,6 +141,7 @@ public State getState() {
 	 *
 	 * @return the attribute value (may be <code>null</code>)
 	 */
+	@Nullable
 	public String getValue() {
 		return value;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java
index 77967df..2d499ca 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java
@@ -28,6 +28,8 @@
 import org.eclipse.jgit.blame.Candidate.HeadCandidate;
 import org.eclipse.jgit.blame.Candidate.ReverseCandidate;
 import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit;
+import org.eclipse.jgit.blame.cache.BlameCache;
+import org.eclipse.jgit.blame.cache.CacheRegion;
 import org.eclipse.jgit.diff.DiffAlgorithm;
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
@@ -129,8 +131,19 @@ public class BlameGenerator implements AutoCloseable {
 
 	/** Blame is currently assigned to this source. */
 	private Candidate outCandidate;
+
 	private Region outRegion;
 
+	private final BlameCache blameCache;
+
+	/**
+	 * Blame in reverse order needs the source lines, but we don't have them in
+	 * the cache. We need to ignore the cache in that case.
+	 */
+	private boolean useCache = true;
+
+	private final Stats stats = new Stats();
+
 	/**
 	 * Create a blame generator for the repository and path (relative to
 	 * repository)
@@ -142,6 +155,25 @@ public class BlameGenerator implements AutoCloseable {
 	 *            repository).
 	 */
 	public BlameGenerator(Repository repository, String path) {
+		this(repository, path, null);
+	}
+
+	/**
+	 * Create a blame generator for the repository and path (relative to
+	 * repository)
+	 *
+	 * @param repository
+	 *            repository to access revision data from.
+	 * @param path
+	 *            initial path of the file to start scanning (relative to the
+	 *            repository).
+	 * @param blameCache
+	 *            previously calculated blames. This generator will *not*
+	 *            populate it, just consume it.
+	 * @since 7.2
+	 */
+	public BlameGenerator(Repository repository, String path,
+			@Nullable BlameCache blameCache) {
 		this.repository = repository;
 		this.resultPath = PathFilter.create(path);
 
@@ -150,6 +182,7 @@ public BlameGenerator(Repository repository, String path) {
 		initRevPool(false);
 
 		remaining = -1;
+		this.blameCache = blameCache;
 	}
 
 	private void initRevPool(boolean reverse) {
@@ -159,10 +192,12 @@ private void initRevPool(boolean reverse) {
 		if (revPool != null)
 			revPool.close();
 
-		if (reverse)
+		if (reverse) {
+			useCache = false;
 			revPool = new ReverseWalk(getRepository());
-		else
+		} else {
 			revPool = new RevWalk(getRepository());
+		}
 
 		SEEN = revPool.newFlag("SEEN"); //$NON-NLS-1$
 		reader = revPool.getObjectReader();
@@ -245,6 +280,31 @@ public RenameDetector getRenameDetector() {
 	}
 
 	/**
+	 * Stats about this generator
+	 *
+	 * @return the stats of this generator
+	 * @since 7.2
+	 */
+	public Stats getStats() {
+		return stats;
+	}
+
+	/**
+	 * Enable/disable the use of cache (if present). Enabled by default.
+	 * <p>
+	 * If caller need source line numbers, the generator cannot use the cache
+	 * (source lines are not there). Use this method to disable the cache in
+	 * that case.
+	 *
+	 * @param useCache
+	 *            should this generator use the cache.
+	 * @since 7.2
+	 */
+	public void setUseCache(boolean useCache) {
+		this.useCache = useCache;
+	}
+
+	/**
 	 * Push a candidate blob onto the generator's traversal stack.
 	 * <p>
 	 * Candidates should be pushed in history order from oldest-to-newest.
@@ -591,6 +651,20 @@ public boolean next() throws IOException {
 			Candidate n = pop();
 			if (n == null)
 				return done();
+			stats.candidatesVisited += 1;
+			if (blameCache != null && useCache) {
+				List<CacheRegion> cachedBlame = blameCache.get(repository,
+						n.sourceCommit, n.sourcePath.getPath());
+				if (cachedBlame != null) {
+					BlameRegionMerger rb = new BlameRegionMerger(repository,
+							revPool, cachedBlame);
+					Candidate fullyBlamed = rb.mergeCandidate(n);
+					if (fullyBlamed != null) {
+						stats.cacheHit = true;
+						return result(fullyBlamed);
+					}
+				}
+			}
 
 			int pCnt = n.getParentCount();
 			if (pCnt == 1) {
@@ -605,7 +679,7 @@ public boolean next() throws IOException {
 				// Do not generate a tip of a reverse. The region
 				// survives and should not appear to be deleted.
 
-			} else /* if (pCnt == 0) */{
+			} else /* if (pCnt == 0) */ {
 				// Root commit, with at least one surviving region.
 				// Assign the remaining blame here.
 				return result(n);
@@ -846,8 +920,8 @@ private boolean processMerge(Candidate n) throws IOException {
 				editList = new EditList(0);
 			} else {
 				p.loadText(reader);
-				editList = diffAlgorithm.diff(textComparator,
-						p.sourceText, n.sourceText);
+				editList = diffAlgorithm.diff(textComparator, p.sourceText,
+						n.sourceText);
 			}
 
 			if (editList.isEmpty()) {
@@ -981,6 +1055,10 @@ public int getRenameScore() {
 	/**
 	 * Get first line of the source data that has been blamed for the current
 	 * region
+	 * <p>
+	 * This value is not reliable when the generator is reusing cached values.
+	 * Cache doesn't keep the source lines, the returned value is based on the
+	 * result and can be off if the region moved in previous commits.
 	 *
 	 * @return first line of the source data that has been blamed for the
 	 *         current region. This is line number of where the region was added
@@ -994,6 +1072,10 @@ public int getSourceStart() {
 	/**
 	 * Get one past the range of the source data that has been blamed for the
 	 * current region
+	 * <p>
+	 * This value is not reliable when the generator is reusing cached values.
+	 * Cache doesn't keep the source lines, the returned value is based on the
+	 * result and can be off if the region moved in previous commits.
 	 *
 	 * @return one past the range of the source data that has been blamed for
 	 *         the current region. This is line number of where the region was
@@ -1124,4 +1206,39 @@ private static boolean isRename(DiffEntry ent) {
 		return ent.getChangeType() == ChangeType.RENAME
 				|| ent.getChangeType() == ChangeType.COPY;
 	}
+
+	/**
+	 * Stats about the work done by the generator
+	 *
+	 * @since 7.2
+	 */
+	public static class Stats {
+
+		/** Candidates taken from the queue */
+		private int candidatesVisited;
+
+		private boolean cacheHit;
+
+		/**
+		 * Number of candidates taken from the queue
+		 * <p>
+		 * The generator could signal it's done without exhausting all
+		 * candidates if there is no more remaining lines or the last visited
+		 * candidate is found in the cache.
+		 *
+		 * @return number of candidates taken from the queue
+		 */
+		public int getCandidatesVisited() {
+			return candidatesVisited;
+		}
+
+		/**
+		 * The generator found a blamed version in the cache
+		 *
+		 * @return true if we used results from the cache
+		 */
+		public boolean isCacheHit() {
+			return cacheHit;
+		}
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameRegionMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameRegionMerger.java
new file mode 100644
index 0000000..67bc6fb
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameRegionMerger.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * 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.blame;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jgit.blame.cache.CacheRegion;
+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.eclipse.jgit.treewalk.filter.PathFilter;
+
+/**
+ * Translates an unblamed region into one or more blamed regions, using the
+ * fully blamed data from cache.
+ * <p>
+ * Blamed and unblamed regions are not symmetrical: An unblamed region is just a
+ * range of lines over the file. A blamed region is a Candidate (with the commit
+ * info) with a region inside (the range blamed).
+ */
+class BlameRegionMerger {
+	private final Repository repo;
+
+	private final List<CacheRegion> cachedRegions;
+
+	private final RevWalk rw;
+
+	BlameRegionMerger(Repository repo, RevWalk rw,
+			List<CacheRegion> cachedRegions) {
+		this.repo = repo;
+		List<CacheRegion> sorted = new ArrayList<>(cachedRegions);
+		Collections.sort(sorted);
+		this.cachedRegions = sorted;
+		this.rw = rw;
+	}
+
+	/**
+	 * Return one or more candidates blaming all the regions of the "unblamed"
+	 * incoming candidate.
+	 *
+	 * @param candidate
+	 *            a candidate with a list of unblamed regions
+	 * @return A linked list of Candidates with their blamed regions, null if
+	 *         there was any error.
+	 */
+	Candidate mergeCandidate(Candidate candidate) {
+		List<Candidate> newCandidates = new ArrayList<>();
+		Region r = candidate.regionList;
+		while (r != null) {
+			try {
+				newCandidates.addAll(mergeOneRegion(r));
+			} catch (IOException e) {
+				return null;
+			}
+			r = r.next;
+		}
+		return asLinkedCandidate(newCandidates);
+	}
+
+	// Visible for testing
+	List<Candidate> mergeOneRegion(Region region) throws IOException {
+		List<CacheRegion> overlaps = findOverlaps(region);
+		if (overlaps.isEmpty()) {
+			throw new IOException(
+					"Cached blame should cover all lines");
+		}
+		/*
+		 * Cached regions cover the whole file. We find first which ones overlap
+		 * with our unblamed region. Then we take the overlapping portions with
+		 * the corresponding blame.
+		 */
+		List<Candidate> candidates = new ArrayList<>();
+		for (CacheRegion overlap : overlaps) {
+			Region blamedRegions = intersectRegions(region, overlap);
+			Candidate c = new Candidate(repo, parse(overlap.getSourceCommit()),
+					PathFilter.create(overlap.getSourcePath()));
+			c.regionList = blamedRegions;
+			candidates.add(c);
+		}
+		return candidates;
+	}
+
+	// Visible for testing
+	List<CacheRegion> findOverlaps(Region unblamed) {
+		int unblamedStart = unblamed.sourceStart;
+		int unblamedEnd = unblamedStart + unblamed.length;
+		List<CacheRegion> overlapping = new ArrayList<>();
+		for (CacheRegion blamed : cachedRegions) {
+			// End is not included
+			if (blamed.getEnd() <= unblamedStart) {
+				// Blamed region is completely before
+				continue;
+			}
+
+			if (blamed.getStart() >= unblamedEnd) {
+				// Blamed region is completely after
+				// Blamed regions are sorted by start position, nothing will
+				// match anymore
+				break;
+			}
+			overlapping.add(blamed);
+		}
+		return overlapping;
+	}
+
+	// Visible for testing
+	/**
+	 * Calculate the intersection between a Region and a CacheRegion, adjusting
+	 * the start if needed.
+	 * <p>
+	 * This should be called only if there is an overlap (filtering the cached
+	 * regions with {@link #findOverlaps(Region)}), otherwise the result is
+	 * meaningless.
+	 *
+	 * @param unblamed
+	 *            a region from the blame generator
+	 * @param cached
+	 *            a cached region
+	 * @return a new region with the intersection.
+	 */
+	static Region intersectRegions(Region unblamed, CacheRegion cached) {
+		int blamedStart = Math.max(cached.getStart(), unblamed.sourceStart);
+		int blamedEnd = Math.min(cached.getEnd(),
+				unblamed.sourceStart + unblamed.length);
+		int length = blamedEnd - blamedStart;
+
+		// result start and source start should move together
+		int blameStartDelta = blamedStart - unblamed.sourceStart;
+		return new Region(unblamed.resultStart + blameStartDelta, blamedStart,
+				length);
+	}
+
+	// Tests can override this, so they don't need a real repo, commit and walk
+	protected RevCommit parse(ObjectId oid) throws IOException {
+		return rw.parseCommit(oid);
+	}
+
+	private static Candidate asLinkedCandidate(List<Candidate> c) {
+		Candidate head = c.get(0);
+		Candidate tail = head;
+		for (int i = 1; i < c.size(); i++) {
+			tail.queueNext = c.get(i);
+			tail = tail.queueNext;
+		}
+		return head;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java
index 5e2746c..48f6b7e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java
@@ -79,6 +79,7 @@ public static BlameResult create(BlameGenerator gen) throws IOException {
 
 	BlameResult(BlameGenerator bg, String path, RawText text) {
 		generator = bg;
+		generator.setUseCache(false);
 		resultPath = path;
 		resultContents = text;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java
new file mode 100644
index 0000000..d44fb5f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * 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.blame.cache;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Keeps the blame information for a path at certain commit.
+ * <p>
+ * If there is a result, it covers the whole file at that revision
+ *
+ * @since 7.2
+ */
+public interface BlameCache {
+	/**
+	 * Gets the blame of a path at a given commit if available.
+	 * <p>
+	 * Since this cache is used in blame calculation, this get() method should
+	 * only retrieve the cache value, and not re-trigger blame calculation. In
+	 * other words, this acts as "getIfPresent", and not "computeIfAbsent".
+	 *
+	 * @param repo
+	 *            repository containing the commit
+	 * @param commitId
+	 *            we are looking at the file in this revision
+	 * @param path
+	 *            path a file in the repo
+	 *
+	 * @return the blame of a path at a given commit or null if not in cache
+	 * @throws IOException
+	 *             error retrieving/parsing values from storage
+	 */
+	List<CacheRegion> get(Repository repo, ObjectId commitId, String path)
+			throws IOException;
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java
new file mode 100644
index 0000000..cf3f978
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * 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.blame.cache;
+
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Region of the blame of a file.
+ * <p>
+ * Usually all parameters are non-null, except when the Region was created
+ * to fill an unblamed gap (to cover for bugs in the calculation). In that
+ * case, path, commit and author will be null.
+ *
+ * @since 7.2
+ **/
+public class CacheRegion implements Comparable<CacheRegion> {
+	private final String sourcePath;
+
+	private final ObjectId sourceCommit;
+
+	private final int end;
+
+	private final int start;
+
+	/**
+	 * A blamed portion of a file
+	 *
+	 * @param path
+	 *            location of the file
+	 * @param commit
+	 *            commit that is modifying this region
+	 * @param start
+	 *            first line of this region (inclusive)
+	 * @param end
+	 *            last line of this region (non-inclusive!)
+	 */
+	public CacheRegion(String path, ObjectId commit,
+			int start, int end) {
+		allOrNoneNull(path, commit);
+		this.sourcePath = path;
+		this.sourceCommit = commit;
+		this.start = start;
+		this.end = end;
+	}
+
+	/**
+	 * First line of this region. Starting by 0, inclusive
+	 *
+	 * @return first line of this region.
+	 */
+	public int getStart() {
+		return start;
+	}
+
+	/**
+	 * One after last line in this region (or: last line non-inclusive)
+	 *
+	 * @return one after last line in this region.
+	 */
+	public int getEnd() {
+		return end;
+	}
+
+
+	/**
+	 * Path of the file this region belongs to
+	 *
+	 * @return path in the repo/commit
+	 */
+	public String getSourcePath() {
+		return sourcePath;
+	}
+
+	/**
+	 * Commit this region belongs to
+	 *
+	 * @return commit for this region
+	 */
+	public ObjectId getSourceCommit() {
+		return sourceCommit;
+	}
+
+	@Override
+	public int compareTo(CacheRegion o) {
+		return start - o.start;
+	}
+
+	@SuppressWarnings("nls")
+	@Override
+	public String toString() {
+		StringBuilder sb = new StringBuilder();
+		if (sourceCommit != null) {
+			sb.append(sourceCommit.name(), 0, 7).append(' ')
+					.append(" (")
+					.append(sourcePath).append(')');
+		} else {
+			sb.append("<unblamed region>");
+		}
+		sb.append(' ').append("start=").append(start).append(", count=")
+				.append(end - start);
+		return sb.toString();
+	}
+
+	private static void allOrNoneNull(String path, ObjectId commit) {
+		if (path != null && commit != null) {
+			return;
+		}
+
+		if (path == null && commit == null) {
+			return;
+		}
+		throw new IllegalArgumentException(MessageFormat
+				.format(JGitText.get().cacheRegionAllOrNoneNull, path, commit));
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffDriver.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffDriver.java
new file mode 100644
index 0000000..b744444
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffDriver.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2024 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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.diff;
+
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * Built-in drivers for various languages, sorted by name. These drivers will be
+ * used to determine function names for a hunk.
+ * <p>
+ * When writing or updating patterns, assume the contents are syntactically
+ * correct. Patterns can be simple and need not cover all syntactical corner
+ * cases, as long as they are sufficiently permissive.
+ *
+ * @since 6.10.1
+ */
+@SuppressWarnings({"ImmutableEnumChecker", "nls"})
+public enum DiffDriver {
+	/**
+	 * Built-in diff driver for <a href=
+	 * "https://learn.microsoft.com/en-us/cpp/cpp/cpp-language-reference">c++</a>
+	 */
+	cpp(List.of(
+			/* Jump targets or access declarations */
+			"^[ \\t]*[A-Za-z_][A-Za-z_0-9]*:\\s*($|/[/*])"), List.of(
+			/* functions/methods, variables, and compounds at top level */
+			"^((::\\s*)?[A-Za-z_].*)$")),
+	/**
+	 * Built-in diff driver for <a href=
+	 * "https://devicetree-specification.readthedocs.io/en/stable/source-language.html">device
+	 * tree files</a>
+	 */
+	dts(List.of(";", "="), List.of(
+			/* lines beginning with a word optionally preceded by '&' or the root */
+			"^[ \\t]*((/[ \\t]*\\{|&?[a-zA-Z_]).*)")),
+	/**
+	 * Built-in diff driver for <a href=
+	 * "https://docs.oracle.com/javase/specs/jls/se21/html/index.html">java</a>
+	 */
+	java(List.of(
+			"^[ \\t]*(catch|do|for|if|instanceof|new|return|switch|throw|while)"),
+			List.of(
+					/* Class, enum, interface, and record declarations */
+					"^[ \\t]*(([a-z-]+[ \\t]+)*(class|enum|interface|record)[ \\t]+.*)$",
+					/* Method definitions; note that constructor signatures are not */
+					/* matched because they are indistinguishable from method calls. */
+					"^[ \\t]*(([A-Za-z_<>&\\]\\[][?&<>.,A-Za-z_0-9]*[ \\t]+)+[A-Za-z_]"
+							+ "[A-Za-z_0-9]*[ \\t]*\\([^;]*)$")),
+	/**
+	 * Built-in diff driver for
+	 * <a href="https://docs.python.org/3/reference/index.html">python</a>
+	 */
+	python(List.of("^[ \\t]*((class|(async[ \\t]+)?def)[ \\t].*)$")),
+	/**
+	 * Built-in diff driver for
+	 * <a href="https://doc.rust-lang.org/reference/introduction.html">rust</a>
+	 */
+	rust(List.of("^[\\t ]*((pub(\\([^\\)]+\\))?[\\t ]+)?"
+			+ "((async|const|unsafe|extern([\\t ]+\"[^\"]+\"))[\\t ]+)?"
+			+ "(struct|enum|union|mod|trait|fn|impl|macro_rules!)[< \\t]+[^;]*)$"));
+
+	private final List<Pattern> negatePatterns;
+
+	private final List<Pattern> matchPatterns;
+
+	DiffDriver(List<String> negate, List<String> match, int flags) {
+		if (negate != null) {
+			this.negatePatterns = negate.stream()
+					.map(r -> Pattern.compile(r, flags))
+					.collect(Collectors.toList());
+		} else {
+			this.negatePatterns = null;
+		}
+		this.matchPatterns = match.stream().map(r -> Pattern.compile(r, flags))
+				.collect(Collectors.toList());
+	}
+
+	DiffDriver(List<String> match) {
+		this(null, match, 0);
+	}
+
+	DiffDriver(List<String> negate, List<String> match) {
+		this(negate, match, 0);
+	}
+
+	/**
+	 * Returns the list of patterns used to exclude certain lines from being
+	 * considered as function names.
+	 *
+	 * @return the list of negate patterns
+	 */
+	public List<Pattern> getNegatePatterns() {
+		return negatePatterns;
+	}
+
+	/**
+	 * Returns the list of patterns used to match lines for potential function
+	 * names.
+	 *
+	 * @return the list of match patterns
+	 */
+	public List<Pattern> getMatchPatterns() {
+		return matchPatterns;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
index 2f472b5..cbac3f9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
@@ -30,7 +30,9 @@
 import java.util.Collections;
 import java.util.List;
 
+import java.util.regex.Pattern;
 import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.attributes.Attribute;
 import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.dircache.DirCacheIterator;
@@ -703,7 +705,7 @@ public void format(List<? extends DiffEntry> entries) throws IOException {
 	 */
 	public void format(DiffEntry ent) throws IOException {
 		FormatResult res = createFormatResult(ent);
-		format(res.header, res.a, res.b);
+		format(res.header, res.a, res.b, getDiffDriver(ent));
 	}
 
 	private static byte[] writeGitLinkText(AbbreviatedObjectId id) {
@@ -749,11 +751,14 @@ private String quotePath(String path) {
 	 *            text source for the post-image version of the content. This
 	 *            must match the content of
 	 *            {@link org.eclipse.jgit.patch.FileHeader#getNewId()}.
+	 * @param diffDriver
+	 *            the diff driver used to obtain function names in hunk headers
 	 * @throws java.io.IOException
-	 *             writing to the supplied stream failed.
+	 *            writing to the supplied stream failed.
+	 * @since 6.10.1
 	 */
-	public void format(FileHeader head, RawText a, RawText b)
-			throws IOException {
+	public void format(FileHeader head, RawText a, RawText b,
+			DiffDriver diffDriver) throws IOException {
 		// Reuse the existing FileHeader as-is by blindly copying its
 		// header lines, but avoiding its hunks. Instead we recreate
 		// the hunks from the text instances we have been supplied.
@@ -763,8 +768,49 @@ public void format(FileHeader head, RawText a, RawText b)
 		if (!head.getHunks().isEmpty())
 			end = head.getHunks().get(0).getStartOffset();
 		out.write(head.getBuffer(), start, end - start);
-		if (head.getPatchType() == PatchType.UNIFIED)
-			format(head.toEditList(), a, b);
+		if (head.getPatchType() == PatchType.UNIFIED) {
+			format(head.toEditList(), a, b, diffDriver);
+		}
+	}
+
+	/**
+	 * Format a patch script, reusing a previously parsed FileHeader.
+	 * <p>
+	 * This formatter is primarily useful for editing an existing patch script
+	 * to increase or reduce the number of lines of context within the script.
+	 * All header lines are reused as-is from the supplied FileHeader.
+	 *
+	 * @param head
+	 * 		existing file header containing the header lines to copy.
+	 * @param a
+	 * 		text source for the pre-image version of the content. This must match
+	 * 		the content of {@link org.eclipse.jgit.patch.FileHeader#getOldId()}.
+	 * @param b
+	 * 		text source for the post-image version of the content. This must match
+	 * 		the content of {@link org.eclipse.jgit.patch.FileHeader#getNewId()}.
+	 * @throws java.io.IOException
+	 * 		writing to the supplied stream failed.
+	 */
+	public void format(FileHeader head, RawText a, RawText b)
+			throws IOException {
+		format(head, a, b, null);
+	}
+
+	/**
+	 * Formats a list of edits in unified diff format
+	 *
+	 * @param edits
+	 * 		some differences which have been calculated between A and B
+	 * @param a
+	 * 		the text A which was compared
+	 * @param b
+	 * 		the text B which was compared
+	 * @throws java.io.IOException
+	 * 		if an IO error occurred
+	 */
+	public void format(EditList edits, RawText a, RawText b)
+			throws IOException {
+		format(edits, a, b, null);
 	}
 
 	/**
@@ -776,11 +822,14 @@ public void format(FileHeader head, RawText a, RawText b)
 	 *            the text A which was compared
 	 * @param b
 	 *            the text B which was compared
+	 * @param diffDriver
+	 *            the diff driver used to obtain function names in hunk headers
 	 * @throws java.io.IOException
 	 *             if an IO error occurred
+	 * @since 6.10.1
 	 */
-	public void format(EditList edits, RawText a, RawText b)
-			throws IOException {
+	public void format(EditList edits, RawText a, RawText b,
+			DiffDriver diffDriver) throws IOException {
 		for (int curIdx = 0; curIdx < edits.size();) {
 			Edit curEdit = edits.get(curIdx);
 			final int endIdx = findCombinedEnd(edits, curIdx);
@@ -791,7 +840,8 @@ public void format(EditList edits, RawText a, RawText b)
 			final int aEnd = (int) Math.min(a.size(), (long) endEdit.getEndA() + context);
 			final int bEnd = (int) Math.min(b.size(), (long) endEdit.getEndB() + context);
 
-			writeHunkHeader(aCur, aEnd, bCur, bEnd);
+			writeHunkHeader(aCur, aEnd, bCur, bEnd,
+					getFuncName(a, aCur - 1, diffDriver));
 
 			while (aCur < aEnd || bCur < bEnd) {
 				if (aCur < curEdit.getBeginA() || endIdx + 1 < curIdx) {
@@ -881,8 +931,30 @@ protected void writeRemovedLine(RawText text, int line)
 	 * @throws java.io.IOException
 	 *             if an IO error occurred
 	 */
-	protected void writeHunkHeader(int aStartLine, int aEndLine,
-			int bStartLine, int bEndLine) throws IOException {
+	protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine,
+			int bEndLine) throws IOException {
+		writeHunkHeader(aStartLine, aEndLine, bStartLine, bEndLine, null);
+	}
+
+	/**
+	 * Output a hunk header
+	 *
+	 * @param aStartLine
+	 *            within first source
+	 * @param aEndLine
+	 *            within first source
+	 * @param bStartLine
+	 *            within second source
+	 * @param bEndLine
+	 *            within second source
+	 * @param funcName
+	 *            function name of this hunk
+	 * @throws java.io.IOException
+	 *             if an IO error occurred
+	 * @since 6.10.1
+	 */
+	protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine,
+			int bEndLine, String funcName) throws IOException {
 		out.write('@');
 		out.write('@');
 		writeRange('-', aStartLine + 1, aEndLine - aStartLine);
@@ -890,6 +962,10 @@ protected void writeHunkHeader(int aStartLine, int aEndLine,
 		out.write(' ');
 		out.write('@');
 		out.write('@');
+		if (funcName != null) {
+			out.write(' ');
+			out.write(funcName.getBytes());
+		}
 		out.write('\n');
 	}
 
@@ -1247,4 +1323,50 @@ private boolean combineB(List<Edit> e, int i) {
 	private static boolean end(Edit edit, int a, int b) {
 		return edit.getEndA() <= a && edit.getEndB() <= b;
 	}
+
+	private String getFuncName(RawText text, int startAt,
+			DiffDriver diffDriver) {
+		if (diffDriver != null) {
+			while (startAt > 0) {
+				String line = text.getString(startAt);
+				startAt--;
+				if (matchesAny(diffDriver.getNegatePatterns(), line)) {
+					continue;
+				}
+				if (matchesAny(diffDriver.getMatchPatterns(), line)) {
+					String funcName = line.replaceAll("^[ \\t]+", ""); //$NON-NLS-1$//$NON-NLS-2$
+					return funcName.substring(0,
+							Math.min(funcName.length(), 80)).trim();
+				}
+			}
+		}
+		return null;
+	}
+
+	private boolean matchesAny(List<Pattern> patterns, String text) {
+		if (patterns != null) {
+			for (Pattern p : patterns) {
+				if (p.matcher(text).find()) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+
+	private DiffDriver getDiffDriver(DiffEntry entry) {
+		Attribute diffAttr = entry.getDiffAttribute();
+		if (diffAttr == null) {
+			return null;
+		}
+		String diffAttrValue = diffAttr.getValue();
+		if (diffAttrValue == null) {
+			return null;
+		}
+		try {
+			return DiffDriver.valueOf(diffAttrValue);
+		} catch (IllegalArgumentException e) {
+			return null;
+		}
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java
index 4343642..b401bbe 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/PatchIdDiffFormatter.java
@@ -44,8 +44,8 @@ public ObjectId getCalulatedPatchId() {
 	}
 
 	@Override
-	protected void writeHunkHeader(int aStartLine, int aEndLine,
-			int bStartLine, int bEndLine) throws IOException {
+	protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine,
+			int bEndLine, String funcName) throws IOException {
 		// The hunk header is not taken into account for patch id calculation
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java
index 76dc87e..fdfe533 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java
@@ -360,18 +360,22 @@ public static boolean isBinary(byte[] raw, int length, boolean complete) {
 			length = maxLength;
 			isComplete = false;
 		}
-		byte last = 'x'; // Just something inconspicuous.
-		for (int ptr = 0; ptr < length; ptr++) {
-			byte curr = raw[ptr];
-			if (isBinary(curr, last)) {
+
+		int ptr = -1;
+		byte current;
+		while (ptr < length - 2) {
+			current = raw[++ptr];
+			if (current == '\0' || (current == '\r' && raw[++ptr] != '\n')) {
 				return true;
 			}
-			last = curr;
 		}
-		if (isComplete) {
-			// Buffer contains everything...
-			return last == '\r'; // ... so this must be a lone CR
+
+		if (ptr == length - 2) {
+			// if '\r' be last, then if isComplete then return binary
+			current = raw[++ptr];
+			return current == '\0' || (current == '\r' && isComplete);
 		}
+
 		return false;
 	}
 
@@ -467,26 +471,30 @@ public static boolean isCrLfText(byte[] raw, int length) {
 	 */
 	public static boolean isCrLfText(byte[] raw, int length, boolean complete) {
 		boolean has_crlf = false;
-		byte last = 'x'; // Just something inconspicuous
-		for (int ptr = 0; ptr < length; ptr++) {
-			byte curr = raw[ptr];
-			if (isBinary(curr, last)) {
+
+		int ptr = -1;
+		byte current;
+		while (ptr < length - 2) {
+			current = raw[++ptr];
+			if (current == '\0') {
 				return false;
 			}
-			if (curr == '\n' && last == '\r') {
+			if (current == '\r') {
+				if (raw[++ptr] != '\n') {
+					return false;
+				}
 				has_crlf = true;
 			}
-			last = curr;
 		}
-		if (last == '\r') {
-			if (complete) {
-				// Lone CR: it's binary after all.
+
+		if (ptr == length - 2) {
+			// if '\r' be last, then if isComplete then return binary
+			current = raw[++ptr];
+			if (current == '\0' || (current == '\r' && complete)) {
 				return false;
 			}
-			// Tough call. If the next byte, which we don't have, would be a
-			// '\n', it'd be a CR-LF text, otherwise it'd be binary. Just decide
-			// based on what we already scanned; it wasn't binary until now.
 		}
+
 		return has_crlf;
 	}
 
@@ -578,4 +586,5 @@ public static RawText load(ObjectLoader ldr, int threshold)
 			return new RawText(data, RawParseUtils.lineMapOrBinary(data, 0, (int) sz));
 		}
 	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
index 5de7bac..fb98df7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
@@ -80,7 +80,7 @@ class SimilarityRenameDetector {
 	private long[] matrix;
 
 	/** Score a pair must exceed to be considered a rename. */
-	private int renameScore = 60;
+	private int renameScore = 50;
 
 	/**
 	 * File size threshold (in bytes) for detecting renames. Files larger
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
index accf732..de02aec 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/Checkout.java
@@ -217,10 +217,18 @@ public void checkout(DirCacheEntry entry, CheckoutMetadata metadata,
 			}
 		}
 		try {
-			if (recursiveDelete && Files.isDirectory(f.toPath(),
-					LinkOption.NOFOLLOW_LINKS)) {
+			boolean isDir = Files.isDirectory(f.toPath(),
+					LinkOption.NOFOLLOW_LINKS);
+			if (recursiveDelete && isDir) {
 				FileUtils.delete(f, FileUtils.RECURSIVE);
 			}
+			if (cache.getRepository().isWorkTreeCaseInsensitive() && !isDir) {
+				// We cannot rely on rename via Files.move() to work correctly
+				// if the target exists in a case variant. For instance with JDK
+				// 17 on Mac OS, the existing case-variant name is kept. On
+				// Windows 11 it would work and use the name given in 'f'.
+				FileUtils.delete(f, FileUtils.SKIP_MISSING);
+			}
 			FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE);
 			cachedParent.remove(f.getName());
 		} catch (IOException e) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
index 34dba0b..c650d6e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
@@ -1037,7 +1037,12 @@ private void updateSmudgedEntries() throws IOException {
 		}
 	}
 
-	enum DirCacheVersion implements ConfigEnum {
+	/**
+	 * DirCache versions
+	 *
+	 * @since 7.2
+	 */
+	public enum DirCacheVersion implements ConfigEnum {
 
 		/** Minimum index version on-disk format that we support. */
 		DIRC_VERSION_MINIMUM(2),
@@ -1060,6 +1065,9 @@ private DirCacheVersion(int versionCode) {
 			this.version = versionCode;
 		}
 
+		/**
+		 * @return the version code for this version
+		 */
 		public int getVersionCode() {
 			return version;
 		}
@@ -1078,6 +1086,13 @@ public boolean matchConfigValue(String in) {
 			}
 		}
 
+		/**
+		 * Create DirCacheVersion from integer value of the version code.
+		 *
+		 * @param val
+		 *            integer value of the version code.
+		 * @return the DirCacheVersion instance of the version code.
+		 */
 		public static DirCacheVersion fromInt(int val) {
 			for (DirCacheVersion v : DirCacheVersion.values()) {
 				if (val == v.getVersionCode()) {
@@ -1098,9 +1113,8 @@ public DirCacheConfig(Config cfg) {
 			boolean manyFiles = cfg.getBoolean(
 					ConfigConstants.CONFIG_FEATURE_SECTION,
 					ConfigConstants.CONFIG_KEY_MANYFILES, false);
-			indexVersion = cfg.getEnum(DirCacheVersion.values(),
-					ConfigConstants.CONFIG_INDEX_SECTION, null,
-					ConfigConstants.CONFIG_KEY_VERSION,
+			indexVersion = cfg.getEnum(ConfigConstants.CONFIG_INDEX_SECTION,
+					null, ConfigConstants.CONFIG_KEY_VERSION,
 					manyFiles ? DirCacheVersion.DIRC_VERSION_PATHCOMPRESS
 							: DirCacheVersion.DIRC_VERSION_EXTENDED);
 			skipHash = cfg.getBoolean(ConfigConstants.CONFIG_INDEX_SECTION,
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 6ae5153..18d7748 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -5,7 +5,7 @@
  * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org>
  * Copyright (C) 2010, Chrisian Halstrick <christian.halstrick@sap.com>
  * Copyright (C) 2019, 2020, Andre Bossert <andre.bossert@siemens.com>
- * Copyright (C) 2017, 2023, Thomas Wolf <twolf@apache.org> and others
+ * Copyright (C) 2017, 2025, Thomas Wolf <twolf@apache.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,6 +31,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.api.errors.FilterFailedException;
@@ -66,7 +67,6 @@
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FS.ExecutionResult;
-import org.eclipse.jgit.util.IntList;
 import org.eclipse.jgit.util.SystemReader;
 import org.eclipse.jgit.util.io.EolStreamTypeUtil;
 import org.slf4j.Logger;
@@ -113,9 +113,11 @@ public CheckoutMetadata(EolStreamType eolStreamType,
 
 	private Map<String, CheckoutMetadata> updated = new LinkedHashMap<>();
 
+	private Set<String> existing;
+
 	private ArrayList<String> conflicts = new ArrayList<>();
 
-	private ArrayList<String> removed = new ArrayList<>();
+	private TreeSet<String> removed;
 
 	private ArrayList<String> kept = new ArrayList<>();
 
@@ -185,7 +187,7 @@ public List<String> getToBeDeleted() {
 	 * @return a list of all files removed by this checkout
 	 */
 	public List<String> getRemoved() {
-		return removed;
+		return new ArrayList<>(removed);
 	}
 
 	/**
@@ -214,6 +216,14 @@ public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc,
 		this.mergeCommitTree = mergeCommitTree;
 		this.workingTree = workingTree;
 		this.initialCheckout = !repo.isBare() && !repo.getIndexFile().exists();
+		boolean caseInsensitive = !repo.isBare()
+				&& repo.isWorkTreeCaseInsensitive();
+		this.removed = caseInsensitive
+				? new TreeSet<>(String::compareToIgnoreCase)
+				: new TreeSet<>();
+		this.existing = caseInsensitive
+				? new TreeSet<>(String::compareToIgnoreCase)
+				: null;
 	}
 
 	/**
@@ -400,9 +410,11 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i,
 						// content to be checked out.
 						update(m);
 					}
-				} else
+				} else {
 					update(m);
-			} else if (f == null || !m.idEqual(i)) {
+				}
+			} else if (f == null || !m.idEqual(i)
+					|| m.getEntryRawMode() != i.getEntryRawMode()) {
 				// The working tree file is missing or the merge content differs
 				// from index content
 				update(m);
@@ -410,11 +422,11 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i,
 				// The index contains a file (and not a folder)
 				if (f.isModified(i.getDirCacheEntry(), true,
 						this.walk.getObjectReader())
-						|| i.getDirCacheEntry().getStage() != 0)
+						|| i.getDirCacheEntry().getStage() != 0) {
 					// The working tree file is dirty or the index contains a
 					// conflict
 					update(m);
-				else {
+				} else {
 					// update the timestamp of the index with the one from the
 					// file if not set, as we are sure to be in sync here.
 					DirCacheEntry entry = i.getDirCacheEntry();
@@ -424,9 +436,10 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i,
 					}
 					keep(i.getEntryPathString(), entry, f);
 				}
-			} else
+			} else {
 				// The index contains a folder
 				keep(i.getEntryPathString(), i.getDirCacheEntry(), f);
+			}
 		} else {
 			// There is no entry in the merge commit. Means: we want to delete
 			// what's currently in the index and working tree
@@ -521,6 +534,13 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
 			// update our index
 			builder.finish();
 
+			// On case-insensitive file systems we may have a case variant kept
+			// and another one removed. In that case, don't remove it.
+			if (existing != null) {
+				removed.removeAll(existing);
+				existing.clear();
+			}
+
 			// init progress reporting
 			int numTotal = removed.size() + updated.size() + conflicts.size();
 			monitor.beginTask(JGitText.get().checkingOutFiles, numTotal);
@@ -531,9 +551,9 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
 			// when deleting files process them in the opposite order as they have
 			// been reported. This ensures the files are deleted before we delete
 			// their parent folders
-			IntList nonDeleted = new IntList();
-			for (int i = removed.size() - 1; i >= 0; i--) {
-				String r = removed.get(i);
+			Iterator<String> iter = removed.descendingIterator();
+			while (iter.hasNext()) {
+				String r = iter.next();
 				file = new File(repo.getWorkTree(), r);
 				if (!file.delete() && repo.getFS().exists(file)) {
 					// The list of stuff to delete comes from the index
@@ -542,7 +562,7 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
 					// to delete it. A submodule is not empty, so it
 					// is safe to check this after a failed delete.
 					if (!repo.getFS().isDirectory(file)) {
-						nonDeleted.add(i);
+						iter.remove();
 						toBeDeleted.add(r);
 					}
 				} else {
@@ -560,8 +580,6 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
 			if (file != null) {
 				removeEmptyParents(file);
 			}
-			removed = filterOut(removed, nonDeleted);
-			nonDeleted = null;
 			Iterator<Map.Entry<String, CheckoutMetadata>> toUpdate = updated
 					.entrySet().iterator();
 			Map.Entry<String, CheckoutMetadata> e = null;
@@ -633,36 +651,6 @@ private boolean doCheckout() throws CorruptObjectException, IOException,
 		return toBeDeleted.isEmpty();
 	}
 
-	private static ArrayList<String> filterOut(ArrayList<String> strings,
-			IntList indicesToRemove) {
-		int n = indicesToRemove.size();
-		if (n == strings.size()) {
-			return new ArrayList<>(0);
-		}
-		switch (n) {
-		case 0:
-			return strings;
-		case 1:
-			strings.remove(indicesToRemove.get(0));
-			return strings;
-		default:
-			int length = strings.size();
-			ArrayList<String> result = new ArrayList<>(length - n);
-			// Process indicesToRemove from the back; we know that it
-			// contains indices in descending order.
-			int j = n - 1;
-			int idx = indicesToRemove.get(j);
-			for (int i = 0; i < length; i++) {
-				if (i == idx) {
-					idx = (--j >= 0) ? indicesToRemove.get(j) : -1;
-				} else {
-					result.add(strings.get(i));
-				}
-			}
-			return result;
-		}
-	}
-
 	private static boolean isSamePrefix(String a, String b) {
 		int as = a.lastIndexOf('/');
 		int bs = b.lastIndexOf('/');
@@ -1233,6 +1221,9 @@ private void keep(String path, DirCacheEntry e, WorkingTreeIterator f)
 		if (!FileMode.TREE.equals(e.getFileMode())) {
 			builder.add(e);
 		}
+		if (existing != null) {
+			existing.add(path);
+		}
 		if (force) {
 			if (f == null || f.isModified(e, true, walk.getObjectReader())) {
 				kept.add(path);
@@ -1401,127 +1392,6 @@ private boolean isModifiedSubtree_IndexTree(String path, ObjectId tree)
 	}
 
 	/**
-	 * Updates the file in the working tree with content and mode from an entry
-	 * in the index. The new content is first written to a new temporary file in
-	 * the same directory as the real file. Then that new file is renamed to the
-	 * final filename.
-	 *
-	 * <p>
-	 * <b>Note:</b> if the entry path on local file system exists as a non-empty
-	 * directory, and the target entry type is a link or file, the checkout will
-	 * fail with {@link java.io.IOException} since existing non-empty directory
-	 * cannot be renamed to file or link without deleting it recursively.
-	 * </p>
-	 *
-	 * @param repo
-	 *            repository managing the destination work tree.
-	 * @param entry
-	 *            the entry containing new mode and content
-	 * @param or
-	 *            object reader to use for checkout
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 * @since 3.6
-	 * @deprecated since 5.1, use
-	 *             {@link #checkoutEntry(Repository, DirCacheEntry, ObjectReader, boolean, CheckoutMetadata, WorkingTreeOptions)}
-	 *             instead
-	 */
-	@Deprecated
-	public static void checkoutEntry(Repository repo, DirCacheEntry entry,
-			ObjectReader or) throws IOException {
-		checkoutEntry(repo, entry, or, false, null, null);
-	}
-
-
-	/**
-	 * Updates the file in the working tree with content and mode from an entry
-	 * in the index. The new content is first written to a new temporary file in
-	 * the same directory as the real file. Then that new file is renamed to the
-	 * final filename.
-	 *
-	 * <p>
-	 * <b>Note:</b> if the entry path on local file system exists as a file, it
-	 * will be deleted and if it exists as a directory, it will be deleted
-	 * recursively, independently if has any content.
-	 * </p>
-	 *
-	 * @param repo
-	 *            repository managing the destination work tree.
-	 * @param entry
-	 *            the entry containing new mode and content
-	 * @param or
-	 *            object reader to use for checkout
-	 * @param deleteRecursive
-	 *            true to recursively delete final path if it exists on the file
-	 *            system
-	 * @param checkoutMetadata
-	 *            containing
-	 *            <ul>
-	 *            <li>smudgeFilterCommand to be run for smudging the entry to be
-	 *            checked out</li>
-	 *            <li>eolStreamType used for stream conversion</li>
-	 *            </ul>
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 * @since 4.2
-	 * @deprecated since 6.3, use
-	 *             {@link #checkoutEntry(Repository, DirCacheEntry, ObjectReader, boolean, CheckoutMetadata, WorkingTreeOptions)}
-	 *             instead
-	 */
-	@Deprecated
-	public static void checkoutEntry(Repository repo, DirCacheEntry entry,
-			ObjectReader or, boolean deleteRecursive,
-			CheckoutMetadata checkoutMetadata) throws IOException {
-		checkoutEntry(repo, entry, or, deleteRecursive, checkoutMetadata, null);
-	}
-
-	/**
-	 * Updates the file in the working tree with content and mode from an entry
-	 * in the index. The new content is first written to a new temporary file in
-	 * the same directory as the real file. Then that new file is renamed to the
-	 * final filename.
-	 *
-	 * <p>
-	 * <b>Note:</b> if the entry path on local file system exists as a file, it
-	 * will be deleted and if it exists as a directory, it will be deleted
-	 * recursively, independently if has any content.
-	 * </p>
-	 *
-	 * @param repo
-	 *            repository managing the destination work tree.
-	 * @param entry
-	 *            the entry containing new mode and content
-	 * @param or
-	 *            object reader to use for checkout
-	 * @param deleteRecursive
-	 *            true to recursively delete final path if it exists on the file
-	 *            system
-	 * @param checkoutMetadata
-	 *            containing
-	 *            <ul>
-	 *            <li>smudgeFilterCommand to be run for smudging the entry to be
-	 *            checked out</li>
-	 *            <li>eolStreamType used for stream conversion</li>
-	 *            </ul>
-	 * @param options
-	 *            {@link WorkingTreeOptions} that are effective; if {@code null}
-	 *            they are loaded from the repository config
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 * @since 6.3
-	 * @deprecated since 6.6.1; use {@link Checkout} instead
-	 */
-	@Deprecated
-	public static void checkoutEntry(Repository repo, DirCacheEntry entry,
-			ObjectReader or, boolean deleteRecursive,
-			CheckoutMetadata checkoutMetadata, WorkingTreeOptions options)
-			throws IOException {
-		Checkout checkout = new Checkout(repo, options)
-				.setRecursiveDeletion(deleteRecursive);
-		checkout.checkout(entry, checkoutMetadata, or, null);
-	}
-
-	/**
 	 * Return filtered content for a specific object (blob). EOL handling and
 	 * smudge-filter handling are applied in the same way as it would be done
 	 * during a checkout.
@@ -1647,6 +1517,8 @@ private static void runExternalFilterCommand(Repository repo, String path,
 		filterProcessBuilder.directory(repo.getWorkTree());
 		filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY,
 				repo.getDirectory().getAbsolutePath());
+		filterProcessBuilder.environment().put(Constants.GIT_COMMON_DIR_KEY,
+				repo.getCommonDirectory().getAbsolutePath());
 		ExecutionResult result;
 		int rc;
 		try {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
index c5e1e4e..5a22938 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
@@ -396,28 +396,6 @@ void write(OutputStream os, DirCacheVersion version, DirCacheEntry previous)
 	 * timestamp. This method tests to see if file was written out at the same
 	 * time as the index.
 	 *
-	 * @param smudge_s
-	 *            seconds component of the index's last modified time.
-	 * @param smudge_ns
-	 *            nanoseconds component of the index's last modified time.
-	 * @return true if extra careful checks should be used.
-	 * @deprecated use {@link #mightBeRacilyClean(Instant)} instead
-	 */
-	@Deprecated
-	public final boolean mightBeRacilyClean(int smudge_s, int smudge_ns) {
-		return mightBeRacilyClean(Instant.ofEpochSecond(smudge_s, smudge_ns));
-	}
-
-	/**
-	 * Is it possible for this entry to be accidentally assumed clean?
-	 * <p>
-	 * The "racy git" problem happens when a work file can be updated faster
-	 * than the filesystem records file modification timestamps. It is possible
-	 * for an application to edit a work file, update the index, then edit it
-	 * again before the filesystem will give the work file a new modification
-	 * timestamp. This method tests to see if file was written out at the same
-	 * time as the index.
-	 *
 	 * @param smudge
 	 *            index's last modified time.
 	 * @return true if extra careful checks should be used.
@@ -653,22 +631,6 @@ public void setCreationTime(long when) {
 	}
 
 	/**
-	 * Get the cached last modification date of this file, in milliseconds.
-	 * <p>
-	 * One of the indicators that the file has been modified by an application
-	 * changing the working tree is if the last modification time for the file
-	 * differs from the time stored in this entry.
-	 *
-	 * @return last modification time of this file, in milliseconds since the
-	 *         Java epoch (midnight Jan 1, 1970 UTC).
-	 * @deprecated use {@link #getLastModifiedInstant()} instead
-	 */
-	@Deprecated
-	public long getLastModified() {
-		return decodeTS(P_MTIME);
-	}
-
-	/**
 	 * Get the cached last modification date of this file.
 	 * <p>
 	 * One of the indicators that the file has been modified by an application
@@ -683,18 +645,6 @@ public Instant getLastModifiedInstant() {
 	}
 
 	/**
-	 * Set the cached last modification date of this file, using milliseconds.
-	 *
-	 * @param when
-	 *            new cached modification date of the file, in milliseconds.
-	 * @deprecated use {@link #setLastModified(Instant)} instead
-	 */
-	@Deprecated
-	public void setLastModified(long when) {
-		encodeTS(P_MTIME, when);
-	}
-
-	/**
 	 * Set the cached last modification date of this file.
 	 *
 	 * @param when
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 1fd8086..38982fd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java
@@ -23,18 +23,6 @@ public class PackInvalidException extends IOException {
 	private static final long serialVersionUID = 1L;
 
 	/**
-	 * Construct a pack invalid error.
-	 *
-	 * @param path
-	 *            path of the invalid pack file.
-	 * @deprecated Use {@link #PackInvalidException(File, Throwable)}.
-	 */
-	@Deprecated
-	public PackInvalidException(File path) {
-		this(path, null);
-	}
-
-	/**
 	 * Construct a pack invalid error with cause.
 	 *
 	 * @param path
@@ -48,18 +36,6 @@ public PackInvalidException(File path, Throwable cause) {
 	}
 
 	/**
-	 * Construct a pack invalid error.
-	 *
-	 * @param path
-	 *            path of the invalid pack file.
-	 * @deprecated Use {@link #PackInvalidException(String, Throwable)}.
-	 */
-	@Deprecated
-	public PackInvalidException(String path) {
-		this(path, null);
-	}
-
-	/**
 	 * Construct a pack invalid error with cause.
 	 *
 	 * @param path
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java
index 3ce97a4..e511a68 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java
@@ -156,6 +156,9 @@ private void prepareIndex(List<RepoProject> projects, DirCache index,
 			ObjectId objectId;
 			if (ObjectId.isId(proj.getRevision())) {
 				objectId = ObjectId.fromString(proj.getRevision());
+				if (config.recordRemoteBranch && proj.getUpstream() != null) {
+					cfg.setString("submodule", name, "ref", proj.getUpstream()); //$NON-NLS-1$//$NON-NLS-2$
+				}
 			} else {
 				objectId = callback.sha1(url, proj.getRevision());
 				if (objectId == null && !config.ignoreRemoteFailures) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
index 957b386..b033177 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
@@ -176,6 +176,10 @@ public void startElement(
 					attributes.getValue("groups"));
 			currentProject
 					.setRecommendShallow(attributes.getValue("clone-depth"));
+			currentProject
+					.setUpstream(attributes.getValue("upstream"));
+			currentProject
+					.setDestBranch(attributes.getValue("dest-branch"));
 			break;
 		case "remote":
 			String alias = attributes.getValue("alias");
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
index 95c1c8b..be77fca 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
@@ -111,32 +111,6 @@ public interface RemoteReader {
 		public ObjectId sha1(String uri, String ref) throws GitAPIException;
 
 		/**
-		 * Read a file from a remote repository.
-		 *
-		 * @param uri
-		 *            The URI of the remote repository
-		 * @param ref
-		 *            The ref (branch/tag/etc.) to read
-		 * @param path
-		 *            The relative path (inside the repo) to the file to read
-		 * @return the file content.
-		 * @throws GitAPIException
-		 *             If the ref have an invalid or ambiguous name, or it does
-		 *             not exist in the repository,
-		 * @throws IOException
-		 *             If the object does not exist or is too large
-		 * @since 3.5
-		 *
-		 * @deprecated Use {@link #readFileWithMode(String, String, String)}
-		 *             instead
-		 */
-		@Deprecated
-		public default byte[] readFile(String uri, String ref, String path)
-				throws GitAPIException, IOException {
-			return readFileWithMode(uri, ref, path).getContents();
-		}
-
-		/**
 		 * Read contents and mode (i.e. permissions) of the file from a remote
 		 * repository.
 		 *
@@ -255,7 +229,8 @@ public RemoteFile readFileWithMode(String uri, String ref, String path)
 	@SuppressWarnings("serial")
 	static class ManifestErrorException extends GitAPIException {
 		ManifestErrorException(Throwable cause) {
-			super(RepoText.get().invalidManifest, cause);
+			super(RepoText.get().invalidManifest + " " + cause.getMessage(), //$NON-NLS-1$
+					cause);
 		}
 	}
 
@@ -615,6 +590,7 @@ private List<RepoProject> renameProjects(List<RepoProject> projects) {
 				p.setUrl(proj.getUrl());
 				p.addCopyFiles(proj.getCopyFiles());
 				p.addLinkFiles(proj.getLinkFiles());
+				p.setUpstream(proj.getUpstream());
 				ret.add(p);
 			}
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
index 8deb738..2630da3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
@@ -38,6 +38,8 @@ public class RepoProject implements Comparable<RepoProject> {
 	private final Set<String> groups;
 	private final List<CopyFile> copyfiles;
 	private final List<LinkFile> linkfiles;
+	private String upstream;
+	private String destBranch;
 	private String recommendShallow;
 	private String url;
 	private String defaultRevision;
@@ -389,6 +391,57 @@ public void clearLinkFiles() {
 		this.linkfiles.clear();
 	}
 
+	/**
+	 * Return the upstream attribute of the project
+	 *
+	 * @return the upstream value if present, null otherwise.
+	 *
+	 * @since 6.10
+	 */
+	public String getUpstream() {
+		return this.upstream;
+	}
+
+	/**
+	 * Return the dest-branch attribute of the project
+	 *
+	 * @return the dest-branch value if present, null otherwise.
+	 *
+	 * @since 6.10
+	 */
+	public String getDestBranch() {
+		return this.destBranch;
+	}
+
+	/**
+	 * Set the upstream attribute of the project
+	 *
+	 * Name of the git ref in which a sha1 can be found, when the revision is a
+	 * sha1.
+	 *
+	 * @param upstream
+	 *            value of the attribute in the manifest
+	 *
+	 * @since 6.10
+	 */
+	public void setUpstream(String upstream) {
+		this.upstream = upstream;
+	}
+
+	/**
+	 * Set the dest-branch attribute of the project
+	 *
+	 * Name of a Git branch.
+	 *
+	 * @param destBranch
+	 *            value of the attribute in the manifest
+	 *
+	 * @since 6.10
+	 */
+	public void setDestBranch(String destBranch) {
+		this.destBranch = destBranch;
+	}
+
 	private String getPathWithSlash() {
 		if (path.endsWith("/")) { //$NON-NLS-1$
 			return path;
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 700b54a..bf252f9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -94,8 +94,6 @@ public static JGitText get() {
 	/***/ public String binaryHunkInvalidLength;
 	/***/ public String binaryHunkLineTooShort;
 	/***/ public String binaryHunkMissingNewline;
-	/***/ public String bitmapAccessErrorForPackfile;
-	/***/ public String bitmapFailedToGet;
 	/***/ public String bitmapMissingObject;
 	/***/ public String bitmapsMustBePrepared;
 	/***/ public String bitmapUseNoopNoListener;
@@ -108,6 +106,7 @@ public static JGitText get() {
 	/***/ public String buildingBitmaps;
 	/***/ public String cachedPacksPreventsIndexCreation;
 	/***/ public String cachedPacksPreventsListingObjects;
+	/***/ public String cacheRegionAllOrNoneNull;
 	/***/ public String cannotAccessLastModifiedForSafeDeletion;
 	/***/ public String cannotBeCombined;
 	/***/ public String cannotBeRecursiveWhenTreesAreIncluded;
@@ -295,6 +294,7 @@ public static JGitText get() {
 	/***/ public String deleteTagUnexpectedResult;
 	/***/ public String deletingBranches;
 	/***/ public String deletingNotSupported;
+	/***/ public String deprecatedTrustFolderStat;
 	/***/ public String depthMustBeAt1;
 	/***/ public String depthWithUnshallow;
 	/***/ public String destinationIsNotAWildcard;
@@ -315,6 +315,9 @@ public static JGitText get() {
 	/***/ public String downloadCancelled;
 	/***/ public String downloadCancelledDuringIndexing;
 	/***/ public String duplicateAdvertisementsOf;
+	/***/ public String duplicateCacheTablesGiven;
+	/***/ public String duplicatePackExtensionsForCacheTables;
+	/***/ public String duplicatePackExtensionsSet;
 	/***/ public String duplicateRef;
 	/***/ public String duplicateRefAttribute;
 	/***/ public String duplicateRemoteRefUpdateIsIllegal;
@@ -490,6 +493,7 @@ public static JGitText get() {
 	/***/ public String invalidTimeUnitValue2;
 	/***/ public String invalidTimeUnitValue3;
 	/***/ public String invalidTreeZeroLengthName;
+	/***/ public String invalidTrustStat;
 	/***/ public String invalidURL;
 	/***/ public String invalidWildcards;
 	/***/ public String invalidRefSpec;
@@ -554,6 +558,8 @@ public static JGitText get() {
 	/***/ public String month;
 	/***/ public String months;
 	/***/ public String monthsAgo;
+	/***/ public String multiPackIndexUnexpectedSize;
+	/***/ public String multiPackIndexWritingCancelled;
 	/***/ public String multipleMergeBasesFor;
 	/***/ public String nameMustNotBeNullOrEmpty;
 	/***/ public String need2Arguments;
@@ -569,6 +575,8 @@ public static JGitText get() {
 	/***/ public String noMergeHeadSpecified;
 	/***/ public String nonBareLinkFilesNotSupported;
 	/***/ public String nonCommitToHeads;
+	/***/ public String noPackExtConfigurationGiven;
+	/***/ public String noPackExtGivenForConfiguration;
 	/***/ public String noPathAttributesFound;
 	/***/ public String noSuchRef;
 	/***/ public String noSuchRefKnown;
@@ -601,7 +609,6 @@ public static JGitText get() {
 	/***/ public String oldIdMustNotBeNull;
 	/***/ public String onlyOneFetchSupported;
 	/***/ public String onlyOneOperationCallPerConnectionIsSupported;
-	/***/ public String onlyOpenPgpSupportedForSigning;
 	/***/ public String openFilesMustBeAtLeast1;
 	/***/ public String openingConnection;
 	/***/ public String operationCanceled;
@@ -623,6 +630,8 @@ public static JGitText get() {
 	/***/ public String packingCancelledDuringObjectsWriting;
 	/***/ public String packObjectCountMismatch;
 	/***/ public String packRefs;
+	/***/ public String packRefsFailed;
+	/***/ public String packRefsSuccessful;
 	/***/ public String packSizeNotSetYet;
 	/***/ public String packTooLargeForIndexVersion1;
 	/***/ public String packWasDeleted;
@@ -639,6 +648,7 @@ public static JGitText get() {
 	/***/ public String personIdentEmailNonNull;
 	/***/ public String personIdentNameNonNull;
 	/***/ public String postCommitHookFailed;
+	/***/ public String precedenceTrustConfig;
 	/***/ public String prefixRemote;
 	/***/ public String problemWithResolvingPushRefSpecsLocally;
 	/***/ public String progressMonUploading;
@@ -666,8 +676,6 @@ public static JGitText get() {
 	/***/ public String readerIsRequired;
 	/***/ public String readingObjectsFromLocalRepositoryFailed;
 	/***/ public String readLastModifiedFailed;
-	/***/ public String readPipeIsNotAllowed;
-	/***/ public String readPipeIsNotAllowedRequiredPermission;
 	/***/ public String readTimedOut;
 	/***/ public String receivePackObjectTooLarge1;
 	/***/ public String receivePackObjectTooLarge2;
@@ -747,6 +755,8 @@ public static JGitText get() {
 	/***/ public String shutdownCleanup;
 	/***/ public String shutdownCleanupFailed;
 	/***/ public String shutdownCleanupListenerFailed;
+	/***/ public String signatureServiceConflict;
+	/***/ public String signatureTypeUnknown;
 	/***/ public String signatureVerificationError;
 	/***/ public String signatureVerificationUnavailable;
 	/***/ public String signedTagMessageNoLf;
@@ -833,6 +843,7 @@ public static JGitText get() {
 	/***/ public String unableToCheckConnectivity;
 	/***/ public String unableToCreateNewObject;
 	/***/ public String unableToReadFullInt;
+	/***/ public String unableToReadFullArray;
 	/***/ public String unableToReadPackfile;
 	/***/ public String unableToRemovePath;
 	/***/ public String unableToWrite;
@@ -858,6 +869,7 @@ public static JGitText get() {
 	/***/ public String unknownObjectInIndex;
 	/***/ public String unknownObjectType;
 	/***/ public String unknownObjectType2;
+	/***/ public String unknownPackExtension;
 	/***/ public String unknownPositionEncoding;
 	/***/ public String unknownRefStorageFormat;
 	/***/ public String unknownRepositoryFormat;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriter.java
index 0d9815e..55539e2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriter.java
@@ -52,6 +52,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.EmptyTreeIterator;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.NB;
 
 /**
@@ -71,6 +72,9 @@ public class CommitGraphWriter {
 
 	private static final int MAX_CHANGED_PATHS = 512;
 
+	private static final PathDiffCalculator PATH_DIFF_CALCULATOR
+			= new PathDiffCalculator();
+
 	private final int hashsz;
 
 	private final GraphCommits graphCommits;
@@ -374,37 +378,6 @@ private void writeCommitData(CancellableDigestOutputStream out)
 		return generations;
 	}
 
-	private static Optional<HashSet<ByteBuffer>> computeBloomFilterPaths(
-			ObjectReader or, RevCommit cmit) throws MissingObjectException,
-			IncorrectObjectTypeException, CorruptObjectException, IOException {
-		HashSet<ByteBuffer> paths = new HashSet<>();
-		try (TreeWalk walk = new TreeWalk(null, or)) {
-			walk.setRecursive(true);
-			if (cmit.getParentCount() == 0) {
-				walk.addTree(new EmptyTreeIterator());
-			} else {
-				walk.addTree(cmit.getParent(0).getTree());
-			}
-			walk.addTree(cmit.getTree());
-			while (walk.next()) {
-				if (walk.idEqual(0, 1)) {
-					continue;
-				}
-				byte[] rawPath = walk.getRawPath();
-				paths.add(ByteBuffer.wrap(rawPath));
-				for (int i = 0; i < rawPath.length; i++) {
-					if (rawPath[i] == '/') {
-						paths.add(ByteBuffer.wrap(rawPath, 0, i));
-					}
-					if (paths.size() > MAX_CHANGED_PATHS) {
-						return Optional.empty();
-					}
-				}
-			}
-		}
-		return Optional.of(paths);
-	}
-
 	private BloomFilterChunks computeBloomFilterChunks(ProgressMonitor monitor)
 			throws MissingObjectException, IncorrectObjectTypeException,
 			CorruptObjectException, IOException {
@@ -435,8 +408,8 @@ private BloomFilterChunks computeBloomFilterChunks(ProgressMonitor monitor)
 					filtersReused++;
 				} else {
 					filtersComputed++;
-					Optional<HashSet<ByteBuffer>> paths = computeBloomFilterPaths(
-							graphCommits.getObjectReader(), cmit);
+					Optional<HashSet<ByteBuffer>> paths = PATH_DIFF_CALCULATOR
+							.changedPaths(graphCommits.getObjectReader(), cmit);
 					if (paths.isEmpty()) {
 						cpf = ChangedPathFilter.FULL;
 					} else {
@@ -473,6 +446,44 @@ private void writeExtraEdges(CancellableDigestOutputStream out)
 		}
 	}
 
+	// Visible for testing
+	static class PathDiffCalculator {
+
+		// Walk steps in the last invocation of changedPaths
+		int stepCounter;
+
+		Optional<HashSet<ByteBuffer>> changedPaths(
+				ObjectReader or, RevCommit cmit) throws MissingObjectException,
+				IncorrectObjectTypeException, CorruptObjectException, IOException {
+			stepCounter = 0;
+			HashSet<ByteBuffer> paths = new HashSet<>();
+			try (TreeWalk walk = new TreeWalk(null, or)) {
+				walk.setRecursive(true);
+				walk.setFilter(TreeFilter.ANY_DIFF);
+				if (cmit.getParentCount() == 0) {
+					walk.addTree(new EmptyTreeIterator());
+				} else {
+					walk.addTree(cmit.getParent(0).getTree());
+				}
+				walk.addTree(cmit.getTree());
+				while (walk.next()) {
+					stepCounter += 1;
+					byte[] rawPath = walk.getRawPath();
+					paths.add(ByteBuffer.wrap(rawPath));
+					for (int i = 0; i < rawPath.length; i++) {
+						if (rawPath[i] == '/') {
+							paths.add(ByteBuffer.wrap(rawPath, 0, i));
+						}
+						if (paths.size() > MAX_CHANGED_PATHS) {
+							return Optional.empty();
+						}
+					}
+				}
+			}
+			return Optional.of(paths);
+		}
+	}
+
 	private static class ChunkHeader {
 		final int id;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStats.java
new file mode 100644
index 0000000..295b702
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStats.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2024, Google LLC 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
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+
+/**
+ * Aggregates values for all given {@link BlockCacheStats}.
+ */
+class AggregatedBlockCacheStats implements BlockCacheStats {
+	private final List<BlockCacheStats> blockCacheStats;
+
+	static BlockCacheStats fromStatsList(
+			List<BlockCacheStats> blockCacheStats) {
+		if (blockCacheStats.size() == 1) {
+			return blockCacheStats.get(0);
+		}
+		return new AggregatedBlockCacheStats(blockCacheStats);
+	}
+
+	private AggregatedBlockCacheStats(List<BlockCacheStats> blockCacheStats) {
+		this.blockCacheStats = blockCacheStats;
+	}
+
+	@Override
+	public String getName() {
+		return AggregatedBlockCacheStats.class.getName();
+	}
+
+	@Override
+	public long[] getCurrentSize() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getCurrentSize());
+		}
+		return sums;
+	}
+
+	@Override
+	public long[] getHitCount() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getHitCount());
+		}
+		return sums;
+	}
+
+	@Override
+	public long[] getMissCount() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getMissCount());
+		}
+		return sums;
+	}
+
+	@Override
+	public long[] getTotalRequestCount() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getTotalRequestCount());
+		}
+		return sums;
+	}
+
+	@Override
+	public long[] getHitRatio() {
+		long[] hit = getHitCount();
+		long[] miss = getMissCount();
+		long[] ratio = new long[Math.max(hit.length, miss.length)];
+		for (int i = 0; i < ratio.length; i++) {
+			if (i >= hit.length) {
+				ratio[i] = 0;
+			} else if (i >= miss.length) {
+				ratio[i] = 100;
+			} else {
+				long total = hit[i] + miss[i];
+				ratio[i] = total == 0 ? 0 : hit[i] * 100 / total;
+			}
+		}
+		return ratio;
+	}
+
+	@Override
+	public long[] getEvictions() {
+		long[] sums = emptyPackStats();
+		for (BlockCacheStats blockCacheStatsEntry : blockCacheStats) {
+			sums = add(sums, blockCacheStatsEntry.getEvictions());
+		}
+		return sums;
+	}
+
+	private static long[] emptyPackStats() {
+		return new long[PackExt.values().length];
+	}
+
+	private static long[] add(long[] first, long[] second) {
+		long[] sums = new long[Integer.max(first.length, second.length)];
+		int i;
+		for (i = 0; i < Integer.min(first.length, second.length); i++) {
+			sums[i] = first[i] + second[i];
+		}
+		for (int j = i; j < first.length; j++) {
+			sums[j] = first[i];
+		}
+		for (int j = i; j < second.length; j++) {
+			sums[j] = second[i];
+		}
+		return sums;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java
index d0907bc..587d482 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTable.java
@@ -12,6 +12,7 @@
 
 import java.io.IOException;
 import java.time.Duration;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicReferenceArray;
@@ -47,6 +48,11 @@
  * invocations is also fixed in size.
  */
 final class ClockBlockCacheTable implements DfsBlockCacheTable {
+	/**
+	 * Table name.
+	 */
+	private final String name;
+
 	/** Number of entries in {@link #table}. */
 	private final int tableSize;
 
@@ -129,14 +135,20 @@ final class ClockBlockCacheTable implements DfsBlockCacheTable {
 				-1, 0, null);
 		clockHand.next = clockHand;
 
-		this.dfsBlockCacheStats = new DfsBlockCacheStats();
+		this.name = cfg.getName();
+		this.dfsBlockCacheStats = new DfsBlockCacheStats(this.name);
 		this.refLockWaitTime = cfg.getRefLockWaitTimeConsumer();
 		this.indexEventConsumer = cfg.getIndexEventConsumer();
 	}
 
 	@Override
-	public DfsBlockCacheStats getDfsBlockCacheStats() {
-		return dfsBlockCacheStats;
+	public List<BlockCacheStats> getBlockCacheStats() {
+		return List.of(dfsBlockCacheStats);
+	}
+
+	@Override
+	public String getName() {
+		return name;
 	}
 
 	@Override
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
index 56719cf..f8e0831 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
@@ -11,7 +11,10 @@
 
 package org.eclipse.jgit.internal.storage.dfs;
 
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+
 import java.io.IOException;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.LongStream;
 
@@ -97,7 +100,12 @@ private DfsBlockCache(DfsBlockCacheConfig cfg) {
 		double streamRatio = cfg.getStreamRatio();
 		maxStreamThroughCache = (long) (maxBytes * streamRatio);
 
-		dfsBlockCacheTable = new ClockBlockCacheTable(cfg);
+		if (!cfg.getPackExtCacheConfigurations().isEmpty()) {
+			dfsBlockCacheTable = PackExtBlockCacheTable
+					.fromBlockCacheConfigs(cfg);
+		} else {
+			dfsBlockCacheTable = new ClockBlockCacheTable(cfg);
+		}
 
 		for (int i = 0; i < PackExt.values().length; ++i) {
 			Integer limit = cfg.getCacheHotMap().get(PackExt.values()[i]);
@@ -119,7 +127,7 @@ boolean shouldCopyThroughCache(long length) {
 	 * @return total number of bytes in the cache, per pack file extension.
 	 */
 	public long[] getCurrentSize() {
-		return dfsBlockCacheTable.getDfsBlockCacheStats().getCurrentSize();
+		return getAggregatedBlockCacheStats().getCurrentSize();
 	}
 
 	/**
@@ -138,7 +146,7 @@ public long getFillPercentage() {
 	 *         extension.
 	 */
 	public long[] getHitCount() {
-		return dfsBlockCacheTable.getDfsBlockCacheStats().getHitCount();
+		return getAggregatedBlockCacheStats().getHitCount();
 	}
 
 	/**
@@ -149,7 +157,7 @@ public long getFillPercentage() {
 	 *         extension.
 	 */
 	public long[] getMissCount() {
-		return dfsBlockCacheTable.getDfsBlockCacheStats().getMissCount();
+		return getAggregatedBlockCacheStats().getMissCount();
 	}
 
 	/**
@@ -158,8 +166,7 @@ public long getFillPercentage() {
 	 * @return total number of requests (hit + miss), per pack file extension.
 	 */
 	public long[] getTotalRequestCount() {
-		return dfsBlockCacheTable.getDfsBlockCacheStats()
-				.getTotalRequestCount();
+		return getAggregatedBlockCacheStats().getTotalRequestCount();
 	}
 
 	/**
@@ -168,7 +175,7 @@ public long getFillPercentage() {
 	 * @return hit ratios
 	 */
 	public long[] getHitRatio() {
-		return dfsBlockCacheTable.getDfsBlockCacheStats().getHitRatio();
+		return getAggregatedBlockCacheStats().getHitRatio();
 	}
 
 	/**
@@ -179,7 +186,18 @@ public long getFillPercentage() {
 	 *         file extension.
 	 */
 	public long[] getEvictions() {
-		return dfsBlockCacheTable.getDfsBlockCacheStats().getEvictions();
+		return getAggregatedBlockCacheStats().getEvictions();
+	}
+
+	/**
+	 * Get the list of {@link BlockCacheStats} for all underlying caches.
+	 * <p>
+	 * Useful in monitoring caches with breakdown.
+	 *
+	 * @return the list of {@link BlockCacheStats} for all underlying caches.
+	 */
+	public List<BlockCacheStats> getAllBlockCacheStats() {
+		return dfsBlockCacheTable.getBlockCacheStats();
 	}
 
 	/**
@@ -259,6 +277,11 @@ <T> T get(DfsStreamKey key, long position) {
 		return dfsBlockCacheTable.get(key, position);
 	}
 
+	private BlockCacheStats getAggregatedBlockCacheStats() {
+		return AggregatedBlockCacheStats
+				.fromStatsList(dfsBlockCacheTable.getBlockCacheStats());
+	}
+
 	static final class Ref<T> {
 		final DfsStreamKey key;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java
index 77273ce..17bf518 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java
@@ -11,17 +11,27 @@
 package org.eclipse.jgit.internal.storage.dfs;
 
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_CACHE_PREFIX;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_LIMIT;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_SIZE;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CONCURRENCY_LEVEL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACK_EXTENSIONS;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO;
 
+import java.io.PrintWriter;
 import java.text.MessageFormat;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
@@ -41,25 +51,103 @@ public class DfsBlockCacheConfig {
 	/** Default number of max cache hits. */
 	public static final int DEFAULT_CACHE_HOT_MAX = 1;
 
+	static final String DEFAULT_NAME = "<default>"; //$NON-NLS-1$
+
+	private String name;
+
 	private long blockLimit;
+
 	private int blockSize;
+
 	private double streamRatio;
+
 	private int concurrencyLevel;
 
 	private Consumer<Long> refLock;
+
 	private Map<PackExt, Integer> cacheHotMap;
 
 	private IndexEventConsumer indexEventConsumer;
 
+	private List<DfsBlockCachePackExtConfig> packExtCacheConfigurations;
+
 	/**
 	 * Create a default configuration.
 	 */
 	public DfsBlockCacheConfig() {
+		name = DEFAULT_NAME;
 		setBlockLimit(32 * MB);
 		setBlockSize(64 * KB);
 		setStreamRatio(0.30);
 		setConcurrencyLevel(32);
 		cacheHotMap = Collections.emptyMap();
+		packExtCacheConfigurations = Collections.emptyList();
+	}
+
+	/**
+	 * Print the current cache configuration to the given {@link PrintWriter}.
+	 *
+	 * @param writer
+	 *            {@link PrintWriter} to write the cache's configuration to.
+	 */
+	public void print(PrintWriter writer) {
+		print(/* linePrefix= */ "", /* pad= */ "  ", writer); //$NON-NLS-1$//$NON-NLS-2$
+	}
+
+	/**
+	 * Print the current cache configuration to the given {@link PrintWriter}.
+	 *
+	 * @param linePrefix
+	 *            prefix to prepend all writen lines with. Ex a string of 0 or
+	 *            more " " entries.
+	 * @param pad
+	 *            filler used to extend linePrefix. Ex a multiple of " ".
+	 * @param writer
+	 *            {@link PrintWriter} to write the cache's configuration to.
+	 */
+	@SuppressWarnings("nls")
+	private void print(String linePrefix, String pad, PrintWriter writer) {
+		String currentPrefixLevel = linePrefix;
+		if (!name.isEmpty() || !packExtCacheConfigurations.isEmpty()) {
+			writer.println(linePrefix + "Name: "
+					+ (name.isEmpty() ? DEFAULT_NAME : this.name));
+			currentPrefixLevel += pad;
+		}
+		writer.println(currentPrefixLevel + "BlockLimit: " + blockLimit);
+		writer.println(currentPrefixLevel + "BlockSize: " + blockSize);
+		writer.println(currentPrefixLevel + "StreamRatio: " + streamRatio);
+		writer.println(
+				currentPrefixLevel + "ConcurrencyLevel: " + concurrencyLevel);
+		for (Map.Entry<PackExt, Integer> entry : cacheHotMap.entrySet()) {
+			writer.println(currentPrefixLevel + "CacheHotMapEntry: "
+					+ entry.getKey() + " : " + entry.getValue());
+		}
+		for (DfsBlockCachePackExtConfig extConfig : packExtCacheConfigurations) {
+			extConfig.print(currentPrefixLevel, pad, writer);
+		}
+	}
+
+	/**
+	 * Get the name for the block cache configured by this cache config.
+	 *
+	 * @return the name for the block cache configured by this cache config.
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Set the name for the block cache configured by this cache config.
+	 * <p>
+	 * Made visible for testing.
+	 *
+	 * @param name
+	 *            the name for the block cache configured by this cache config.
+	 * @return {@code this}
+	 */
+	DfsBlockCacheConfig setName(String name) {
+		this.name = name;
+		return this;
 	}
 
 	/**
@@ -77,10 +165,10 @@ public long getBlockLimit() {
 	 * Set maximum number bytes of heap memory to dedicate to caching pack file
 	 * data.
 	 * <p>
-	 * It is strongly recommended to set the block limit to be an integer multiple
-	 * of the block size. This constraint is not enforced by this method (since
-	 * it may be called before {@link #setBlockSize(int)}), but it is enforced by
-	 * {@link #fromConfig(Config)}.
+	 * It is strongly recommended to set the block limit to be an integer
+	 * multiple of the block size. This constraint is not enforced by this
+	 * method (since it may be called before {@link #setBlockSize(int)}), but it
+	 * is enforced by {@link #fromConfig(Config)}.
 	 *
 	 * @param newLimit
 	 *            maximum number bytes of heap memory to dedicate to caching
@@ -89,9 +177,9 @@ public long getBlockLimit() {
 	 */
 	public DfsBlockCacheConfig setBlockLimit(long newLimit) {
 		if (newLimit <= 0) {
-			throw new IllegalArgumentException(MessageFormat.format(
-					JGitText.get().blockLimitNotPositive,
-					Long.valueOf(newLimit)));
+			throw new IllegalArgumentException(
+					MessageFormat.format(JGitText.get().blockLimitNotPositive,
+							Long.valueOf(newLimit)));
 		}
 		blockLimit = newLimit;
 		return this;
@@ -211,12 +299,24 @@ public Map<PackExt, Integer> getCacheHotMap() {
 	 *            map of hot count per pack extension for {@code DfsBlockCache}.
 	 * @return {@code this}
 	 */
+	/*
+	 * TODO The cache HotMap configuration should be set as a config option and
+	 * not passed in through a setter.
+	 */
 	public DfsBlockCacheConfig setCacheHotMap(
 			Map<PackExt, Integer> cacheHotMap) {
 		this.cacheHotMap = Collections.unmodifiableMap(cacheHotMap);
+		setCacheHotMapToPackExtConfigs(this.cacheHotMap);
 		return this;
 	}
 
+	private void setCacheHotMapToPackExtConfigs(
+			Map<PackExt, Integer> cacheHotMap) {
+		for (DfsBlockCachePackExtConfig packExtConfig : packExtCacheConfigurations) {
+			packExtConfig.setCacheHotMap(cacheHotMap);
+		}
+	}
+
 	/**
 	 * Get the consumer of cache index events.
 	 *
@@ -240,61 +340,121 @@ public DfsBlockCacheConfig setIndexEventConsumer(
 	}
 
 	/**
+	 * Get the list of pack ext cache configs.
+	 *
+	 * @return the list of pack ext cache configs.
+	 */
+	List<DfsBlockCachePackExtConfig> getPackExtCacheConfigurations() {
+		return packExtCacheConfigurations;
+	}
+
+	/**
+	 * Set the list of pack ext cache configs.
+	 *
+	 * Made visible for testing.
+	 *
+	 * @param packExtCacheConfigurations
+	 *            the list of pack ext cache configs to set.
+	 * @return {@code this}
+	 */
+	DfsBlockCacheConfig setPackExtCacheConfigurations(
+			List<DfsBlockCachePackExtConfig> packExtCacheConfigurations) {
+		this.packExtCacheConfigurations = packExtCacheConfigurations;
+		return this;
+	}
+
+	/**
 	 * Update properties by setting fields from the configuration.
 	 * <p>
 	 * If a property is not defined in the configuration, then it is left
 	 * unmodified.
 	 * <p>
-	 * Enforces certain constraints on the combination of settings in the config,
-	 * for example that the block limit is a multiple of the block size.
+	 * Enforces certain constraints on the combination of settings in the
+	 * config, for example that the block limit is a multiple of the block size.
 	 *
 	 * @param rc
 	 *            configuration to read properties from.
 	 * @return {@code this}
 	 */
 	public DfsBlockCacheConfig fromConfig(Config rc) {
-		long cfgBlockLimit = rc.getLong(
-				CONFIG_CORE_SECTION,
-				CONFIG_DFS_SECTION,
-				CONFIG_KEY_BLOCK_LIMIT,
-				getBlockLimit());
-		int cfgBlockSize = rc.getInt(
-				CONFIG_CORE_SECTION,
-				CONFIG_DFS_SECTION,
-				CONFIG_KEY_BLOCK_SIZE,
+		fromConfig(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION, rc);
+		loadPackExtConfigs(rc);
+		return this;
+	}
+
+	private void fromConfig(String section, String subSection, Config rc) {
+		long cfgBlockLimit = rc.getLong(section, subSection,
+				CONFIG_KEY_BLOCK_LIMIT, getBlockLimit());
+		int cfgBlockSize = rc.getInt(section, subSection, CONFIG_KEY_BLOCK_SIZE,
 				getBlockSize());
 		if (cfgBlockLimit % cfgBlockSize != 0) {
 			throw new IllegalArgumentException(MessageFormat.format(
 					JGitText.get().blockLimitNotMultipleOfBlockSize,
-					Long.valueOf(cfgBlockLimit),
-					Long.valueOf(cfgBlockSize)));
+					Long.valueOf(cfgBlockLimit), Long.valueOf(cfgBlockSize)));
 		}
 
+		// Set name only if `core dfs` is configured, otherwise fall back to the
+		// default.
+		if (rc.getSubsections(section).contains(subSection)) {
+			this.name = subSection;
+		}
 		setBlockLimit(cfgBlockLimit);
 		setBlockSize(cfgBlockSize);
 
-		setConcurrencyLevel(rc.getInt(
-				CONFIG_CORE_SECTION,
-				CONFIG_DFS_SECTION,
-				CONFIG_KEY_CONCURRENCY_LEVEL,
-				getConcurrencyLevel()));
+		setConcurrencyLevel(rc.getInt(section, subSection,
+				CONFIG_KEY_CONCURRENCY_LEVEL, getConcurrencyLevel()));
 
-		String v = rc.getString(
-				CONFIG_CORE_SECTION,
-				CONFIG_DFS_SECTION,
-				CONFIG_KEY_STREAM_RATIO);
+		String v = rc.getString(section, subSection, CONFIG_KEY_STREAM_RATIO);
 		if (v != null) {
 			try {
 				setStreamRatio(Double.parseDouble(v));
 			} catch (NumberFormatException e) {
 				throw new IllegalArgumentException(MessageFormat.format(
-						JGitText.get().enumValueNotSupported3,
-						CONFIG_CORE_SECTION,
-						CONFIG_DFS_SECTION,
-						CONFIG_KEY_STREAM_RATIO, v), e);
+						JGitText.get().enumValueNotSupported3, section,
+						subSection, CONFIG_KEY_STREAM_RATIO, v), e);
 			}
 		}
-		return this;
+	}
+
+	private void loadPackExtConfigs(Config config) {
+		List<String> subSections = config.getSubsections(CONFIG_CORE_SECTION)
+				.stream()
+				.filter(section -> section.startsWith(CONFIG_DFS_CACHE_PREFIX))
+				.collect(Collectors.toList());
+		if (subSections.size() == 0) {
+			return;
+		}
+		ArrayList<DfsBlockCachePackExtConfig> cacheConfigs = new ArrayList<>();
+		Set<PackExt> extensionsSeen = new HashSet<>();
+		for (String subSection : subSections) {
+			var cacheConfig = DfsBlockCachePackExtConfig.fromConfig(config,
+					CONFIG_CORE_SECTION, subSection);
+			Set<PackExt> packExtsDuplicates = intersection(extensionsSeen,
+					cacheConfig.packExts);
+			if (packExtsDuplicates.size() > 0) {
+				String duplicatePackExts = packExtsDuplicates.stream()
+						.map(PackExt::toString)
+						.collect(Collectors.joining(",")); //$NON-NLS-1$
+				throw new IllegalArgumentException(MessageFormat.format(
+						JGitText.get().duplicatePackExtensionsSet,
+						CONFIG_CORE_SECTION, subSection,
+						CONFIG_KEY_PACK_EXTENSIONS, duplicatePackExts));
+			}
+			extensionsSeen.addAll(cacheConfig.packExts);
+			cacheConfigs.add(cacheConfig);
+		}
+		packExtCacheConfigurations = cacheConfigs;
+		setCacheHotMapToPackExtConfigs(this.cacheHotMap);
+	}
+
+	private static <T> Set<T> intersection(Set<T> first, Set<T> second) {
+		Set<T> ret = new HashSet<>();
+		for (T entry : second) {
+			if (first.contains(entry)) {
+				ret.add(entry);
+			}
+		}
+		return ret;
 	}
 
 	/** Consumer of DfsBlockCache loading and eviction events for indexes. */
@@ -346,4 +506,102 @@ default boolean shouldReportEvictedEvent() {
 			return false;
 		}
 	}
-}
\ No newline at end of file
+
+	/**
+	 * A configuration for a single cache table storing 1 or more Pack
+	 * extensions.
+	 * <p>
+	 * The current pack ext cache tables implementation supports the same
+	 * parameters the ClockBlockCacheTable (current default implementation).
+	 * <p>
+	 * Configuration falls back to the defaults coded values defined in the
+	 * {@link DfsBlockCacheConfig} when not set on each cache table
+	 * configuration and NOT the values of the basic dfs section.
+	 * <p>
+	 * <code>
+	 *
+	 * Format:
+	 * [core "dfs.packCache"]
+	 *   packExtensions = "PACK"
+	 *   blockSize = 512
+	 *   blockLimit = 100
+	 *   concurrencyLevel = 5
+	 *
+	 * [core "dfs.multipleExtensionCache"]
+	 *   packExtensions = "INDEX REFTABLE BITMAP_INDEX"
+	 *   blockSize = 512
+	 *   blockLimit = 100
+	 *   concurrencyLevel = 5
+	 * </code>
+	 */
+	static class DfsBlockCachePackExtConfig {
+		// Set of pack extensions that will map to the cache instance.
+		private final EnumSet<PackExt> packExts;
+
+		// Configuration for the cache instance.
+		private final DfsBlockCacheConfig packExtCacheConfiguration;
+
+		/**
+		 * Made visible for testing.
+		 *
+		 * @param packExts
+		 *            Set of {@link PackExt}s associated to this cache config.
+		 * @param packExtCacheConfiguration
+		 *            {@link DfsBlockCacheConfig} for this cache config.
+		 */
+		DfsBlockCachePackExtConfig(EnumSet<PackExt> packExts,
+				DfsBlockCacheConfig packExtCacheConfiguration) {
+			this.packExts = packExts;
+			this.packExtCacheConfiguration = packExtCacheConfiguration;
+		}
+
+		Set<PackExt> getPackExts() {
+			return packExts;
+		}
+
+		DfsBlockCacheConfig getPackExtCacheConfiguration() {
+			return packExtCacheConfiguration;
+		}
+
+		void setCacheHotMap(Map<PackExt, Integer> cacheHotMap) {
+			Map<PackExt, Integer> packExtHotMap = packExts.stream()
+					.filter(cacheHotMap::containsKey)
+					.collect(Collectors.toUnmodifiableMap(Function.identity(),
+							cacheHotMap::get));
+			packExtCacheConfiguration.setCacheHotMap(packExtHotMap);
+		}
+
+		private static DfsBlockCachePackExtConfig fromConfig(Config config,
+				String section, String subSection) {
+			String packExtensions = config.getString(section, subSection,
+					CONFIG_KEY_PACK_EXTENSIONS);
+			if (packExtensions == null) {
+				throw new IllegalArgumentException(
+						JGitText.get().noPackExtGivenForConfiguration);
+			}
+			String[] extensions = packExtensions.split(" ", -1); //$NON-NLS-1$
+			Set<PackExt> packExts = new HashSet<>(extensions.length);
+			for (String extension : extensions) {
+				try {
+					packExts.add(PackExt.valueOf(extension));
+				} catch (IllegalArgumentException e) {
+					throw new IllegalArgumentException(MessageFormat.format(
+							JGitText.get().unknownPackExtension, section,
+							subSection, CONFIG_KEY_PACK_EXTENSIONS, extension),
+							e);
+				}
+			}
+
+			DfsBlockCacheConfig dfsBlockCacheConfig = new DfsBlockCacheConfig();
+			dfsBlockCacheConfig.fromConfig(section, subSection, config);
+			return new DfsBlockCachePackExtConfig(EnumSet.copyOf(packExts),
+					dfsBlockCacheConfig);
+		}
+
+		void print(String linePrefix, String pad, PrintWriter writer) {
+			packExtCacheConfiguration.print(linePrefix, pad, writer);
+			writer.println(linePrefix + pad + "PackExts: " //$NON-NLS-1$
+					+ packExts.stream().sorted().collect(Collectors.toList()));
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheStats.java
new file mode 100644
index 0000000..436f574
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheStats.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2024, Google LLC 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
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+
+/**
+ * Keeps track of stats for a Block Cache table.
+ */
+class DfsBlockCacheStats implements BlockCacheStats {
+	private final String name;
+
+	/**
+	 * Number of times a block was found in the cache, per pack file extension.
+	 */
+	private final AtomicReference<AtomicLong[]> statHit;
+
+	/**
+	 * Number of times a block was not found, and had to be loaded, per pack
+	 * file extension.
+	 */
+	private final AtomicReference<AtomicLong[]> statMiss;
+
+	/**
+	 * Number of blocks evicted due to cache being full, per pack file
+	 * extension.
+	 */
+	private final AtomicReference<AtomicLong[]> statEvict;
+
+	/**
+	 * Number of bytes currently loaded in the cache, per pack file extension.
+	 */
+	private final AtomicReference<AtomicLong[]> liveBytes;
+
+	DfsBlockCacheStats() {
+		this(""); //$NON-NLS-1$
+	}
+
+	DfsBlockCacheStats(String name) {
+		this.name = name;
+		statHit = new AtomicReference<>(newCounters());
+		statMiss = new AtomicReference<>(newCounters());
+		statEvict = new AtomicReference<>(newCounters());
+		liveBytes = new AtomicReference<>(newCounters());
+	}
+
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Increment the {@code statHit} count.
+	 *
+	 * @param key
+	 *            key identifying which liveBytes entry to update.
+	 */
+	void incrementHit(DfsStreamKey key) {
+		getStat(statHit, key).incrementAndGet();
+	}
+
+	/**
+	 * Increment the {@code statMiss} count.
+	 *
+	 * @param key
+	 *            key identifying which liveBytes entry to update.
+	 */
+	void incrementMiss(DfsStreamKey key) {
+		getStat(statMiss, key).incrementAndGet();
+	}
+
+	/**
+	 * Increment the {@code statEvict} count.
+	 *
+	 * @param key
+	 *            key identifying which liveBytes entry to update.
+	 */
+	void incrementEvict(DfsStreamKey key) {
+		getStat(statEvict, key).incrementAndGet();
+	}
+
+	/**
+	 * Add {@code size} to the {@code liveBytes} count.
+	 *
+	 * @param key
+	 *            key identifying which liveBytes entry to update.
+	 * @param size
+	 *            amount to increment the count by.
+	 */
+	void addToLiveBytes(DfsStreamKey key, long size) {
+		getStat(liveBytes, key).addAndGet(size);
+	}
+
+	@Override
+	public long[] getCurrentSize() {
+		return getStatVals(liveBytes);
+	}
+
+	@Override
+	public long[] getHitCount() {
+		return getStatVals(statHit);
+	}
+
+	@Override
+	public long[] getMissCount() {
+		return getStatVals(statMiss);
+	}
+
+	@Override
+	public long[] getTotalRequestCount() {
+		AtomicLong[] hit = statHit.get();
+		AtomicLong[] miss = statMiss.get();
+		long[] cnt = new long[Math.max(hit.length, miss.length)];
+		for (int i = 0; i < hit.length; i++) {
+			cnt[i] += hit[i].get();
+		}
+		for (int i = 0; i < miss.length; i++) {
+			cnt[i] += miss[i].get();
+		}
+		return cnt;
+	}
+
+	@Override
+	public long[] getHitRatio() {
+		AtomicLong[] hit = statHit.get();
+		AtomicLong[] miss = statMiss.get();
+		long[] ratio = new long[Math.max(hit.length, miss.length)];
+		for (int i = 0; i < ratio.length; i++) {
+			if (i >= hit.length) {
+				ratio[i] = 0;
+			} else if (i >= miss.length) {
+				ratio[i] = 100;
+			} else {
+				long hitVal = hit[i].get();
+				long missVal = miss[i].get();
+				long total = hitVal + missVal;
+				ratio[i] = total == 0 ? 0 : hitVal * 100 / total;
+			}
+		}
+		return ratio;
+	}
+
+	@Override
+	public long[] getEvictions() {
+		return getStatVals(statEvict);
+	}
+
+	private static AtomicLong[] newCounters() {
+		AtomicLong[] ret = new AtomicLong[PackExt.values().length];
+		for (int i = 0; i < ret.length; i++) {
+			ret[i] = new AtomicLong();
+		}
+		return ret;
+	}
+
+	private static long[] getStatVals(AtomicReference<AtomicLong[]> stat) {
+		AtomicLong[] stats = stat.get();
+		long[] cnt = new long[stats.length];
+		for (int i = 0; i < stats.length; i++) {
+			cnt[i] = stats[i].get();
+		}
+		return cnt;
+	}
+
+	private static AtomicLong getStat(AtomicReference<AtomicLong[]> stats,
+			DfsStreamKey key) {
+		int pos = key.packExtPos;
+		while (true) {
+			AtomicLong[] vals = stats.get();
+			if (pos < vals.length) {
+				return vals[pos];
+			}
+			AtomicLong[] expect = vals;
+			vals = new AtomicLong[Math.max(pos + 1, PackExt.values().length)];
+			System.arraycopy(expect, 0, vals, 0, expect.length);
+			for (int i = expect.length; i < vals.length; i++) {
+				vals[i] = new AtomicLong();
+			}
+			if (stats.compareAndSet(expect, vals)) {
+				return vals[pos];
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java
index 701d1fd..c3fd07b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTable.java
@@ -11,10 +11,7 @@
 package org.eclipse.jgit.internal.storage.dfs;
 
 import java.io.IOException;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.eclipse.jgit.internal.storage.pack.PackExt;
+import java.util.List;
 
 /**
  * Block cache table.
@@ -129,99 +126,43 @@ <T> DfsBlockCache.Ref<T> getOrLoadRef(DfsStreamKey key, long position,
 	<T> T get(DfsStreamKey key, long position);
 
 	/**
-	 * Get the DfsBlockCacheStats object for this block cache table's
-	 * statistics.
+	 * Get the list of {@link BlockCacheStats} held by this cache.
+	 * <p>
+	 * The returned list has a {@link BlockCacheStats} per configured cache
+	 * table, with a minimum of 1 {@link BlockCacheStats} object returned.
 	 *
-	 * @return the DfsBlockCacheStats tracking this block cache table's
-	 *         statistics.
+	 * Use {@link AggregatedBlockCacheStats} to combine the results of the stats
+	 * in the list for an aggregated view of the cache's stats.
+	 *
+	 * @return the list of {@link BlockCacheStats} held by this cache.
 	 */
-	DfsBlockCacheStats getDfsBlockCacheStats();
+	List<BlockCacheStats> getBlockCacheStats();
 
 	/**
-	 * Keeps track of stats for a Block Cache table.
+	 * Get the name of the table.
+	 *
+	 * @return this table's name.
 	 */
-	class DfsBlockCacheStats {
-		/**
-		 * Number of times a block was found in the cache, per pack file
-		 * extension.
-		 */
-		private final AtomicReference<AtomicLong[]> statHit;
+	String getName();
+
+	/**
+	 * Provides methods used with Block Cache statistics.
+	 */
+	interface BlockCacheStats {
 
 		/**
-		 * Number of times a block was not found, and had to be loaded, per pack
-		 * file extension.
-		 */
-		private final AtomicReference<AtomicLong[]> statMiss;
-
-		/**
-		 * Number of blocks evicted due to cache being full, per pack file
-		 * extension.
-		 */
-		private final AtomicReference<AtomicLong[]> statEvict;
-
-		/**
-		 * Number of bytes currently loaded in the cache, per pack file
-		 * extension.
-		 */
-		private final AtomicReference<AtomicLong[]> liveBytes;
-
-		DfsBlockCacheStats() {
-			statHit = new AtomicReference<>(newCounters());
-			statMiss = new AtomicReference<>(newCounters());
-			statEvict = new AtomicReference<>(newCounters());
-			liveBytes = new AtomicReference<>(newCounters());
-		}
-
-		/**
-		 * Increment the {@code statHit} count.
+		 * Get the name of the block cache generating this instance.
 		 *
-		 * @param key
-		 *            key identifying which liveBytes entry to update.
+		 * @return this cache's name.
 		 */
-		void incrementHit(DfsStreamKey key) {
-			getStat(statHit, key).incrementAndGet();
-		}
-
-		/**
-		 * Increment the {@code statMiss} count.
-		 *
-		 * @param key
-		 *            key identifying which liveBytes entry to update.
-		 */
-		void incrementMiss(DfsStreamKey key) {
-			getStat(statMiss, key).incrementAndGet();
-		}
-
-		/**
-		 * Increment the {@code statEvict} count.
-		 *
-		 * @param key
-		 *            key identifying which liveBytes entry to update.
-		 */
-		void incrementEvict(DfsStreamKey key) {
-			getStat(statEvict, key).incrementAndGet();
-		}
-
-		/**
-		 * Add {@code size} to the {@code liveBytes} count.
-		 *
-		 * @param key
-		 *            key identifying which liveBytes entry to update.
-		 * @param size
-		 *            amount to increment the count by.
-		 */
-		void addToLiveBytes(DfsStreamKey key, long size) {
-			getStat(liveBytes, key).addAndGet(size);
-		}
+		String getName();
 
 		/**
 		 * Get total number of bytes in the cache, per pack file extension.
 		 *
 		 * @return total number of bytes in the cache, per pack file extension.
 		 */
-		long[] getCurrentSize() {
-			return getStatVals(liveBytes);
-		}
+		long[] getCurrentSize();
 
 		/**
 		 * Get number of requests for items in the cache, per pack file
@@ -230,9 +171,7 @@ void addToLiveBytes(DfsStreamKey key, long size) {
 		 * @return the number of requests for items in the cache, per pack file
 		 *         extension.
 		 */
-		long[] getHitCount() {
-			return getStatVals(statHit);
-		}
+		long[] getHitCount();
 
 		/**
 		 * Get number of requests for items not in the cache, per pack file
@@ -241,9 +180,7 @@ void addToLiveBytes(DfsStreamKey key, long size) {
 		 * @return the number of requests for items not in the cache, per pack
 		 *         file extension.
 		 */
-		long[] getMissCount() {
-			return getStatVals(statMiss);
-		}
+		long[] getMissCount();
 
 		/**
 		 * Get total number of requests (hit + miss), per pack file extension.
@@ -251,42 +188,14 @@ void addToLiveBytes(DfsStreamKey key, long size) {
 		 * @return total number of requests (hit + miss), per pack file
 		 *         extension.
 		 */
-		long[] getTotalRequestCount() {
-			AtomicLong[] hit = statHit.get();
-			AtomicLong[] miss = statMiss.get();
-			long[] cnt = new long[Math.max(hit.length, miss.length)];
-			for (int i = 0; i < hit.length; i++) {
-				cnt[i] += hit[i].get();
-			}
-			for (int i = 0; i < miss.length; i++) {
-				cnt[i] += miss[i].get();
-			}
-			return cnt;
-		}
+		long[] getTotalRequestCount();
 
 		/**
 		 * Get hit ratios.
 		 *
 		 * @return hit ratios.
 		 */
-		long[] getHitRatio() {
-			AtomicLong[] hit = statHit.get();
-			AtomicLong[] miss = statMiss.get();
-			long[] ratio = new long[Math.max(hit.length, miss.length)];
-			for (int i = 0; i < ratio.length; i++) {
-				if (i >= hit.length) {
-					ratio[i] = 0;
-				} else if (i >= miss.length) {
-					ratio[i] = 100;
-				} else {
-					long hitVal = hit[i].get();
-					long missVal = miss[i].get();
-					long total = hitVal + missVal;
-					ratio[i] = total == 0 ? 0 : hitVal * 100 / total;
-				}
-			}
-			return ratio;
-		}
+		long[] getHitRatio();
 
 		/**
 		 * Get number of evictions performed due to cache being full, per pack
@@ -295,46 +204,6 @@ void addToLiveBytes(DfsStreamKey key, long size) {
 		 * @return the number of evictions performed due to cache being full,
 		 *         per pack file extension.
 		 */
-		long[] getEvictions() {
-			return getStatVals(statEvict);
-		}
-
-		private static AtomicLong[] newCounters() {
-			AtomicLong[] ret = new AtomicLong[PackExt.values().length];
-			for (int i = 0; i < ret.length; i++) {
-				ret[i] = new AtomicLong();
-			}
-			return ret;
-		}
-
-		private static long[] getStatVals(AtomicReference<AtomicLong[]> stat) {
-			AtomicLong[] stats = stat.get();
-			long[] cnt = new long[stats.length];
-			for (int i = 0; i < stats.length; i++) {
-				cnt[i] = stats[i].get();
-			}
-			return cnt;
-		}
-
-		private static AtomicLong getStat(AtomicReference<AtomicLong[]> stats,
-				DfsStreamKey key) {
-			int pos = key.packExtPos;
-			while (true) {
-				AtomicLong[] vals = stats.get();
-				if (pos < vals.length) {
-					return vals[pos];
-				}
-				AtomicLong[] expect = vals;
-				vals = new AtomicLong[Math.max(pos + 1,
-						PackExt.values().length)];
-				System.arraycopy(expect, 0, vals, 0, expect.length);
-				for (int i = expect.length; i < vals.length; i++) {
-					vals[i] = new AtomicLong();
-				}
-				if (stats.compareAndSet(expect, vals)) {
-					return vals[pos];
-				}
-			}
-		}
+		long[] getEvictions();
 	}
-}
\ No newline at end of file
+}
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 a177669..199481c 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
@@ -18,13 +18,13 @@
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
 import static org.eclipse.jgit.internal.storage.dfs.DfsPackCompactor.configureReftable;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.COMMIT_GRAPH;
-import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;
 import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -90,7 +90,7 @@ public class DfsGarbageCollector {
 	private long coalesceGarbageLimit = 50 << 20;
 	private long garbageTtlMillis = TimeUnit.DAYS.toMillis(1);
 
-	private long startTimeMillis;
+	private Instant startTime;
 	private List<DfsPackFile> packsBefore;
 	private List<DfsReftable> reftablesBefore;
 	private List<DfsPackFile> expiredGarbagePacks;
@@ -100,6 +100,7 @@ public class DfsGarbageCollector {
 	private Set<ObjectId> allTags;
 	private Set<ObjectId> nonHeads;
 	private Set<ObjectId> tagTargets;
+	private Instant refLogExpire;
 
 	/**
 	 * Initialize a garbage collector.
@@ -200,6 +201,22 @@ public DfsGarbageCollector setReftableInitialMinUpdateIndex(long u) {
 		return this;
 	}
 
+
+	/**
+	 *  Set time limit to the reflog history.
+         *  <p>
+         *  Garbage Collector prunes entries from reflog history older than {@code refLogExpire}
+         *  <p>
+	 *
+	 * @param refLogExpire
+	 *            instant in time which defines refLog expiration
+	 * @return {@code this}
+	 */
+	public DfsGarbageCollector setRefLogExpire(Instant refLogExpire) {
+		this.refLogExpire = refLogExpire;
+		return this;
+	}
+
 	/**
 	 * Set maxUpdateIndex for the initial reftable created during conversion.
 	 *
@@ -335,7 +352,7 @@ public boolean pack(ProgressMonitor pm) throws IOException {
 			throw new IllegalStateException(
 					JGitText.get().supportOnlyPackIndexVersion2);
 
-		startTimeMillis = SystemReader.getInstance().getCurrentTime();
+		startTime = SystemReader.getInstance().now();
 		ctx = objdb.newReader();
 		try {
 			refdb.refresh();
@@ -418,7 +435,7 @@ private void readPacksBefore() throws IOException {
 		packsBefore = new ArrayList<>(packs.length);
 		expiredGarbagePacks = new ArrayList<>(packs.length);
 
-		long now = SystemReader.getInstance().getCurrentTime();
+		long now = SystemReader.getInstance().now().toEpochMilli();
 		for (DfsPackFile p : packs) {
 			DfsPackDescription d = p.getPackDescription();
 			if (d.getPackSource() != UNREACHABLE_GARBAGE) {
@@ -687,14 +704,7 @@ private DfsPackDescription writePack(PackSource source, PackWriter pw,
 			pack.setBlockSize(PACK, out.blockSize());
 		}
 
-		try (DfsOutputStream out = objdb.writeFile(pack, INDEX)) {
-			CountingOutputStream cnt = new CountingOutputStream(out);
-			pw.writeIndex(cnt);
-			pack.addFileExt(INDEX);
-			pack.setFileSize(INDEX, cnt.getCount());
-			pack.setBlockSize(INDEX, out.blockSize());
-			pack.setIndexVersion(pw.getIndexVersion());
-		}
+		pw.writeIndex(objdb.getPackIndexWriter(pack, pw.getIndexVersion()));
 
 		if (source != UNREACHABLE_GARBAGE && packConfig.getMinBytesForObjSizeIndex() >= 0) {
 			try (DfsOutputStream out = objdb.writeFile(pack,
@@ -713,7 +723,7 @@ private DfsPackDescription writePack(PackSource source, PackWriter pw,
 
 		PackStatistics stats = pw.getStatistics();
 		pack.setPackStats(stats);
-		pack.setLastModified(startTimeMillis);
+		pack.setLastModified(startTime.toEpochMilli());
 		newPackDesc.add(pack);
 		newPackStats.add(stats);
 		newPackObj.add(pw.getObjectSet());
@@ -741,6 +751,10 @@ private void writeReftable(DfsPackDescription pack) throws IOException {
 			compact.addAll(stack.readers());
 			compact.setIncludeDeletes(includeDeletes);
 			compact.setConfig(configureReftable(reftableConfig, out));
+			if(refLogExpire != null ){
+				compact.setReflogExpireOldestReflogTimeMillis(
+						refLogExpire.toEpochMilli());
+			}
 			compact.compact();
 			pack.addFileExt(REFTABLE);
 			pack.setReftableStats(compact.getStats());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java
index a07d841..16315bf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java
@@ -41,12 +41,13 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter;
 import org.eclipse.jgit.internal.storage.file.PackIndex;
-import org.eclipse.jgit.internal.storage.file.PackIndexWriter;
 import org.eclipse.jgit.internal.storage.file.PackObjectSizeIndexWriter;
 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.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdOwnerMap;
@@ -54,7 +55,6 @@
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.ObjectStream;
-import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.PackedObjectInfo;
 import org.eclipse.jgit.util.BlockList;
 import org.eclipse.jgit.util.IO;
@@ -71,6 +71,8 @@ public class DfsInserter extends ObjectInserter {
 	private static final int INDEX_VERSION = 2;
 
 	final DfsObjDatabase db;
+
+	private final int minBytesForObjectSizeIndex;
 	int compression = Deflater.BEST_COMPRESSION;
 
 	List<PackedObjectInfo> objectList;
@@ -83,8 +85,6 @@ public class DfsInserter extends ObjectInserter {
 	private boolean rollback;
 	private boolean checkExisting = true;
 
-	private int minBytesForObjectSizeIndex = -1;
-
 	/**
 	 * Initialize a new inserter.
 	 *
@@ -93,8 +93,9 @@ public class DfsInserter extends ObjectInserter {
 	 */
 	protected DfsInserter(DfsObjDatabase db) {
 		this.db = db;
-		PackConfig pc = new PackConfig(db.getRepository().getConfig());
-		this.minBytesForObjectSizeIndex = pc.getMinBytesForObjSizeIndex();
+		this.minBytesForObjectSizeIndex = db.getRepository().getConfig().getInt(
+				ConfigConstants.CONFIG_PACK_SECTION,
+				ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, -1);
 	}
 
 	/**
@@ -112,21 +113,6 @@ public void checkExisting(boolean check) {
 	void setCompressionLevel(int compression) {
 		this.compression = compression;
 	}
-
-	/**
-	 * Set minimum size for an object to be included in the object size index.
-	 *
-	 * <p>
-	 * Use 0 for all and -1 for nothing (the pack won't have object size index).
-	 *
-	 * @param minBytes
-	 *            only objects with size bigger or equal to this are included in
-	 *            the index.
-	 */
-	protected void setMinBytesForObjectSizeIndex(int minBytes) {
-		this.minBytesForObjectSizeIndex = minBytes;
-	}
-
 	@Override
 	public DfsPackParser newPackParser(InputStream in) throws IOException {
 		return new DfsPackParser(db, this, in);
@@ -333,7 +319,7 @@ PackIndex writePackIndex(DfsPackDescription pack, byte[] packHash,
 
 	private static void index(OutputStream out, byte[] packHash,
 			List<PackedObjectInfo> list) throws IOException {
-		PackIndexWriter.createVersion(out, INDEX_VERSION).write(list, packHash);
+		BasePackIndexWriter.createVersion(out, INDEX_VERSION).write(list, packHash);
 	}
 
 	void writeObjectSizeIndex(DfsPackDescription pack,
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 616563f..efd666f 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
@@ -12,6 +12,7 @@
 
 import static java.util.stream.Collectors.joining;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -27,7 +28,9 @@
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter;
 import org.eclipse.jgit.internal.storage.file.PackBitmapIndexWriterV1;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.internal.storage.pack.PackBitmapIndexWriter;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -771,4 +774,35 @@ public PackBitmapIndexWriter getPackBitmapIndexWriter(
 			}
 		};
 	}
+
+	/**
+	 * Returns a writer to store the pack index in this object database.
+	 *
+	 * @param pack
+	 *            Pack file to which the index is associated.
+	 * @param indexVersion
+	 *            which version of the index to write
+	 * @return a writer to store the index associated with the pack
+	 * @throws IOException
+	 *             when some I/O problem occurs while creating or writing to
+	 *             output stream
+	 */
+	public PackIndexWriter getPackIndexWriter(
+			DfsPackDescription pack, int indexVersion)
+			throws IOException {
+		return (objectsToStore, packDataChecksum) -> {
+			try (DfsOutputStream out = writeFile(pack, INDEX);
+					CountingOutputStream cnt = new CountingOutputStream(out)) {
+				final PackIndexWriter iw = BasePackIndexWriter
+						.createVersion(cnt,
+						indexVersion);
+				iw.write(objectsToStore, packDataChecksum);
+				pack.addFileExt(INDEX);
+				pack.setFileSize(INDEX, cnt.getCount());
+				pack.setBlockSize(INDEX, out.blockSize());
+				pack.setIndexVersion(indexVersion);
+			}
+		};
+	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
index 86144b3..f9c01b9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
@@ -12,7 +12,7 @@
 
 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.pack.PackExt.INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;
 import static org.eclipse.jgit.internal.storage.pack.StoredObjectRepresentation.PACK_DELTA;
@@ -249,6 +249,7 @@ private void compactPacks(DfsReader ctx, ProgressMonitor pm)
 			try {
 				writePack(objdb, outDesc, pw, pm);
 				writeIndex(objdb, outDesc, pw);
+				writeObjectSizeIndex(objdb, outDesc, pw);
 
 				PackStatistics stats = pw.getStatistics();
 
@@ -458,13 +459,20 @@ private static void writePack(DfsObjDatabase objdb,
 	private static void writeIndex(DfsObjDatabase objdb,
 			DfsPackDescription pack,
 			PackWriter pw) throws IOException {
-		try (DfsOutputStream out = objdb.writeFile(pack, INDEX)) {
+		pw.writeIndex(objdb.getPackIndexWriter(pack, pw.getIndexVersion()));
+	}
+
+	private static void writeObjectSizeIndex(DfsObjDatabase objdb,
+											 DfsPackDescription pack,
+											 PackWriter pw) throws IOException {
+		try (DfsOutputStream out = objdb.writeFile(pack, OBJECT_SIZE_INDEX)) {
 			CountingOutputStream cnt = new CountingOutputStream(out);
-			pw.writeIndex(cnt);
-			pack.addFileExt(INDEX);
-			pack.setFileSize(INDEX, cnt.getCount());
-			pack.setBlockSize(INDEX, out.blockSize());
-			pack.setIndexVersion(pw.getIndexVersion());
+			pw.writeObjectSizeIndex(cnt);
+			if (cnt.getCount() > 0) {
+				pack.addFileExt(OBJECT_SIZE_INDEX);
+				pack.setFileSize(OBJECT_SIZE_INDEX, cnt.getCount());
+				pack.setBlockSize(OBJECT_SIZE_INDEX, out.blockSize());
+			}
 		}
 	}
 
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 5cc2a57..48ed47a 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
@@ -29,6 +29,7 @@
 import java.text.MessageFormat;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.zip.CRC32;
 import java.util.zip.DataFormatException;
 import java.util.zip.Inflater;
@@ -106,6 +107,53 @@ public final class DfsPackFile extends BlockBasedFile {
 	/** Lock for {@link #corruptObjects}. */
 	private final Object corruptObjectsLock = new Object();
 
+	private final IndexFactory indexFactory;
+
+	/**
+	 * Returns the indexes for this pack.
+	 * <p>
+	 * We define indexes in different sub interfaces to allow implementing the
+	 * indexes over different combinations of backends.
+	 * <p>
+	 * Implementations decide if/how to cache the indexes. The calling
+	 * DfsPackFile will keep the reference to the index as long as it needs it.
+	 */
+	public interface IndexFactory {
+		/**
+		 * Take care of loading the primary and reverse indexes for this pack.
+		 */
+		interface PackIndexes {
+			/**
+			 * Load the primary index for the pack.
+			 *
+			 * @param ctx
+			 *            reader to find the raw bytes
+			 * @return a primary index
+			 * @throws IOException
+			 *             a problem finding/parsing the index
+			 */
+			PackIndex index(DfsReader ctx) throws IOException;
+
+			/**
+			 * Load the reverse index of the pack
+			 *
+			 * @param ctx
+			 *            reader to find the raw bytes
+			 * @return the reverse index of the pack
+			 * @throws IOException
+			 *             a problem finding/parsing the reverse index
+			 */
+			PackReverseIndex reverseIndex(DfsReader ctx) throws IOException;
+		}
+
+		/**
+		 * Returns a provider of the primary and reverse indexes of this pack
+		 *
+		 * @return an implementation of the {@link PackIndexes} interface
+		 */
+		PackIndexes getPackIndexes();
+	}
+
 	/**
 	 * Construct a reader for an existing, packfile.
 	 *
@@ -115,7 +163,8 @@ public final class DfsPackFile extends BlockBasedFile {
 	 *            description of the pack within the DFS.
 	 */
 	DfsPackFile(DfsBlockCache cache, DfsPackDescription desc) {
-		this(cache, desc, DEFAULT_BITMAP_LOADER);
+		this(cache, desc, DEFAULT_BITMAP_LOADER,
+				new CachedStreamIndexFactory(cache, desc));
 	}
 
 	/**
@@ -127,9 +176,11 @@ public final class DfsPackFile extends BlockBasedFile {
 	 *            description of the pack within the DFS
 	 * @param bitmapLoader
 	 *            loader to get the bitmaps of this pack (if any)
+	 * @param indexFactory
+	 *            an IndexFactory to get references to the indexes of this pack
 	 */
 	public DfsPackFile(DfsBlockCache cache, DfsPackDescription desc,
-			PackBitmapIndexLoader bitmapLoader) {
+			PackBitmapIndexLoader bitmapLoader, IndexFactory indexFactory) {
 		super(cache, desc, PACK);
 
 		int bs = desc.getBlockSize(PACK);
@@ -141,6 +192,7 @@ public DfsPackFile(DfsBlockCache cache, DfsPackDescription desc,
 		length = sz > 0 ? sz : -1;
 
 		this.bitmapLoader = bitmapLoader;
+		this.indexFactory = indexFactory;
 	}
 
 	/**
@@ -195,19 +247,10 @@ private PackIndex idx(DfsReader ctx) throws IOException {
 		Repository.getGlobalListenerList()
 				.dispatch(new BeforeDfsPackIndexLoadedEvent(this));
 		try {
-			DfsStreamKey idxKey = desc.getStreamKey(INDEX);
-			AtomicBoolean cacheHit = new AtomicBoolean(true);
-			DfsBlockCache.Ref<PackIndex> idxref = cache.getOrLoadRef(idxKey,
-					REF_POSITION, () -> {
-						cacheHit.set(false);
-						return loadPackIndex(ctx, idxKey);
-					});
-			if (cacheHit.get()) {
-				ctx.stats.idxCacheHit++;
-			}
-			PackIndex idx = idxref.get();
-			if (index == null && idx != null) {
-				index = idx;
+			index = indexFactory.getPackIndexes().index(ctx);
+			if (index == null) {
+				throw new IOException(
+						"Couldn't get a reference to the primary index"); //$NON-NLS-1$
 			}
 			ctx.emitIndexLoad(desc, INDEX, index);
 			return index;
@@ -321,20 +364,10 @@ public PackReverseIndex getReverseIdx(DfsReader ctx) throws IOException {
 			return reverseIndex;
 		}
 
-		PackIndex idx = idx(ctx);
-		DfsStreamKey revKey = desc.getStreamKey(REVERSE_INDEX);
-		AtomicBoolean cacheHit = new AtomicBoolean(true);
-		DfsBlockCache.Ref<PackReverseIndex> revref = cache.getOrLoadRef(revKey,
-				REF_POSITION, () -> {
-					cacheHit.set(false);
-					return loadReverseIdx(ctx, revKey, idx);
-				});
-		if (cacheHit.get()) {
-			ctx.stats.ridxCacheHit++;
-		}
-		PackReverseIndex revidx = revref.get();
-		if (reverseIndex == null && revidx != null) {
-			reverseIndex = revidx;
+		reverseIndex = indexFactory.getPackIndexes().reverseIndex(ctx);
+		if (reverseIndex == null) {
+			throw new IOException(
+					"Couldn't get a reference to the reverse index"); //$NON-NLS-1$
 		}
 		ctx.emitIndexLoad(desc, REVERSE_INDEX, reverseIndex);
 		return reverseIndex;
@@ -347,6 +380,7 @@ private PackObjectSizeIndex getObjectSizeIndex(DfsReader ctx)
 		}
 
 		if (objectSizeIndexLoadAttempted
+				|| !ctx.getOptions().shouldUseObjectSizeIndex()
 				|| !desc.hasFileExt(OBJECT_SIZE_INDEX)) {
 			// Pack doesn't have object size index
 			return null;
@@ -1210,48 +1244,6 @@ private void setCorrupt(long offset) {
 		}
 	}
 
-	private DfsBlockCache.Ref<PackIndex> loadPackIndex(
-			DfsReader ctx, DfsStreamKey idxKey) throws IOException {
-		try {
-			ctx.stats.readIdx++;
-			long start = System.nanoTime();
-			try (ReadableChannel rc = ctx.db.openFile(desc, INDEX)) {
-				PackIndex idx = PackIndex.read(alignTo8kBlocks(rc));
-				ctx.stats.readIdxBytes += rc.position();
-				index = idx;
-				return new DfsBlockCache.Ref<>(
-						idxKey,
-						REF_POSITION,
-						idx.getObjectCount() * REC_SIZE,
-						idx);
-			} finally {
-				ctx.stats.readIdxMicros += elapsedMicros(start);
-			}
-		} catch (EOFException e) {
-			throw new IOException(MessageFormat.format(
-					DfsText.get().shortReadOfIndex,
-					desc.getFileName(INDEX)), e);
-		} catch (IOException e) {
-			throw new IOException(MessageFormat.format(
-					DfsText.get().cannotReadIndex,
-					desc.getFileName(INDEX)), e);
-		}
-	}
-
-	private DfsBlockCache.Ref<PackReverseIndex> loadReverseIdx(
-			DfsReader ctx, DfsStreamKey revKey, PackIndex idx) {
-		ctx.stats.readReverseIdx++;
-		long start = System.nanoTime();
-		PackReverseIndex revidx = PackReverseIndexFactory.computeFromIndex(idx);
-		reverseIndex = revidx;
-		ctx.stats.readReverseIdxMicros += elapsedMicros(start);
-		return new DfsBlockCache.Ref<>(
-				revKey,
-				REF_POSITION,
-				idx.getObjectCount() * 8,
-				revidx);
-	}
-
 	private DfsBlockCache.Ref<PackObjectSizeIndex> loadObjectSizeIndex(
 			DfsReader ctx, DfsStreamKey objectSizeIndexKey) throws IOException {
 		ctx.stats.readObjectSizeIndex++;
@@ -1288,9 +1280,12 @@ private DfsBlockCache.Ref<PackObjectSizeIndex> loadObjectSizeIndex(
 	private DfsBlockCache.Ref<PackBitmapIndex> loadBitmapIndex(DfsReader ctx,
 			DfsStreamKey bitmapKey) throws IOException {
 		ctx.stats.readBitmap++;
+		long start = System.nanoTime();
 		PackBitmapIndexLoader.LoadResult result = bitmapLoader
 				.loadPackBitmapIndex(ctx, this);
 		bitmapIndex = result.bitmapIndex;
+		ctx.stats.readBitmapIdxBytes += result.bytesRead;
+		ctx.stats.readBitmapIdxMicros += elapsedMicros(start);
 		return new DfsBlockCache.Ref<>(bitmapKey, REF_POSITION,
 				result.bytesRead, result.bitmapIndex);
 	}
@@ -1449,4 +1444,141 @@ public LoadResult loadPackBitmapIndex(DfsReader ctx, DfsPackFile pack)
 			}
 		}
 	}
+
+	/**
+	 * An index factory backed by Dfs streams and references cached in
+	 * DfsBlockCache
+	 */
+	public static final class CachedStreamIndexFactory implements IndexFactory {
+		private final CachedStreamPackIndexes indexes;
+
+		/**
+		 * An index factory
+		 *
+		 * @param cache
+		 *            DFS block cache to use for the references
+		 * @param desc
+		 *            This factory loads indexes for this package
+		 */
+		public CachedStreamIndexFactory(DfsBlockCache cache,
+				DfsPackDescription desc) {
+			this.indexes = new CachedStreamPackIndexes(cache, desc);
+		}
+
+		@Override
+		public PackIndexes getPackIndexes() {
+			return indexes;
+		}
+	}
+
+	/**
+	 * Load primary and reverse index from Dfs streams and cache the references
+	 * in DfsBlockCache.
+	 */
+	public static final class CachedStreamPackIndexes implements IndexFactory.PackIndexes {
+		private final DfsBlockCache cache;
+
+		private final DfsPackDescription desc;
+
+		/**
+		 * An index factory
+		 *
+		 * @param cache
+		 *            DFS block cache to use for the references
+		 * @param desc This factory loads indexes for this package
+		 */
+		public CachedStreamPackIndexes(DfsBlockCache cache,
+									   DfsPackDescription desc) {
+			this.cache = cache;
+			this.desc = desc;
+		}
+
+		@Override
+		public PackIndex index(DfsReader ctx) throws IOException {
+			DfsStreamKey idxKey = desc.getStreamKey(INDEX);
+			// Keep the value parsed in the loader, in case the Ref<> is
+			// nullified in ClockBlockCacheTable#reserveSpace
+			// before we read its value.
+			AtomicReference<PackIndex> loadedRef = new AtomicReference<>(null);
+			DfsBlockCache.Ref<PackIndex> cachedRef = cache.getOrLoadRef(idxKey,
+					REF_POSITION, () -> {
+						RefWithSize<PackIndex> idx = loadPackIndex(ctx, desc);
+						loadedRef.set(idx.ref);
+						return new DfsBlockCache.Ref<>(idxKey, REF_POSITION,
+								idx.size, idx.ref);
+					});
+			if (loadedRef.get() == null) {
+				ctx.stats.idxCacheHit++;
+			}
+			return cachedRef.get() != null ? cachedRef.get() : loadedRef.get();
+		}
+
+		private static RefWithSize<PackIndex> loadPackIndex(DfsReader ctx,
+				DfsPackDescription desc) throws IOException {
+			try {
+				ctx.stats.readIdx++;
+				long start = System.nanoTime();
+				try (ReadableChannel rc = ctx.db.openFile(desc, INDEX)) {
+					PackIndex idx = PackIndex.read(alignTo8kBlocks(rc));
+					ctx.stats.readIdxBytes += rc.position();
+					return new RefWithSize<>(idx,
+							idx.getObjectCount() * REC_SIZE);
+				} finally {
+					ctx.stats.readIdxMicros += elapsedMicros(start);
+				}
+			} catch (EOFException e) {
+				throw new IOException(
+						MessageFormat.format(DfsText.get().shortReadOfIndex,
+								desc.getFileName(INDEX)),
+						e);
+			} catch (IOException e) {
+				throw new IOException(
+						MessageFormat.format(DfsText.get().cannotReadIndex,
+								desc.getFileName(INDEX)),
+						e);
+			}
+		}
+
+		@Override
+		public PackReverseIndex reverseIndex(DfsReader ctx) throws IOException {
+			PackIndex idx = index(ctx);
+			DfsStreamKey revKey = desc.getStreamKey(REVERSE_INDEX);
+			// Keep the value parsed in the loader, in case the Ref<> is
+			// nullified in ClockBlockCacheTable#reserveSpace
+			// before we read its value.
+			AtomicReference<PackReverseIndex> loadedRef = new AtomicReference<>(
+					null);
+			DfsBlockCache.Ref<PackReverseIndex> cachedRef = cache
+					.getOrLoadRef(revKey, REF_POSITION, () -> {
+						RefWithSize<PackReverseIndex> ridx = loadReverseIdx(ctx,
+								idx);
+						loadedRef.set(ridx.ref);
+						return new DfsBlockCache.Ref<>(revKey, REF_POSITION,
+								ridx.size, ridx.ref);
+					});
+			if (loadedRef.get() == null) {
+				ctx.stats.ridxCacheHit++;
+			}
+			return cachedRef.get() != null ? cachedRef.get() : loadedRef.get();
+		}
+
+		private static RefWithSize<PackReverseIndex> loadReverseIdx(
+				DfsReader ctx, PackIndex idx) {
+			ctx.stats.readReverseIdx++;
+			long start = System.nanoTime();
+			PackReverseIndex revidx = PackReverseIndexFactory
+					.computeFromIndex(idx);
+			ctx.stats.readReverseIdxMicros += elapsedMicros(start);
+			return new RefWithSize<>(revidx, idx.getObjectCount() * 8);
+		}
+	}
+
+	private static final class RefWithSize<V> {
+		final V ref;
+		final long size;
+		RefWithSize(V ref, long size) {
+			this.ref = ref;
+			this.size = size;
+		}
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
index c939114..62f6753 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
@@ -307,7 +307,7 @@ private static class FoundObject<T extends ObjectId> {
 
 	private <T extends ObjectId> Iterable<FoundObject<T>> findAll(
 			Iterable<T> objectIds) throws IOException {
-		Collection<T> pending = new ArrayList<>();
+		HashSet<T> pending = new HashSet<>();
 		for (T id : objectIds) {
 			pending.add(id);
 		}
@@ -327,22 +327,21 @@ private <T extends ObjectId> Iterable<FoundObject<T>> findAll(
 	}
 
 	private <T extends ObjectId> void findAllImpl(PackList packList,
-			Collection<T> pending, List<FoundObject<T>> r) {
+			HashSet<T> pending, List<FoundObject<T>> r) {
 		DfsPackFile[] packs = packList.packs;
 		if (packs.length == 0) {
 			return;
 		}
 		int lastIdx = 0;
 		DfsPackFile lastPack = packs[lastIdx];
-
-		OBJECT_SCAN: for (Iterator<T> it = pending.iterator(); it.hasNext();) {
-			T t = it.next();
+		HashSet<T> toRemove = new HashSet<>();
+		OBJECT_SCAN: for (T t : pending) {
 			if (!skipGarbagePack(lastPack)) {
 				try {
 					long p = lastPack.findOffset(this, t);
 					if (0 < p) {
 						r.add(new FoundObject<>(t, lastIdx, lastPack, p));
-						it.remove();
+						toRemove.add(t);
 						continue;
 					}
 				} catch (IOException e) {
@@ -360,7 +359,7 @@ private <T extends ObjectId> void findAllImpl(PackList packList,
 					long p = pack.findOffset(this, t);
 					if (0 < p) {
 						r.add(new FoundObject<>(t, i, pack, p));
-						it.remove();
+						toRemove.add(t);
 						lastIdx = i;
 						lastPack = pack;
 						continue OBJECT_SCAN;
@@ -370,6 +369,7 @@ private <T extends ObjectId> void findAllImpl(PackList packList,
 				}
 			}
 		}
+		pending.removeAll(toRemove);
 
 		last = lastPack;
 	}
@@ -511,18 +511,15 @@ public long getObjectSize(AnyObjectId objectId, int typeHint)
 			throw new MissingObjectException(objectId.copy(), typeHint);
 		}
 
-		if (typeHint != Constants.OBJ_BLOB || !pack.hasObjectSizeIndex(this)) {
+		if (typeHint != Constants.OBJ_BLOB || !safeHasObjectSizeIndex(pack)) {
 			return pack.getObjectSize(this, objectId);
 		}
 
-		long sz = pack.getIndexedObjectSize(this, objectId);
+		Optional<Long> maybeSz = safeGetIndexedObjectSize(pack, objectId);
+		long sz = maybeSz.orElse(-1L);
 		if (sz >= 0) {
-			stats.objectSizeIndexHit += 1;
 			return sz;
 		}
-
-		// Object wasn't in the index
-		stats.objectSizeIndexMiss += 1;
 		return pack.getObjectSize(this, objectId);
 	}
 
@@ -541,23 +538,61 @@ public boolean isNotLargerThan(AnyObjectId objectId, int typeHint,
 		}
 
 		stats.isNotLargerThanCallCount += 1;
-		if (typeHint != Constants.OBJ_BLOB || !pack.hasObjectSizeIndex(this)) {
+		if (typeHint != Constants.OBJ_BLOB || !safeHasObjectSizeIndex(pack)) {
 			return pack.getObjectSize(this, objectId) <= limit;
 		}
 
-		long sz = pack.getIndexedObjectSize(this, objectId);
+		Optional<Long> maybeSz = safeGetIndexedObjectSize(pack, objectId);
+		if (maybeSz.isEmpty()) {
+			// Exception in object size index
+			return pack.getObjectSize(this, objectId) <= limit;
+		}
+
+		long sz = maybeSz.get();
+		if (sz >= 0) {
+			return sz <= limit;
+		}
+
+		if (isLimitInsideIndexThreshold(pack, limit)) {
+			// With threshold T, not-found means object < T
+			// If limit L > T, then object < T < L
+			return true;
+		}
+
+		return pack.getObjectSize(this, objectId) <= limit;
+	}
+
+	private boolean safeHasObjectSizeIndex(DfsPackFile pack) {
+		try {
+			return pack.hasObjectSizeIndex(this);
+		} catch (IOException e) {
+			return false;
+		}
+	}
+
+	private Optional<Long> safeGetIndexedObjectSize(DfsPackFile pack,
+			AnyObjectId objectId) {
+		long sz;
+		try {
+			sz = pack.getIndexedObjectSize(this, objectId);
+		} catch (IOException e) {
+			// Do not count the exception as an index miss
+			return Optional.empty();
+		}
 		if (sz < 0) {
 			stats.objectSizeIndexMiss += 1;
 		} else {
 			stats.objectSizeIndexHit += 1;
 		}
+		return Optional.of(sz);
+	}
 
-		// Got size from index or we didn't but we are sure it should be there.
-		if (sz >= 0 || pack.getObjectSizeIndexThreshold(this) <= limit) {
-			return sz <= limit;
+	private boolean isLimitInsideIndexThreshold(DfsPackFile pack, long limit) {
+		try {
+			return pack.getObjectSizeIndexThreshold(this) <= limit;
+		} catch (IOException e) {
+			return false;
 		}
-
-		return pack.getObjectSize(this, objectId) <= limit;
 	}
 
 	private DfsPackFile findPackWithObject(AnyObjectId objectId)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java
index adb4673..fcfa3e0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java
@@ -40,12 +40,12 @@ public static class Accumulator {
 		/** Total number of complete pack indexes read into memory. */
 		long readIdx;
 
-		/** Total number of complete bitmap indexes read into memory. */
-		long readBitmap;
-
 		/** Total number of reverse indexes added into memory. */
 		long readReverseIdx;
 
+		/** Total number of complete bitmap indexes read into memory. */
+		long readBitmap;
+
 		/** Total number of complete commit graphs read into memory. */
 		long readCommitGraph;
 
@@ -55,6 +55,9 @@ public static class Accumulator {
 		/** Total number of bytes read from pack indexes. */
 		long readIdxBytes;
 
+		/** Total number of bytes read from bitmap indexes. */
+		long readBitmapIdxBytes;
+
 		/** Total number of bytes read from commit graphs. */
 		long readCommitGraphBytes;
 
@@ -67,18 +70,15 @@ public static class Accumulator {
 		/** Total microseconds spent creating reverse indexes. */
 		long readReverseIdxMicros;
 
+		/** Total microseconds spent reading bitmap indexes. */
+		long readBitmapIdxMicros;
+
 		/** Total microseconds spent creating commit graphs. */
 		long readCommitGraphMicros;
 
 		/** Total microseconds spent creating object size indexes */
 		long readObjectSizeIndexMicros;
 
-		/** Total number of bytes read from bitmap indexes. */
-		long readBitmapIdxBytes;
-
-		/** Total microseconds spent reading bitmap indexes. */
-		long readBitmapIdxMicros;
-
 		/** Total number of block cache hits. */
 		long blockCacheHit;
 
@@ -195,15 +195,6 @@ public long getReadReverseIndexCount() {
 	}
 
 	/**
-	 * Get total number of times the commit graph read into memory.
-	 *
-	 * @return total number of commit graph read into memory.
-	 */
-	public long getReadCommitGraphCount() {
-		return stats.readCommitGraph;
-	}
-
-	/**
 	 * Get total number of complete bitmap indexes read into memory.
 	 *
 	 * @return total number of complete bitmap indexes read into memory.
@@ -213,6 +204,15 @@ public long getReadBitmapIndexCount() {
 	}
 
 	/**
+	 * Get total number of times the commit graph read into memory.
+	 *
+	 * @return total number of commit graph read into memory.
+	 */
+	public long getReadCommitGraphCount() {
+		return stats.readCommitGraph;
+	}
+
+	/**
 	 * Get total number of complete object size indexes read into memory.
 	 *
 	 * @return total number of complete object size indexes read into memory.
@@ -231,6 +231,15 @@ public long getReadIndexBytes() {
 	}
 
 	/**
+	 * Get total number of bytes read from bitmap indexes.
+	 *
+	 * @return total number of bytes read from bitmap indexes.
+	 */
+	public long getReadBitmapIndexBytes() {
+		return stats.readBitmapIdxBytes;
+	}
+
+	/**
 	 * Get total number of bytes read from commit graphs.
 	 *
 	 * @return total number of bytes read from commit graphs.
@@ -240,6 +249,15 @@ public long getCommitGraphBytes() {
 	}
 
 	/**
+	 * Get total number of bytes read from object size indexes.
+	 *
+	 * @return total number of bytes read from object size indexes.
+	 */
+	public long getObjectSizeIndexBytes() {
+		return stats.readObjectSizeIndexBytes;
+	}
+
+	/**
 	 * Get total microseconds spent reading pack indexes.
 	 *
 	 * @return total microseconds spent reading pack indexes.
@@ -258,6 +276,15 @@ public long getReadReverseIndexMicros() {
 	}
 
 	/**
+	 * Get total microseconds spent reading bitmap indexes.
+	 *
+	 * @return total microseconds spent reading bitmap indexes.
+	 */
+	public long getReadBitmapIndexMicros() {
+		return stats.readBitmapIdxMicros;
+	}
+
+	/**
 	 * Get total microseconds spent reading commit graphs.
 	 *
 	 * @return total microseconds spent reading commit graphs.
@@ -267,21 +294,12 @@ public long getReadCommitGraphMicros() {
 	}
 
 	/**
-	 * Get total number of bytes read from bitmap indexes.
+	 * Get total microseconds spent reading object size indexes.
 	 *
-	 * @return total number of bytes read from bitmap indexes.
+	 * @return total microseconds spent reading object size indexes.
 	 */
-	public long getReadBitmapIndexBytes() {
-		return stats.readBitmapIdxBytes;
-	}
-
-	/**
-	 * Get total microseconds spent reading bitmap indexes.
-	 *
-	 * @return total microseconds spent reading bitmap indexes.
-	 */
-	public long getReadBitmapIndexMicros() {
-		return stats.readBitmapIdxMicros;
+	public long getReadObjectSizeIndexMicros() {
+		return stats.readObjectSizeIndexMicros;
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java
index f2ac461..c397469 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java
@@ -13,8 +13,10 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_DELTA_BASE_CACHE_LIMIT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_LOAD_REV_INDEX_IN_PARALLEL;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_BUFFER;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_FILE_THRESHOLD;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_USE_OBJECT_SIZE_INDEX;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.pack.PackConfig;
@@ -36,6 +38,8 @@ public class DfsReaderOptions {
 
 	private boolean loadRevIndexInParallel;
 
+	private boolean useObjectSizeIndex;
+
 	/**
 	 * Create a default reader configuration.
 	 */
@@ -137,6 +141,28 @@ public DfsReaderOptions setLoadRevIndexInParallel(
 	}
 
 	/**
+	 * Use the object size index if available.
+	 *
+	 * @return true if the reader should try to use the object size index. if
+	 *         false, the reader ignores that index.
+	 */
+	public boolean shouldUseObjectSizeIndex() {
+		return useObjectSizeIndex;
+	}
+
+	/**
+	 * Set if the reader should try to use the object size index
+	 *
+	 * @param useObjectSizeIndex true to use it, false to ignore the object size index
+	 *
+	 * @return {@code this}
+	 */
+	public DfsReaderOptions setUseObjectSizeIndex(boolean useObjectSizeIndex) {
+		this.useObjectSizeIndex = useObjectSizeIndex;
+		return this;
+	}
+
+	/**
 	 * Update properties by setting fields from the configuration.
 	 * <p>
 	 * If a property is not defined in the configuration, then it is left
@@ -168,6 +194,13 @@ public DfsReaderOptions fromConfig(Config rc) {
 				CONFIG_DFS_SECTION,
 				CONFIG_KEY_STREAM_BUFFER,
 				getStreamPackBufferSize()));
+
+		setUseObjectSizeIndex(rc.getBoolean(CONFIG_CORE_SECTION,
+				CONFIG_DFS_SECTION, CONFIG_KEY_USE_OBJECT_SIZE_INDEX,
+				false));
+		setLoadRevIndexInParallel(
+				rc.getBoolean(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+						CONFIG_KEY_LOAD_REV_INDEX_IN_PARALLEL, false));
 		return this;
 	}
 }
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 3ba74b2..2751cd2 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
@@ -28,6 +28,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.RefList;
@@ -177,6 +178,11 @@ public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> exclu
 	}
 
 	@Override
+	public ReflogReader getReflogReader(Ref ref) throws IOException {
+		return reftableDatabase.getReflogReader(ref.getName());
+	}
+
+	@Override
 	public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException {
 		if (!getReftableConfig().isIndexObjects()) {
 			return super.getTipsWithSha1(id);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java
new file mode 100644
index 0000000..bb44f93
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTable.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2024, Google LLC 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
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.ReadableChannelSupplier;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.Ref;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.RefLoader;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+
+/**
+ * A table that holds multiple cache tables accessed by {@link PackExt} types.
+ *
+ * <p>
+ * Allows the separation of entries from different {@link PackExt} types to
+ * limit churn in cache caused by entries of differing sizes.
+ * <p>
+ * Separating these tables enables the fine-tuning of cache tables per extension
+ * type.
+ */
+class PackExtBlockCacheTable implements DfsBlockCacheTable {
+	/**
+	 * Table name.
+	 */
+	private final String name;
+
+	private final DfsBlockCacheTable defaultBlockCacheTable;
+
+	// Holds the unique tables backing the extBlockCacheTables values.
+	private final List<DfsBlockCacheTable> blockCacheTableList;
+
+	// Holds the mapping of PackExt to DfsBlockCacheTables.
+	// The relation between the size of extBlockCacheTables entries and
+	// blockCacheTableList entries is:
+	// blockCacheTableList.size() <= extBlockCacheTables.size()
+	private final Map<PackExt, DfsBlockCacheTable> extBlockCacheTables;
+
+	/**
+	 * Builds the PackExtBlockCacheTable from a list of
+	 * {@link DfsBlockCachePackExtConfig}s.
+	 *
+	 * @param cacheConfig
+	 *            {@link DfsBlockCacheConfig} containing
+	 *            {@link DfsBlockCachePackExtConfig}s used to configure
+	 *            PackExtBlockCacheTable. The {@link DfsBlockCacheConfig} holds
+	 *            the configuration for the default cache table.
+	 * @return the cache table built from the given configs.
+	 * @throws IllegalArgumentException
+	 *             when no {@link DfsBlockCachePackExtConfig} exists in the
+	 *             {@link DfsBlockCacheConfig}.
+	 */
+	static PackExtBlockCacheTable fromBlockCacheConfigs(
+			DfsBlockCacheConfig cacheConfig) {
+		DfsBlockCacheTable defaultTable = new ClockBlockCacheTable(cacheConfig);
+		Map<PackExt, DfsBlockCacheTable> packExtBlockCacheTables = new HashMap<>();
+		List<DfsBlockCachePackExtConfig> packExtConfigs = cacheConfig
+				.getPackExtCacheConfigurations();
+		if (packExtConfigs == null || packExtConfigs.size() == 0) {
+			throw new IllegalArgumentException(
+					JGitText.get().noPackExtConfigurationGiven);
+		}
+		for (DfsBlockCachePackExtConfig packExtCacheConfig : packExtConfigs) {
+			DfsBlockCacheTable table = new ClockBlockCacheTable(
+					packExtCacheConfig.getPackExtCacheConfiguration());
+			for (PackExt packExt : packExtCacheConfig.getPackExts()) {
+				if (packExtBlockCacheTables.containsKey(packExt)) {
+					throw new IllegalArgumentException(MessageFormat.format(
+							JGitText.get().duplicatePackExtensionsForCacheTables,
+							packExt));
+				}
+				packExtBlockCacheTables.put(packExt, table);
+			}
+		}
+		return fromCacheTables(defaultTable, packExtBlockCacheTables);
+	}
+
+	/**
+	 * Creates a new PackExtBlockCacheTable from the combination of a default
+	 * {@link DfsBlockCacheTable} and a map of {@link PackExt}s to
+	 * {@link DfsBlockCacheTable}s.
+	 * <p>
+	 * This method allows for the PackExtBlockCacheTable to handle a mapping of
+	 * {@link PackExt}s to arbitrarily defined {@link DfsBlockCacheTable}
+	 * implementations. This is especially useful for users wishing to implement
+	 * custom cache tables.
+	 * <p>
+	 * This is currently made visible for testing.
+	 *
+	 * @param defaultBlockCacheTable
+	 *            the default table used when a handling a {@link PackExt} type
+	 *            that does not map to a {@link DfsBlockCacheTable} mapped by
+	 *            packExtsCacheTablePairs.
+	 * @param packExtBlockCacheTables
+	 *            the mapping of {@link PackExt}s to
+	 *            {@link DfsBlockCacheTable}s. A single
+	 *            {@link DfsBlockCacheTable} can be defined for multiple
+	 *            {@link PackExt}s in a many-to-one relationship.
+	 * @return the PackExtBlockCacheTable created from the
+	 *         defaultBlockCacheTable and packExtsCacheTablePairs mapping.
+	 * @throws IllegalArgumentException
+	 *             when a {@link PackExt} is defined for multiple
+	 *             {@link DfsBlockCacheTable}s.
+	 */
+	static PackExtBlockCacheTable fromCacheTables(
+			DfsBlockCacheTable defaultBlockCacheTable,
+			Map<PackExt, DfsBlockCacheTable> packExtBlockCacheTables) {
+		Set<DfsBlockCacheTable> blockCacheTables = new HashSet<>();
+		blockCacheTables.add(defaultBlockCacheTable);
+		blockCacheTables.addAll(packExtBlockCacheTables.values());
+		String name = defaultBlockCacheTable.getName() + "," //$NON-NLS-1$
+				+ packExtBlockCacheTables.values().stream()
+						.map(DfsBlockCacheTable::getName).sorted()
+						.collect(Collectors.joining(",")); //$NON-NLS-1$
+		return new PackExtBlockCacheTable(name, defaultBlockCacheTable,
+				List.copyOf(blockCacheTables), packExtBlockCacheTables);
+	}
+
+	private PackExtBlockCacheTable(String name,
+			DfsBlockCacheTable defaultBlockCacheTable,
+			List<DfsBlockCacheTable> blockCacheTableList,
+			Map<PackExt, DfsBlockCacheTable> extBlockCacheTables) {
+		this.name = name;
+		this.defaultBlockCacheTable = defaultBlockCacheTable;
+		this.blockCacheTableList = blockCacheTableList;
+		this.extBlockCacheTables = extBlockCacheTables;
+	}
+
+	@Override
+	public boolean hasBlock0(DfsStreamKey key) {
+		return getTable(key).hasBlock0(key);
+	}
+
+	@Override
+	public DfsBlock getOrLoad(BlockBasedFile file, long position,
+			DfsReader dfsReader, ReadableChannelSupplier fileChannel)
+			throws IOException {
+		return getTable(file.ext).getOrLoad(file, position, dfsReader,
+				fileChannel);
+	}
+
+	@Override
+	public <T> Ref<T> getOrLoadRef(DfsStreamKey key, long position,
+			RefLoader<T> loader) throws IOException {
+		return getTable(key).getOrLoadRef(key, position, loader);
+	}
+
+	@Override
+	public void put(DfsBlock v) {
+		getTable(v.stream).put(v);
+	}
+
+	@Override
+	public <T> Ref<T> put(DfsStreamKey key, long pos, long size, T v) {
+		return getTable(key).put(key, pos, size, v);
+	}
+
+	@Override
+	public <T> Ref<T> putRef(DfsStreamKey key, long size, T v) {
+		return getTable(key).putRef(key, size, v);
+	}
+
+	@Override
+	public boolean contains(DfsStreamKey key, long position) {
+		return getTable(key).contains(key, position);
+	}
+
+	@Override
+	public <T> T get(DfsStreamKey key, long position) {
+		return getTable(key).get(key, position);
+	}
+
+	@Override
+	public List<BlockCacheStats> getBlockCacheStats() {
+		return blockCacheTableList.stream()
+				.flatMap(cacheTable -> cacheTable.getBlockCacheStats().stream())
+				.collect(Collectors.toList());
+	}
+
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	private DfsBlockCacheTable getTable(PackExt packExt) {
+		return extBlockCacheTables.getOrDefault(packExt,
+				defaultBlockCacheTable);
+	}
+
+	private DfsBlockCacheTable getTable(DfsStreamKey key) {
+		return extBlockCacheTables.getOrDefault(getPackExt(key),
+				defaultBlockCacheTable);
+	}
+
+	private static PackExt getPackExt(DfsStreamKey key) {
+		return PackExt.values()[key.packExtPos];
+	}
+}
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/BasePackIndexWriter.java
similarity index 97%
rename from org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriter.java
rename to org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/BasePackIndexWriter.java
index 87e0b44..b89cc1e 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/BasePackIndexWriter.java
@@ -19,6 +19,7 @@
 import java.util.List;
 
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.transport.PackedObjectInfo;
 import org.eclipse.jgit.util.NB;
@@ -31,7 +32,7 @@
  * random access to any object in the pack by associating an ObjectId to the
  * byte offset within the pack where the object's data can be read.
  */
-public abstract class PackIndexWriter {
+public abstract class BasePackIndexWriter implements PackIndexWriter {
 	/** Magic constant indicating post-version 1 format. */
 	protected static final byte[] TOC = { -1, 't', 'O', 'c' };
 
@@ -147,7 +148,7 @@ public static PackIndexWriter createVersion(final OutputStream dst,
 	 *            the stream this instance outputs to. If not already buffered
 	 *            it will be automatically wrapped in a buffered stream.
 	 */
-	protected PackIndexWriter(OutputStream dst) {
+	protected BasePackIndexWriter(OutputStream dst) {
 		out = new DigestOutputStream(dst instanceof BufferedOutputStream ? dst
 				: new BufferedOutputStream(dst),
 				Constants.newMessageDigest());
@@ -172,6 +173,7 @@ protected PackIndexWriter(OutputStream dst) {
 	 *             an error occurred while writing to the output stream, or this
 	 *             index format cannot store the object data supplied.
 	 */
+	@Override
 	public void write(final List<? extends PackedObjectInfo> toStore,
 			final byte[] packDataChecksum) throws IOException {
 		entries = toStore;
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 ed2516d..e9782e2 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
@@ -16,6 +16,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -23,22 +24,27 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Collectors;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.PackRefsCommand;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.events.RefsChangedEvent;
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.reftable.MergedReftable;
 import org.eclipse.jgit.internal.storage.reftable.ReftableBatchRefUpdate;
 import org.eclipse.jgit.internal.storage.reftable.ReftableDatabase;
 import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
 import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefRename;
@@ -67,15 +73,20 @@ public class FileReftableDatabase extends RefDatabase {
 
 	private final FileReftableStack reftableStack;
 
+	private final AtomicBoolean autoRefresh;
+
 	FileReftableDatabase(FileRepository repo) throws IOException {
-		this(repo, new File(new File(repo.getDirectory(), Constants.REFTABLE),
+		this(repo, new File(new File(repo.getCommonDirectory(), Constants.REFTABLE),
 				Constants.TABLES_LIST));
 	}
 
 	FileReftableDatabase(FileRepository repo, File refstackName) throws IOException {
 		this.fileRepository = repo;
+		this.autoRefresh = new AtomicBoolean(repo.getConfig().getBoolean(
+				ConfigConstants.CONFIG_REFTABLE_SECTION,
+				ConfigConstants.CONFIG_KEY_AUTOREFRESH, false));
 		this.reftableStack = new FileReftableStack(refstackName,
-			new File(fileRepository.getDirectory(), Constants.REFTABLE),
+				new File(fileRepository.getCommonDirectory(), Constants.REFTABLE),
 			() -> fileRepository.fireEvent(new RefsChangedEvent()),
 			() -> fileRepository.getConfig());
 		this.reftableDatabase = new ReftableDatabase() {
@@ -87,7 +98,13 @@ public MergedReftable openMergedReftable() throws IOException {
 		};
 	}
 
-	ReflogReader getReflogReader(String refname) throws IOException {
+	@Override
+	public ReflogReader getReflogReader(Ref ref) throws IOException {
+		return reftableDatabase.getReflogReader(ref.getName());
+	}
+
+	@Override
+	public ReflogReader getReflogReader(String refname) throws IOException {
 		return reftableDatabase.getReflogReader(refname);
 	}
 
@@ -108,6 +125,22 @@ public boolean hasFastTipsWithSha1() throws IOException {
 	}
 
 	/**
+	 * {@inheritDoc}
+	 *
+	 * For Reftable, all the data is compacted into a single table.
+	 */
+	@Override
+	public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs)
+			throws IOException {
+		pm.beginTask(JGitText.get().packRefs, 1);
+		try {
+			compactFully();
+		} finally {
+			pm.endTask();
+		}
+	}
+
+	/**
 	 * Runs a full compaction for GC purposes.
 	 * @throws IOException on I/O errors
 	 */
@@ -158,6 +191,7 @@ public RefUpdate newUpdate(String refName, boolean detach)
 
 	@Override
 	public Ref exactRef(String name) throws IOException {
+		autoRefresh();
 		return reftableDatabase.exactRef(name);
 	}
 
@@ -168,6 +202,7 @@ public List<Ref> getRefs() throws IOException {
 
 	@Override
 	public Map<String, Ref> getRefs(String prefix) throws IOException {
+		autoRefresh();
 		List<Ref> refs = reftableDatabase.getRefsByPrefix(prefix);
 		RefList.Builder<Ref> builder = new RefList.Builder<>(refs.size());
 		for (Ref r : refs) {
@@ -180,6 +215,7 @@ public Map<String, Ref> getRefs(String prefix) throws IOException {
 	@Override
 	public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes)
 			throws IOException {
+		autoRefresh();
 		return reftableDatabase.getRefsByPrefixWithExclusions(include, excludes);
 	}
 
@@ -198,6 +234,50 @@ public Ref peel(Ref ref) throws IOException {
 
 	}
 
+	/**
+	 * Whether to auto-refresh the reftable stack if it is out of date.
+	 *
+	 * @param autoRefresh
+	 *            whether to auto-refresh the reftable stack if it is out of
+	 *            date.
+	 */
+	public void setAutoRefresh(boolean autoRefresh) {
+		this.autoRefresh.set(autoRefresh);
+	}
+
+	/**
+	 * Whether the reftable stack is auto-refreshed if it is out of date.
+	 *
+	 * @return whether the reftable stack is auto-refreshed if it is out of
+	 *         date.
+	 */
+	public boolean isAutoRefresh() {
+		return autoRefresh.get();
+	}
+
+	private void autoRefresh() {
+		if (autoRefresh.get()) {
+			refresh();
+		}
+	}
+
+	/**
+	 * Check if the reftable stack is up to date, and if not, reload it.
+	 * <p>
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void refresh() {
+		try {
+			if (!reftableStack.isUpToDate()) {
+				reftableDatabase.clearCache();
+				reftableStack.reload();
+			}
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
 	private Ref doPeel(Ref leaf) throws IOException {
 		try (RevWalk rw = new RevWalk(fileRepository)) {
 			RevObject obj = rw.parseAny(leaf.getObjectId());
@@ -318,7 +398,7 @@ public void close() {
 	@Override
 	public void create() throws IOException {
 		FileUtils.mkdir(
-				new File(fileRepository.getDirectory(), Constants.REFTABLE),
+				new File(fileRepository.getCommonDirectory(), Constants.REFTABLE),
 				true);
 	}
 
@@ -538,9 +618,10 @@ private static void writeConvertTable(Repository repo, ReftableWriter w,
 			boolean writeLogs) throws IOException {
 		int size = 0;
 		List<Ref> refs = repo.getRefDatabase().getRefs();
+		RefDatabase refDb = repo.getRefDatabase();
 		if (writeLogs) {
 			for (Ref r : refs) {
-				ReflogReader rlr = repo.getReflogReader(r.getName());
+				ReflogReader rlr = refDb.getReflogReader(r);
 				if (rlr != null) {
 					size = Math.max(rlr.getReverseEntries().size(), size);
 				}
@@ -563,10 +644,7 @@ private static void writeConvertTable(Repository repo, ReftableWriter w,
 		if (writeLogs) {
 			for (Ref r : refs) {
 				long idx = size;
-				ReflogReader reader = repo.getReflogReader(r.getName());
-				if (reader == null) {
-					continue;
-				}
+				ReflogReader reader = refDb.getReflogReader(r);
 				for (ReflogEntry e : reader.getReverseEntries()) {
 					w.writeLog(r.getName(), idx, e.getWho(), e.getOldId(),
 							e.getNewId(), e.getComment());
@@ -615,7 +693,7 @@ public static FileReftableDatabase convertFrom(FileRepository repo,
 		FileReftableDatabase newDb = null;
 		File reftableList = null;
 		try {
-			File reftableDir = new File(repo.getDirectory(),
+			File reftableDir = new File(repo.getCommonDirectory(),
 					Constants.REFTABLE);
 			reftableList = new File(reftableDir, Constants.TABLES_LIST);
 			if (!reftableDir.isDirectory()) {
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 0f5ff0f..b2c8892 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
@@ -18,8 +18,10 @@
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.StandardCopyOption;
 import java.security.SecureRandom;
 import java.util.ArrayList;
@@ -27,6 +29,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -39,6 +42,8 @@
 import org.eclipse.jgit.internal.storage.reftable.ReftableReader;
 import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.CoreConfig;
+import org.eclipse.jgit.lib.CoreConfig.TrustStat;
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.SystemReader;
 
@@ -59,6 +64,9 @@ private static class StackEntry {
 
 	private List<StackEntry> stack;
 
+	private AtomicReference<FileSnapshot> snapshot = new AtomicReference<>(
+			FileSnapshot.DIRTY);
+
 	private long lastNextUpdateIndex;
 
 	private final File stackPath;
@@ -98,6 +106,8 @@ static class CompactionStats {
 
 	private final CompactionStats stats;
 
+	private final TrustStat trustTablesListStat;
+
 	/**
 	 * Creates a stack corresponding to the list of reftables in the argument
 	 *
@@ -126,6 +136,8 @@ public FileReftableStack(File stackPath, File reftableDir,
 		reload();
 
 		stats = new CompactionStats();
+		trustTablesListStat = configSupplier.get().get(CoreConfig.KEY)
+				.getTrustTablesListStat();
 	}
 
 	CompactionStats getStats() {
@@ -272,8 +284,9 @@ public interface Writer {
 	}
 
 	private List<String> readTableNames() throws IOException {
+		FileSnapshot old;
 		List<String> names = new ArrayList<>(stack.size() + 1);
-
+		old = snapshot.get();
 		try (BufferedReader br = new BufferedReader(
 				new InputStreamReader(new FileInputStream(stackPath), UTF_8))) {
 			String line;
@@ -282,8 +295,10 @@ private List<String> readTableNames() throws IOException {
 					names.add(line);
 				}
 			}
+			snapshot.compareAndSet(old, FileSnapshot.save(stackPath));
 		} catch (FileNotFoundException e) {
 			// file isn't there: empty repository.
+			snapshot.compareAndSet(old, FileSnapshot.MISSING_FILE);
 		}
 		return names;
 	}
@@ -294,9 +309,28 @@ private List<String> readTableNames() throws IOException {
 	 *             on IO problem
 	 */
 	boolean isUpToDate() throws IOException {
-		// We could use FileSnapshot to avoid reading the file, but the file is
-		// small so it's probably a minor optimization.
 		try {
+			switch (trustTablesListStat) {
+			case NEVER:
+				break;
+			case AFTER_OPEN:
+				try (InputStream stream = Files
+						.newInputStream(stackPath.toPath())) {
+					// open the tables.list file to refresh attributes (on some
+					// NFS clients)
+				} catch (FileNotFoundException | NoSuchFileException e) {
+					// ignore
+				}
+				//$FALL-THROUGH$
+			case ALWAYS:
+				if (!snapshot.get().isModified(stackPath)) {
+					return true;
+				}
+				break;
+			case INHERIT:
+				// only used in CoreConfig internally
+				throw new IllegalStateException();
+			}
 			List<String> names = readTableNames();
 			if (names.size() != stack.size()) {
 				return false;
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 e5a00d3..bcf9f1e 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
@@ -2,7 +2,7 @@
  * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
  * Copyright (C) 2008-2010, Google Inc.
  * Copyright (C) 2006-2010, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2006-2024, 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,8 +31,8 @@
 import java.util.Objects;
 import java.util.Set;
 
-import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.attributes.AttributesNode;
 import org.eclipse.jgit.attributes.AttributesNodeProvider;
@@ -60,7 +60,6 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
-import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.IO;
@@ -165,7 +164,7 @@ public FileRepository(BaseRepositoryBuilder options) throws IOException {
 			throw new IOException(e.getMessage(), e);
 		}
 		repoConfig = new FileBasedConfig(userConfig, getFS().resolve(
-				getDirectory(), Constants.CONFIG),
+				getCommonDirectory(), Constants.CONFIG),
 				getFS());
 		loadRepoConfig();
 
@@ -193,7 +192,7 @@ public FileRepository(BaseRepositoryBuilder options) throws IOException {
 				options.getObjectDirectory(), //
 				options.getAlternateObjectDirectories(), //
 				getFS(), //
-				new File(getDirectory(), Constants.SHALLOW));
+				new File(getCommonDirectory(), Constants.SHALLOW));
 
 		if (objectDatabase.exists()) {
 			if (repositoryFormatVersion > 1)
@@ -215,6 +214,16 @@ private void loadRepoConfig() throws IOException {
 		}
 	}
 
+	private String getRelativeDir(File base, File other) {
+		File relPath;
+		try {
+			relPath = base.toPath().relativize(other.toPath()).toFile();
+		} catch (IllegalArgumentException e) {
+			relPath = other;
+		}
+		return FileUtils.pathToString(relPath);
+	}
+
 	/**
 	 * {@inheritDoc}
 	 * <p>
@@ -223,6 +232,22 @@ private void loadRepoConfig() throws IOException {
 	 */
 	@Override
 	public void create(boolean bare) throws IOException {
+		create(bare, false);
+	}
+
+	/**
+	 * Create a new Git repository initializing the necessary files and
+	 * directories.
+	 *
+	 * @param bare
+	 *            if true, a bare repository (a repository without a working
+	 *            directory) is created.
+	 * @param relativePaths
+	 *            if true, relative paths are used for GIT_DIR and GIT_WORK_TREE
+	 * @throws IOException
+	 *             in case of IO problem
+	 */
+	public void create(boolean bare, boolean relativePaths) throws IOException {
 		final FileBasedConfig cfg = getConfig();
 		if (cfg.getFile().exists()) {
 			throw new IllegalStateException(MessageFormat.format(
@@ -293,15 +318,25 @@ && getDirectory().getName().startsWith(".")) //$NON-NLS-1$
 		if (!bare) {
 			File workTree = getWorkTree();
 			if (!getDirectory().getParentFile().equals(workTree)) {
+				String workTreePath;
+				String gitDirPath;
+				if (relativePaths) {
+					File canonGitDir = getDirectory().getCanonicalFile();
+					File canonWorkTree = getWorkTree().getCanonicalFile();
+					workTreePath = getRelativeDir(canonGitDir, canonWorkTree);
+					gitDirPath = getRelativeDir(canonWorkTree, canonGitDir);
+				} else {
+					workTreePath = getWorkTree().getAbsolutePath();
+					gitDirPath = getDirectory().getAbsolutePath();
+				}
 				cfg.setString(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_WORKTREE, getWorkTree()
-								.getAbsolutePath());
+						ConfigConstants.CONFIG_KEY_WORKTREE, workTreePath);
 				LockFile dotGitLockFile = new LockFile(new File(workTree,
 						Constants.DOT_GIT));
 				try {
 					if (dotGitLockFile.lock()) {
 						dotGitLockFile.write(Constants.encode(Constants.GITDIR
-								+ getDirectory().getAbsolutePath()));
+								+ gitDirPath));
 						dotGitLockFile.commit();
 					}
 				} finally {
@@ -507,29 +542,6 @@ public void notifyIndexChanged(boolean internal) {
 	}
 
 	@Override
-	public ReflogReader getReflogReader(String refName) throws IOException {
-		if (refs instanceof FileReftableDatabase) {
-			// Cannot use findRef: reftable stores log data for deleted or renamed
-			// branches.
-			return ((FileReftableDatabase)refs).getReflogReader(refName);
-		}
-
-		// TODO: use exactRef here, which offers more predictable and therefore preferable
-		// behavior.
-		Ref ref = findRef(refName);
-		if (ref == null) {
-			return null;
-		}
-		return new ReflogReaderImpl(this, ref.getName());
-	}
-
-	@Override
-	public @NonNull ReflogReader getReflogReader(@NonNull Ref ref)
-			throws IOException {
-		return new ReflogReaderImpl(this, ref.getName());
-	}
-
-	@Override
 	public AttributesNodeProvider createAttributesNodeProvider() {
 		return new AttributesNodeProviderImpl(this);
 	}
@@ -595,13 +607,12 @@ private boolean shouldAutoDetach() {
 	@Override
 	public void autoGC(ProgressMonitor monitor) {
 		GC gc = new GC(this);
-		gc.setPackConfig(new PackConfig(this));
 		gc.setProgressMonitor(monitor);
 		gc.setAuto(true);
 		gc.setBackground(shouldAutoDetach());
 		try {
 			gc.gc();
-		} catch (ParseException | IOException e) {
+		} catch (ParseException | IOException | GitAPIException e) {
 			throw new JGitInternalException(JGitText.get().gcFailed, e);
 		}
 	}
@@ -622,16 +633,17 @@ public void autoGC(ProgressMonitor monitor) {
 	 *             on IO problem
 	 */
 	void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException {
+		File commonDirectory = getCommonDirectory();
 		List<Ref> all = refs.getRefs();
-		File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
+		File packedRefs = new File(commonDirectory, Constants.PACKED_REFS);
 		if (packedRefs.exists()) {
 			throw new IOException(MessageFormat.format(JGitText.get().fileAlreadyExists,
 				packedRefs.getName()));
 		}
 
-		File refsFile = new File(getDirectory(), "refs"); //$NON-NLS-1$
+		File refsFile = new File(commonDirectory, "refs"); //$NON-NLS-1$
 		File refsHeadsFile = new File(refsFile, "heads");//$NON-NLS-1$
-		File headFile = new File(getDirectory(), Constants.HEAD);
+		File headFile = new File(commonDirectory, Constants.HEAD);
 		FileReftableDatabase oldDb = (FileReftableDatabase) refs;
 
 		// Remove the dummy files that ensure compatibility with older git
@@ -661,8 +673,8 @@ void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException {
 			}
 
 			if (writeLogs) {
-				List<ReflogEntry> logs = oldDb.getReflogReader(r.getName())
-						.getReverseEntries();
+				ReflogReader reflogReader = oldDb.getReflogReader(r);
+				List<ReflogEntry> logs = reflogReader.getReverseEntries();
 				Collections.reverse(logs);
 				for (ReflogEntry e : logs) {
 					logWriter.log(r.getName(), e);
@@ -701,12 +713,14 @@ void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException {
 		}
 
 		if (!backup) {
-			File reftableDir = new File(getDirectory(), Constants.REFTABLE);
+			File reftableDir = new File(commonDirectory, Constants.REFTABLE);
 			FileUtils.delete(reftableDir,
 					FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
 		}
 		repoConfig.unset(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
 				ConfigConstants.CONFIG_KEY_REF_STORAGE);
+		repoConfig.setLong(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
 		repoConfig.save();
 	}
 
@@ -730,8 +744,10 @@ void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException {
 	@SuppressWarnings("nls")
 	void convertToReftable(boolean writeLogs, boolean backup)
 			throws IOException {
-		File reftableDir = new File(getDirectory(), Constants.REFTABLE);
-		File headFile = new File(getDirectory(), Constants.HEAD);
+		File commonDirectory = getCommonDirectory();
+		File directory = getDirectory();
+		File reftableDir = new File(commonDirectory, Constants.REFTABLE);
+		File headFile = new File(directory, Constants.HEAD);
 		if (reftableDir.exists() && FileUtils.hasFiles(reftableDir.toPath())) {
 			throw new IOException(JGitText.get().reftableDirExists);
 		}
@@ -739,28 +755,28 @@ void convertToReftable(boolean writeLogs, boolean backup)
 		// Ignore return value, as it is tied to temporary newRefs file.
 		FileReftableDatabase.convertFrom(this, writeLogs);
 
-		File refsFile = new File(getDirectory(), "refs");
+		File refsFile = new File(commonDirectory, "refs");
 
 		// non-atomic: remove old data.
-		File packedRefs = new File(getDirectory(), Constants.PACKED_REFS);
-		File logsDir = new File(getDirectory(), Constants.LOGS);
+		File packedRefs = new File(commonDirectory, Constants.PACKED_REFS);
+		File logsDir = new File(commonDirectory, Constants.LOGS);
 
 		List<String> additional = getRefDatabase().getAdditionalRefs().stream()
 				.map(Ref::getName).collect(toList());
 		additional.add(Constants.HEAD);
 		if (backup) {
-			FileUtils.rename(refsFile, new File(getDirectory(), "refs.old"));
+			FileUtils.rename(refsFile, new File(commonDirectory, "refs.old"));
 			if (packedRefs.exists()) {
-				FileUtils.rename(packedRefs, new File(getDirectory(),
+				FileUtils.rename(packedRefs, new File(commonDirectory,
 						Constants.PACKED_REFS + ".old"));
 			}
 			if (logsDir.exists()) {
 				FileUtils.rename(logsDir,
-						new File(getDirectory(), Constants.LOGS + ".old"));
+						new File(commonDirectory, Constants.LOGS + ".old"));
 			}
 			for (String r : additional) {
-				FileUtils.rename(new File(getDirectory(), r),
-					new File(getDirectory(), r + ".old"));
+				FileUtils.rename(new File(commonDirectory, r),
+						new File(commonDirectory, r + ".old"));
 			}
 		} else {
 			FileUtils.delete(packedRefs, FileUtils.SKIP_MISSING);
@@ -770,7 +786,7 @@ void convertToReftable(boolean writeLogs, boolean backup)
 			FileUtils.delete(refsFile,
 					FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
 			for (String r : additional) {
-				new File(getDirectory(), r).delete();
+				new File(commonDirectory, r).delete();
 			}
 		}
 
@@ -784,7 +800,7 @@ void convertToReftable(boolean writeLogs, boolean backup)
 
 		// Some tools might write directly into .git/refs/heads/BRANCH. By
 		// putting a file here, this fails spectacularly.
-		FileUtils.createNewFile(new File(refsFile, "heads"));
+		FileUtils.createNewFile(new File(refsFile, Constants.HEADS));
 
 		repoConfig.setString(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null,
 				ConfigConstants.CONFIG_KEY_REF_STORAGE,
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 c88ac98..b6bde6e 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
@@ -15,6 +15,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.FileSystemException;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.time.Duration;
@@ -22,10 +23,12 @@
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.Locale;
+import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.util.FileUtils;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FS.FileStoreAttributes;
 import org.slf4j.Logger;
@@ -139,29 +142,6 @@ private static Object getFileKey(BasicFileAttributes fileAttributes) {
 	 * @param modified
 	 *            the last modification time of the file
 	 * @return the snapshot.
-	 * @deprecated use {@link #save(Instant)} instead.
-	 */
-	@Deprecated
-	public static FileSnapshot save(long modified) {
-		final Instant read = Instant.now();
-		return new FileSnapshot(read, Instant.ofEpochMilli(modified),
-				UNKNOWN_SIZE, FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
-	}
-
-	/**
-	 * Record a snapshot for a file for which the last modification time is
-	 * already known.
-	 * <p>
-	 * This method should be invoked before the file is accessed.
-	 * <p>
-	 * Note that this method cannot rely on measuring file timestamp resolution
-	 * to avoid racy git issues caused by finite file timestamp resolution since
-	 * it's unknown in which filesystem the file is located. Hence the worst
-	 * case fallback for timestamp resolution is used.
-	 *
-	 * @param modified
-	 *            the last modification time of the file
-	 * @return the snapshot.
 	 */
 	public static FileSnapshot save(Instant modified) {
 		final Instant read = Instant.now();
@@ -231,14 +211,8 @@ protected FileSnapshot(File file, boolean useConfig) {
 		this.useConfig = useConfig;
 		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) {
-			LOG.error(e.getMessage(), e);
+			fileAttributes = getFileAttributes(file);
+		} catch (NoSuchElementException e) {
 			this.lastModified = Instant.EPOCH;
 			this.size = 0L;
 			this.fileKey = MISSING_FILEKEY;
@@ -282,17 +256,6 @@ private FileSnapshot(Instant read, Instant modified, long size,
 	 * Get time of last snapshot update
 	 *
 	 * @return time of last snapshot update
-	 * @deprecated use {@link #lastModifiedInstant()} instead
-	 */
-	@Deprecated
-	public long lastModified() {
-		return lastModified.toEpochMilli();
-	}
-
-	/**
-	 * Get time of last snapshot update
-	 *
-	 * @return time of last snapshot update
 	 */
 	public Instant lastModifiedInstant() {
 		return lastModified;
@@ -319,16 +282,11 @@ public boolean isModified(File path) {
 		long currSize;
 		Object currFileKey;
 		try {
-			BasicFileAttributes fileAttributes = FS.DETECTED.fileAttributes(path);
+			BasicFileAttributes fileAttributes = getFileAttributes(path);
 			currLastModified = fileAttributes.lastModifiedTime().toInstant();
 			currSize = fileAttributes.size();
 			currFileKey = getFileKey(fileAttributes);
-		} catch (NoSuchFileException e) {
-			currLastModified = Instant.EPOCH;
-			currSize = 0L;
-			currFileKey = MISSING_FILEKEY;
-		} catch (IOException e) {
-			LOG.error(e.getMessage(), e);
+		} catch (NoSuchElementException e) {
 			currLastModified = Instant.EPOCH;
 			currSize = 0L;
 			currFileKey = MISSING_FILEKEY;
@@ -586,4 +544,27 @@ private FileStoreAttributes fileStoreAttributeCache() {
 		}
 		return fileStoreAttributeCache;
 	}
+
+	private static BasicFileAttributes getFileAttributes(File path)
+			throws NoSuchElementException {
+		try {
+			try {
+				return FS.DETECTED.fileAttributes(path);
+			} catch (IOException e) {
+				if (!FileUtils.isStaleFileHandle(e)) {
+					throw e;
+				}
+			}
+		} catch (NoSuchFileException e) {
+			// ignore
+		} catch (FileSystemException e) {
+			String msg = e.getMessage();
+			if (!msg.endsWith("Not a directory")) { //$NON-NLS-1$
+				LOG.error(msg, e);
+			}
+		} catch (IOException e) {
+			LOG.error(e.getMessage(), e);
+		}
+		throw new NoSuchElementException(path.toString());
+	}
 }
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 cf26f8d..05bd970 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
@@ -63,6 +63,8 @@
 import java.util.stream.Stream;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.PackRefsCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.CancelledException;
 import org.eclipse.jgit.errors.CorruptObjectException;
@@ -100,9 +102,8 @@
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FS.LockToken;
 import org.eclipse.jgit.util.FileUtils;
-import org.eclipse.jgit.util.GitDateParser;
+import org.eclipse.jgit.util.GitTimeParser;
 import org.eclipse.jgit.util.StringUtils;
-import org.eclipse.jgit.util.SystemReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -158,11 +159,11 @@ public static void setExecutor(ExecutorService e) {
 
 	private long expireAgeMillis = -1;
 
-	private Date expire;
+	private Instant expire;
 
 	private long packExpireAgeMillis = -1;
 
-	private Date packExpire;
+	private Instant packExpire;
 
 	private Boolean packKeptObjects;
 
@@ -233,9 +234,11 @@ public GC(FileRepository repo) {
 	 * @throws java.text.ParseException
 	 *             If the configuration parameter "gc.pruneexpire" couldn't be
 	 *             parsed
+	 * @throws GitAPIException
+	 *             If packing refs failed
 	 */
 	public CompletableFuture<Collection<Pack>> gc()
-			throws IOException, ParseException {
+			throws IOException, ParseException, GitAPIException {
 		if (!background) {
 			return CompletableFuture.completedFuture(doGc());
 		}
@@ -254,7 +257,7 @@ public CompletableFuture<Collection<Pack>> gc()
 					gcLog.commit();
 				}
 				return newPacks;
-			} catch (IOException | ParseException e) {
+			} catch (IOException | ParseException | GitAPIException e) {
 				try {
 					gcLog.write(e.getMessage());
 					StringWriter sw = new StringWriter();
@@ -277,7 +280,8 @@ private ExecutorService executor() {
 		return (executor != null) ? executor : WorkQueue.getExecutor();
 	}
 
-	private Collection<Pack> doGc() throws IOException, ParseException {
+	private Collection<Pack> doGc()
+			throws IOException, ParseException, GitAPIException {
 		if (automatic && !needGc()) {
 			return Collections.emptyList();
 		}
@@ -286,7 +290,8 @@ private Collection<Pack> doGc() throws IOException, ParseException {
 				return Collections.emptyList();
 			}
 			pm.start(6 /* tasks */);
-			packRefs();
+			new PackRefsCommand(repo).setProgressMonitor(pm).setAll(true)
+					.call();
 			// TODO: implement reflog_expire(pm, repo);
 			Collection<Pack> newPacks = repack();
 			prune(Collections.emptySet());
@@ -692,16 +697,18 @@ private long getExpireDate() throws ParseException {
 
 		if (expire == null && expireAgeMillis == -1) {
 			String pruneExpireStr = getPruneExpireStr();
-			if (pruneExpireStr == null)
+			if (pruneExpireStr == null) {
 				pruneExpireStr = PRUNE_EXPIRE_DEFAULT;
-			expire = GitDateParser.parse(pruneExpireStr, null, SystemReader
-					.getInstance().getLocale());
+			}
+			expire = GitTimeParser.parseInstant(pruneExpireStr);
 			expireAgeMillis = -1;
 		}
-		if (expire != null)
-			expireDate = expire.getTime();
-		if (expireAgeMillis != -1)
+		if (expire != null) {
+			expireDate = expire.toEpochMilli();
+		}
+		if (expireAgeMillis != -1) {
 			expireDate = System.currentTimeMillis() - expireAgeMillis;
+		}
 		return expireDate;
 	}
 
@@ -718,16 +725,18 @@ private long getPackExpireDate() throws ParseException {
 			String prunePackExpireStr = repo.getConfig().getString(
 					ConfigConstants.CONFIG_GC_SECTION, null,
 					ConfigConstants.CONFIG_KEY_PRUNEPACKEXPIRE);
-			if (prunePackExpireStr == null)
+			if (prunePackExpireStr == null) {
 				prunePackExpireStr = PRUNE_PACK_EXPIRE_DEFAULT;
-			packExpire = GitDateParser.parse(prunePackExpireStr, null,
-					SystemReader.getInstance().getLocale());
+			}
+			packExpire = GitTimeParser.parseInstant(prunePackExpireStr);
 			packExpireAgeMillis = -1;
 		}
-		if (packExpire != null)
-			packExpireDate = packExpire.getTime();
-		if (packExpireAgeMillis != -1)
+		if (packExpire != null) {
+			packExpireDate = packExpire.toEpochMilli();
+		}
+		if (packExpireAgeMillis != -1) {
 			packExpireDate = System.currentTimeMillis() - packExpireAgeMillis;
+		}
 		return packExpireDate;
 	}
 
@@ -780,43 +789,6 @@ private static boolean equals(Ref r1, Ref r2) {
 	}
 
 	/**
-	 * Pack ref storage. For a RefDirectory database, this packs all
-	 * non-symbolic, loose refs into packed-refs. For Reftable, all of the data
-	 * is compacted into a single table.
-	 *
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 */
-	public void packRefs() throws IOException {
-		RefDatabase refDb = repo.getRefDatabase();
-		if (refDb instanceof FileReftableDatabase) {
-			// TODO: abstract this more cleanly.
-			pm.beginTask(JGitText.get().packRefs, 1);
-			try {
-				((FileReftableDatabase) refDb).compactFully();
-			} finally {
-				pm.endTask();
-			}
-			return;
-		}
-
-		Collection<Ref> refs = refDb.getRefsByPrefix(Constants.R_REFS);
-		List<String> refsToBePacked = new ArrayList<>(refs.size());
-		pm.beginTask(JGitText.get().packRefs, refs.size());
-		try {
-			for (Ref ref : refs) {
-				checkCancelled();
-				if (!ref.isSymbolic() && ref.getStorage().isLoose())
-					refsToBePacked.add(ref.getName());
-				pm.update(1);
-			}
-			((RefDirectory) repo.getRefDatabase()).pack(refsToBePacked);
-		} finally {
-			pm.endTask();
-		}
-	}
-
-	/**
 	 * Packs all objects which reachable from any of the heads into one pack
 	 * file. Additionally all objects which are not reachable from any head but
 	 * which are reachable from any of the other refs (e.g. tags), special refs
@@ -1047,7 +1019,7 @@ private static boolean isTag(Ref ref) {
 	}
 
 	private void deleteEmptyRefsFolders() throws IOException {
-		Path refs = repo.getDirectory().toPath().resolve(Constants.R_REFS);
+		Path refs = repo.getCommonDirectory().toPath().resolve(Constants.R_REFS);
 		// Avoid deleting a folder that was created after the threshold so that concurrent
 		// operations trying to create a reference are not impacted
 		Instant threshold = Instant.now().minus(30, ChronoUnit.SECONDS);
@@ -1185,7 +1157,7 @@ private void deleteTempPacksIdx() {
 	 *             if an IO error occurred
 	 */
 	private Set<ObjectId> listRefLogObjects(Ref ref, long minTime) throws IOException {
-		ReflogReader reflogReader = repo.getReflogReader(ref);
+		ReflogReader reflogReader = repo.getRefDatabase().getReflogReader(ref);
 		List<ReflogEntry> rlEntries = reflogReader
 				.getReverseEntries();
 		if (rlEntries == null || rlEntries.isEmpty())
@@ -1509,6 +1481,18 @@ public static class RepoStatistics {
 		public long numberOfPackFiles;
 
 		/**
+		 * The number of pack files that were created since the last bitmap
+		 * generation.
+		 */
+		public long numberOfPackFilesSinceBitmap;
+
+		/**
+		 * The number of objects stored in pack files and as loose object
+		 * created after the last bitmap generation.
+		 */
+		public long numberOfObjectsSinceBitmap;
+
+		/**
 		 * The number of objects stored as loose objects.
 		 */
 		public long numberOfLooseObjects;
@@ -1543,6 +1527,10 @@ public String toString() {
 			final StringBuilder b = new StringBuilder();
 			b.append("numberOfPackedObjects=").append(numberOfPackedObjects); //$NON-NLS-1$
 			b.append(", numberOfPackFiles=").append(numberOfPackFiles); //$NON-NLS-1$
+			b.append(", numberOfPackFilesSinceBitmap=") //$NON-NLS-1$
+					.append(numberOfPackFilesSinceBitmap);
+			b.append(", numberOfObjectsSinceBitmap=") //$NON-NLS-1$
+					.append(numberOfObjectsSinceBitmap);
 			b.append(", numberOfLooseObjects=").append(numberOfLooseObjects); //$NON-NLS-1$
 			b.append(", numberOfLooseRefs=").append(numberOfLooseRefs); //$NON-NLS-1$
 			b.append(", numberOfPackedRefs=").append(numberOfPackedRefs); //$NON-NLS-1$
@@ -1563,12 +1551,22 @@ public String toString() {
 	public RepoStatistics getStatistics() throws IOException {
 		RepoStatistics ret = new RepoStatistics();
 		Collection<Pack> packs = repo.getObjectDatabase().getPacks();
+		long latestBitmapTime = 0L;
 		for (Pack p : packs) {
-			ret.numberOfPackedObjects += p.getIndex().getObjectCount();
+			long packedObjects = p.getIndex().getObjectCount();
+			ret.numberOfPackedObjects += packedObjects;
 			ret.numberOfPackFiles++;
 			ret.sizeOfPackedObjects += p.getPackFile().length();
-			if (p.getBitmapIndex() != null)
+			if (p.getBitmapIndex() != null) {
 				ret.numberOfBitmaps += p.getBitmapIndex().getBitmapCount();
+				if (latestBitmapTime == 0L) {
+					latestBitmapTime = p.getFileSnapshot().lastModifiedInstant().toEpochMilli();
+				}
+			}
+			else if (latestBitmapTime == 0L) {
+				ret.numberOfPackFilesSinceBitmap++;
+				ret.numberOfObjectsSinceBitmap += packedObjects;
+			}
 		}
 		File objDir = repo.getObjectsDirectory();
 		String[] fanout = objDir.list();
@@ -1584,6 +1582,9 @@ public RepoStatistics getStatistics() throws IOException {
 						continue;
 					ret.numberOfLooseObjects++;
 					ret.sizeOfLooseObjects += f.length();
+					if (f.lastModified() > latestBitmapTime) {
+						ret.numberOfObjectsSinceBitmap ++;
+					}
 				}
 			}
 		}
@@ -1659,12 +1660,31 @@ public void setPackConfig(@NonNull PackConfig pconfig) {
 	 * candidate for pruning.
 	 *
 	 * @param expire
-	 *            instant in time which defines object expiration
-	 *            objects with modification time before this instant are expired
-	 *            objects with modification time newer or equal to this instant
-	 *            are not expired
+	 *            instant in time which defines object expiration objects with
+	 *            modification time before this instant are expired objects with
+	 *            modification time newer or equal to this instant are not
+	 *            expired
+	 * @deprecated use {@link #setExpire(Instant)} instead
 	 */
+	@Deprecated(since = "7.2")
 	public void setExpire(Date expire) {
+		this.expire = expire.toInstant();
+		expireAgeMillis = -1;
+	}
+
+	/**
+	 * During gc() or prune() each unreferenced, loose object which has been
+	 * created or modified after or at <code>expire</code> will not be pruned.
+	 * Only older objects may be pruned. If set to null then every object is a
+	 * candidate for pruning.
+	 *
+	 * @param expire
+	 *            instant in time which defines object expiration objects with
+	 *            modification time before this instant are expired objects with
+	 *            modification time newer or equal to this instant are not
+	 *            expired
+	 */
+	public void setExpire(Instant expire) {
 		this.expire = expire;
 		expireAgeMillis = -1;
 	}
@@ -1677,8 +1697,24 @@ public void setExpire(Date expire) {
 	 *
 	 * @param packExpire
 	 *            instant in time which defines packfile expiration
+	 * @deprecated use {@link #setPackExpire(Instant)} instead
 	 */
+	@Deprecated(since = "7.2")
 	public void setPackExpire(Date packExpire) {
+		this.packExpire = packExpire.toInstant();
+		packExpireAgeMillis = -1;
+	}
+
+	/**
+	 * During gc() or prune() packfiles which are created or modified after or
+	 * at <code>packExpire</code> will not be deleted. Only older packfiles may
+	 * be deleted. If set to null then every packfile is a candidate for
+	 * deletion.
+	 *
+	 * @param packExpire
+	 *            instant in time which defines packfile expiration
+	 */
+	public void setPackExpire(Instant packExpire) {
 		this.packExpire = packExpire;
 		packExpireAgeMillis = -1;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java
index 628bf5d..862aaab 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java
@@ -23,8 +23,7 @@
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.util.FileUtils;
-import org.eclipse.jgit.util.GitDateParser;
-import org.eclipse.jgit.util.SystemReader;
+import org.eclipse.jgit.util.GitTimeParser;
 
 /**
  * This class manages the gc.log file for a {@link FileRepository}.
@@ -50,7 +49,7 @@ class GcLog {
 	 */
 	GcLog(FileRepository repo) {
 		this.repo = repo;
-		logFile = new File(repo.getDirectory(), "gc.log"); //$NON-NLS-1$
+		logFile = new File(repo.getCommonDirectory(), "gc.log"); //$NON-NLS-1$
 		lock = new LockFile(logFile);
 	}
 
@@ -62,8 +61,7 @@ private Instant getLogExpiry() throws ParseException {
 			if (logExpiryStr == null) {
 				logExpiryStr = LOG_EXPIRY_DEFAULT;
 			}
-			gcLogExpire = GitDateParser.parse(logExpiryStr, null,
-					SystemReader.getInstance().getLocale()).toInstant();
+			gcLogExpire = GitTimeParser.parseInstant(logExpiryStr);
 		}
 		return gcLogExpire;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java
index 11d842b..e8d442b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java
@@ -46,7 +46,7 @@ public AttributesNode load() throws IOException {
 
 		FS fs = repository.getFS();
 
-		File attributes = fs.resolve(repository.getDirectory(),
+		File attributes = fs.resolve(repository.getCommonDirectory(),
 				Constants.INFO_ATTRIBUTES);
 		FileRepository.AttributesNodeProviderImpl.loadRulesFromFile(r, attributes);
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
index a2d8bd0..9e12ee8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
@@ -24,6 +24,7 @@
 import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.FileTime;
 import java.text.MessageFormat;
@@ -141,9 +142,8 @@ public boolean lock() throws IOException {
 			throw new IllegalStateException(
 					MessageFormat.format(JGitText.get().lockAlreadyHeld, ref));
 		}
-		FileUtils.mkdirs(lck.getParentFile(), true);
 		try {
-			token = FS.DETECTED.createNewFileAtomic(lck);
+			token = createLockFileWithRetry();
 		} catch (IOException e) {
 			LOG.error(JGitText.get().failedCreateLockFile, lck, e);
 			throw e;
@@ -160,6 +160,19 @@ public boolean lock() throws IOException {
 		return obtainedLock;
 	}
 
+	private FS.LockToken createLockFileWithRetry() throws IOException {
+		try {
+			return createLockFile();
+		} catch (NoSuchFileException e) {
+			return createLockFile();
+		}
+	}
+
+	private FS.LockToken createLockFile() throws IOException {
+		FileUtils.mkdirs(lck.getParentFile(), true);
+		return FS.DETECTED.createNewFileAtomic(lck);
+	}
+
 	/**
 	 * Try to establish the lock for appending.
 	 *
@@ -515,17 +528,6 @@ private void saveStatInformation() {
 	 * Get the modification time of the output file when it was committed.
 	 *
 	 * @return modification time of the lock file right before we committed it.
-	 * @deprecated use {@link #getCommitLastModifiedInstant()} instead
-	 */
-	@Deprecated
-	public long getCommitLastModified() {
-		return commitSnapshot.lastModified();
-	}
-
-	/**
-	 * Get the modification time of the output file when it was committed.
-	 *
-	 * @return modification time of the lock file right before we committed it.
 	 */
 	public Instant getCommitLastModifiedInstant() {
 		return commitSnapshot.lastModifiedInstant();
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
index b4bb2a9..909b3e3 100644
--- 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
@@ -26,8 +26,9 @@
 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.CoreConfig;
+import org.eclipse.jgit.lib.CoreConfig.TrustStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.util.FileUtils;
@@ -49,13 +50,13 @@ class LooseObjects {
 	 * 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 static int MAX_STALE_READ_RETRIES = 5;
 
 	private final File directory;
 
 	private final UnpackedObjectCache unpackedObjectCache;
 
-	private final boolean trustFolderStat;
+	private final TrustStat trustLooseObjectStat;
 
 	/**
 	 * Initialize a reference to an on-disk object directory.
@@ -68,9 +69,8 @@ class LooseObjects {
 	LooseObjects(Config config, File dir) {
 		directory = dir;
 		unpackedObjectCache = new UnpackedObjectCache();
-		trustFolderStat = config.getBoolean(
-				ConfigConstants.CONFIG_CORE_SECTION,
-				ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true);
+		trustLooseObjectStat = config.get(CoreConfig.KEY)
+				.getTrustLooseObjectStat();
 	}
 
 	/**
@@ -108,7 +108,8 @@ boolean hasCached(AnyObjectId id) {
 	 */
 	boolean has(AnyObjectId objectId) {
 		boolean exists = hasWithoutRefresh(objectId);
-		if (trustFolderStat || exists) {
+		if (trustLooseObjectStat == TrustStat.ALWAYS
+				|| exists) {
 			return exists;
 		}
 		try (InputStream stream = Files.newInputStream(directory.toPath())) {
@@ -163,13 +164,31 @@ boolean resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
 	}
 
 	ObjectLoader open(WindowCursor curs, AnyObjectId id) throws IOException {
-		int readAttempts = 0;
-		while (readAttempts < MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS) {
-			readAttempts++;
-			File path = fileFor(id);
-			if (trustFolderStat && !path.exists()) {
+		File path = fileFor(id);
+		for (int retries = 0; retries < MAX_STALE_READ_RETRIES; retries++) {
+			boolean reload = true;
+			switch (trustLooseObjectStat) {
+			case NEVER:
 				break;
+			case AFTER_OPEN:
+				try (InputStream stream = Files
+						.newInputStream(path.getParentFile().toPath())) {
+					// open the loose object's fanout directory to refresh
+					// attributes (on some NFS clients)
+				} catch (FileNotFoundException | NoSuchFileException e) {
+					// ignore
+				}
+				//$FALL-THROUGH$
+			case ALWAYS:
+				if (!path.exists()) {
+					reload = false;
+				}
+				break;
+			case INHERIT:
+				// only used in CoreConfig internally
+				throw new IllegalStateException();
 			}
+			if (reload) {
 			try {
 				return getObjectLoader(curs, path, id);
 			} catch (FileNotFoundException noFile) {
@@ -183,9 +202,10 @@ ObjectLoader open(WindowCursor curs, AnyObjectId id) throws IOException {
 				}
 				if (LOG.isDebugEnabled()) {
 					LOG.debug(MessageFormat.format(
-							JGitText.get().looseObjectHandleIsStale, id.name(),
-							Integer.valueOf(readAttempts), Integer.valueOf(
-									MAX_LOOSE_OBJECT_STALE_READ_ATTEMPTS)));
+								JGitText.get().looseObjectHandleIsStale,
+								id.name(), Integer.valueOf(retries),
+								Integer.valueOf(MAX_STALE_READ_RETRIES)));
+					}
 				}
 			}
 		}
@@ -211,7 +231,7 @@ ObjectLoader getObjectLoader(WindowCursor curs, File path, AnyObjectId id)
 		try {
 			return getObjectLoaderWithoutRefresh(curs, path, id);
 		} catch (FileNotFoundException e) {
-			if (trustFolderStat) {
+			if (trustLooseObjectStat == TrustStat.ALWAYS) {
 				throw e;
 			}
 			try (InputStream stream = Files
@@ -248,7 +268,7 @@ long getSize(WindowCursor curs, AnyObjectId id) throws IOException {
 			return getSizeWithoutRefresh(curs, id);
 		} catch (FileNotFoundException noFile) {
 			try {
-				if (trustFolderStat) {
+				if (trustLooseObjectStat == TrustStat.ALWAYS) {
 					throw noFile;
 				}
 				try (InputStream stream = Files
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 9f27f4b..746e124 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
@@ -28,6 +28,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.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig;
@@ -110,7 +111,7 @@ public class ObjectDirectoryPackParser extends PackParser {
 	 * @param version
 	 *            the version to write. The special version 0 designates the
 	 *            oldest (most compatible) format available for the objects.
-	 * @see PackIndexWriter
+	 * @see BasePackIndexWriter
 	 */
 	public void setIndexVersion(int version) {
 		indexVersion = version;
@@ -386,9 +387,9 @@ private void writeIdx() throws IOException {
 		try (FileOutputStream os = new FileOutputStream(tmpIdx)) {
 			final PackIndexWriter iw;
 			if (indexVersion <= 0)
-				iw = PackIndexWriter.createOldestPossible(os, list);
+				iw = BasePackIndexWriter.createOldestPossible(os, list);
 			else
-				iw = PackIndexWriter.createVersion(os, indexVersion);
+				iw = BasePackIndexWriter.createVersion(os, indexVersion);
 			iw.write(list, packHash);
 			os.getChannel().force(true);
 		}
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
index f87329c..5813d39 100644
--- 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
@@ -95,6 +95,9 @@ public class Pack implements Iterable<PackIndex.MutableEntry> {
 
 	private RandomAccessFile fd;
 
+	/** For managing open/close accounting of {@link #fd}. */
+	private final Object activeLock = new Object();
+
 	/** Serializes reads performed against {@link #fd}. */
 	private final Object readLock = new Object();
 
@@ -113,13 +116,13 @@ public class Pack implements Iterable<PackIndex.MutableEntry> {
 	private volatile Exception invalidatingCause;
 
 	@Nullable
-	private PackFile bitmapIdxFile;
+	private volatile PackFile bitmapIdxFile;
 
 	private AtomicInteger transientErrorCount = new AtomicInteger();
 
 	private byte[] packChecksum;
 
-	private volatile Optionally<PackIndex> loadedIdx = Optionally.empty();
+	private Optionally<PackIndex> loadedIdx = Optionally.empty();
 
 	private Optionally<PackReverseIndex> reverseIdx = Optionally.empty();
 
@@ -159,60 +162,54 @@ public Pack(Config cfg, File packFile, @Nullable PackFile bitmapIdxFile) {
 		length = Long.MAX_VALUE;
 	}
 
-	private PackIndex idx() throws IOException {
+	private synchronized PackIndex idx() throws IOException {
 		Optional<PackIndex> optional = loadedIdx.getOptional();
 		if (optional.isPresent()) {
 			return optional.get();
 		}
-		synchronized (this) {
-			optional = loadedIdx.getOptional();
-			if (optional.isPresent()) {
-				return optional.get();
+		if (invalid) {
+			throw new PackInvalidException(packFile, invalidatingCause);
+		}
+		try {
+			long start = System.currentTimeMillis();
+			PackFile idxFile = packFile.create(INDEX);
+			PackIndex 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 (invalid) {
-				throw new PackInvalidException(packFile, invalidatingCause);
-			}
-			try {
-				long start = System.currentTimeMillis();
-				PackFile idxFile = packFile.create(INDEX);
-				PackIndex 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(),
-									PackExt.PACK.getExtension(),
-									Hex.toHexString(packChecksum),
-									PackExt.INDEX.getExtension(),
-									Hex.toHexString(idx.packChecksum)));
-				}
-				loadedIdx = optionally(idx);
-				return 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;
+				packChecksum = idx.getChecksum();
+				fileSnapshot.setChecksum(
+						ObjectId.fromRaw(packChecksum));
+			} else if (!Arrays.equals(packChecksum,
+					idx.getChecksum())) {
+				throw new PackMismatchException(MessageFormat
+						.format(JGitText.get().packChecksumMismatch,
+								packFile.getPath(),
+								PackExt.PACK.getExtension(),
+								Hex.toHexString(packChecksum),
+								PackExt.INDEX.getExtension(),
+							Hex.toHexString(idx.getChecksum())));
 			}
+			loadedIdx = optionally(idx);
+			return 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;
 		}
 	}
+
 	/**
 	 * Get the File object which locates this pack on disk.
 	 *
@@ -296,15 +293,28 @@ void resolve(Set<ObjectId> matches, AbbreviatedObjectId id, int matchLimit)
 	}
 
 	/**
-	 * Close the resources utilized by this repository
+	 * Close the resources utilized by these pack files
+	 *
+	 * @param packs
+	 *            packs to close
+	 */
+	public static void close(Set<Pack> packs) {
+		WindowCache.purge(packs);
+		packs.forEach(p -> p.closeIndices());
+	}
+
+	/**
+	 * Close the resources utilized by this pack file
 	 */
 	public void close() {
 		WindowCache.purge(this);
-		synchronized (this) {
-			loadedIdx.clear();
-			reverseIdx.clear();
-			bitmapIdx.clear();
-		}
+		closeIndices();
+	}
+
+	private synchronized void closeIndices() {
+		loadedIdx.clear();
+		reverseIdx.clear();
+		bitmapIdx.clear();
 	}
 
 	/**
@@ -416,185 +426,202 @@ private void copyAsIs2(PackOutputStream out, LocalObjectToPack src,
 		final CRC32 crc2 = validate ? new CRC32() : null;
 		final byte[] buf = out.getCopyBuffer();
 
+		boolean isHeaderWritten = false;
 		// 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);
+			readFully(src.offset, buf, 0, 20, curs);
 
-			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;
+			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;
 			}
-		} catch (DataFormatException dataFormat) {
-			setCorrupt(src.offset);
 
-			CorruptObjectException corruptObject = new CorruptObjectException(
-					MessageFormat.format(
-							JGitText.get().objectAtHasBadZlibStream,
-							Long.valueOf(src.offset), getPackFile()),
-					dataFormat);
+			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);
+				}
 
-			throw new StoredObjectRepresentationNotAvailableException(
-					corruptObject);
+				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);
+			}
 
-		} catch (IOException ioError) {
-			throw new StoredObjectRepresentationNotAvailableException(ioError);
-		}
+			final long dataOffset = src.offset + headerCnt;
+			final long dataLength = src.length;
+			final long expectedCRC;
+			final ByteArrayWindow quickCopy;
 
-		if (quickCopy != null) {
-			// The entire object fits into a single byte array window slice,
-			// and we have it pinned.  Write this out without copying.
+			// Verify the object isn't corrupt before sending. If it is,
+			// we report it missing instead.
 			//
-			out.writeHeader(src, inflatedLength);
-			quickCopy.write(out, dataOffset, (int) dataLength);
+			try {
+				quickCopy = curs.quickCopy(this, dataOffset, 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) {
+				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(
+						corruptObject);
+			}
+
+			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);
+				isHeaderWritten = true;
+				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);
+				isHeaderWritten = true;
+				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.
+				//
 				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;
+					if (validate) {
+						assert(crc2 != null);
+						crc2.update(buf, 0, n);
+					}
 					cnt -= n;
+					if (!isHeaderWritten) {
+						if (invalid && cnt > 0) {
+							// Since this is not the last iteration and the packfile is invalid,
+							// better to assume the iterations will not all complete here while
+							// it is still likely recoverable.
+							throw new StoredObjectRepresentationNotAvailableException(invalidatingCause);
+						}
+						out.writeHeader(src, inflatedLength);
+						isHeaderWritten = true;
+					}
+					out.write(buf, 0, n);
+					pos += 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()));
+					if (crc2.getValue() != expectedCRC) {
+						throw new CorruptObjectException(MessageFormat.format(
+								JGitText.get().objectAtHasBadZlibStream,
+								Long.valueOf(src.offset), getPackFile()));
+					}
 				}
 			}
+		} catch (IOException ioError) {
+			if (!isHeaderWritten) {
+				throw new StoredObjectRepresentationNotAvailableException(ioError);
+			}
+			throw ioError;
 		}
 	}
 
@@ -621,42 +648,53 @@ private void readFully(final long position, final byte[] dstbuf,
 			throw new EOFException();
 	}
 
-	private synchronized void beginCopyAsIs()
+	private void beginCopyAsIs()
 			throws StoredObjectRepresentationNotAvailableException {
-		if (++activeCopyRawData == 1 && activeWindows == 0) {
-			try {
-				doOpen();
-			} catch (IOException thisPackNotValid) {
-				throw new StoredObjectRepresentationNotAvailableException(
-						thisPackNotValid);
+		synchronized (activeLock) {
+			if (++activeCopyRawData == 1 && activeWindows == 0) {
+				try {
+					doOpen();
+				} catch (IOException thisPackNotValid) {
+					throw new StoredObjectRepresentationNotAvailableException(
+							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;
+	private void endCopyAsIs() {
+		synchronized (activeLock) {
+			if (--activeCopyRawData == 0 && activeWindows == 0) {
+				doClose();
+			}
 		}
-		return false;
 	}
 
-	synchronized boolean endWindowCache() {
-		final boolean r = --activeWindows == 0;
-		if (r && activeCopyRawData == 0)
-			doClose();
-		return r;
+	boolean beginWindowCache() throws IOException {
+		synchronized (activeLock) {
+			if (++activeWindows == 1) {
+				if (activeCopyRawData == 0) {
+					doOpen();
+				}
+				return true;
+			}
+			return false;
+		}
+	}
+
+	boolean endWindowCache() {
+		synchronized (activeLock) {
+			boolean r = --activeWindows == 0;
+			if (r && activeCopyRawData == 0) {
+				doClose();
+			}
+			return r;
+		}
 	}
 
 	private void doOpen() throws IOException {
 		if (invalid) {
-			openFail(true, invalidatingCause);
+			openFail(invalidatingCause);
 			throw new PackInvalidException(packFile, invalidatingCause);
 		}
 		try {
@@ -667,39 +705,41 @@ private void doOpen() throws IOException {
 			}
 		} catch (InterruptedIOException e) {
 			// don't invalidate the pack, we are interrupted from another thread
-			openFail(false, e);
+			openFail(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);
+			if (!packFile.exists()) {
+				// Failure to open an existing file may be related to a temporary lack of resources
+				// (e.g. max open files)
+				invalid = true;
+			}
+			openFail(fn);
 			throw fn;
 		} catch (EOFException | AccessDeniedException | NoSuchFileException
 				| CorruptObjectException | NoPackSignatureException
 				| PackMismatchException | UnpackException
 				| UnsupportedPackIndexVersionException
 				| UnsupportedPackVersionException pe) {
-			// exceptions signaling permanent problems with a pack
-			openFail(true, pe);
+			invalid = true; // exceptions signaling permanent problems with a pack
+			openFail(pe);
 			throw pe;
 		} catch (IOException ioe) {
-			// mark this packfile as invalid when NFS stale file handle error
-			// occur
-			openFail(FileUtils.isStaleFileHandleInCausalChain(ioe), ioe);
+			if (FileUtils.isStaleFileHandleInCausalChain(ioe)) {
+				invalid = true;
+			}
+			openFail(ioe);
 			throw ioe;
 		} catch (RuntimeException ge) {
 			// generic exceptions could be transient so we should not mark the
 			// pack invalid to avoid false MissingObjectExceptions
-			openFail(false, ge);
+			openFail(ge);
 			throw ge;
 		}
 	}
 
-	private void openFail(boolean invalidate, Exception cause) {
+	private void openFail(Exception cause) {
 		activeWindows = 0;
 		activeCopyRawData = 0;
-		invalid = invalidate;
 		invalidatingCause = cause;
 		doClose();
 	}
@@ -791,7 +831,7 @@ private void onOpenPack() throws IOException {
 					MessageFormat.format(JGitText.get().packChecksumMismatch,
 							getPackFile(), PackExt.PACK.getExtension(),
 							Hex.toHexString(buf), PackExt.INDEX.getExtension(),
-							Hex.toHexString(idx.packChecksum)));
+							Hex.toHexString(idx.getChecksum())));
 		}
 	}
 
@@ -1173,17 +1213,8 @@ synchronized PackBitmapIndex getBitmapIndex() throws IOException {
 		return null;
 	}
 
-	synchronized void refreshBitmapIndex(PackFile bitmapIndexFile) {
-		this.bitmapIdx = Optionally.empty();
-		this.invalid = false;
+	void setBitmapIndexFile(PackFile bitmapIndexFile) {
 		this.bitmapIdxFile = bitmapIndexFile;
-		try {
-			getBitmapIndex();
-		} catch (IOException e) {
-			LOG.warn(JGitText.get().bitmapFailedToGet, bitmapIdxFile, e);
-			this.bitmapIdx = Optionally.empty();
-			this.bitmapIdxFile = null;
-		}
 	}
 
 	private synchronized PackReverseIndex getReverseIdx() throws IOException {
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
index 8221cff..f50c17e 100644
--- 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
@@ -17,6 +17,8 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -24,6 +26,7 @@
 import java.util.Collections;
 import java.util.EnumMap;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -41,7 +44,8 @@
 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.CoreConfig;
+import org.eclipse.jgit.lib.CoreConfig.TrustStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.util.FileUtils;
@@ -71,7 +75,7 @@ class PackDirectory {
 
 	private final AtomicReference<PackList> packList;
 
-	private final boolean trustFolderStat;
+	private final TrustStat trustPackStat;
 
 	/**
 	 * Initialize a reference to an on-disk 'pack' directory.
@@ -85,14 +89,7 @@ class PackDirectory {
 		this.config = config;
 		this.directory = directory;
 		packList = new AtomicReference<>(NO_PACKS);
-
-		// 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 folder's size,
-		// modification time, and key (inode) and assume that no new pack files
-		// can be in this folder if these attributes have not changed.
-		trustFolderStat = config.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
-				ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true);
+		trustPackStat = config.get(CoreConfig.KEY).getTrustPackStat();
 	}
 
 	/**
@@ -111,9 +108,7 @@ void create() throws IOException {
 	void close() {
 		PackList packs = packList.get();
 		if (packs != NO_PACKS && packList.compareAndSet(packs, NO_PACKS)) {
-			for (Pack p : packs.packs) {
-				p.close();
-			}
+			Pack.close(Set.of(packs.packs));
 		}
 	}
 
@@ -314,38 +309,42 @@ private int checkRescanPackThreshold(int retries, PackMismatchException e)
 	}
 
 	private void handlePackError(IOException e, Pack p) {
-		String warnTmpl = null;
+		String warnTemplate = null;
+		String debugTemplate = null;
 		int transientErrorCount = 0;
-		String errTmpl = JGitText.get().exceptionWhileReadingPack;
+		String errorTemplate = JGitText.get().exceptionWhileReadingPack;
 		if ((e instanceof CorruptObjectException)
 				|| (e instanceof PackInvalidException)) {
-			warnTmpl = JGitText.get().corruptPack;
-			LOG.warn(MessageFormat.format(warnTmpl,
+			warnTemplate = JGitText.get().corruptPack;
+			LOG.warn(MessageFormat.format(warnTemplate,
 					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;
+				errorTemplate = JGitText.get().packInaccessible;
 				transientErrorCount = p.incrementTransientErrorCount();
 			} else {
-				warnTmpl = JGitText.get().packWasDeleted;
+				debugTemplate = JGitText.get().packWasDeleted;
 				remove(p);
 			}
 		} else if (FileUtils.isStaleFileHandleInCausalChain(e)) {
-			warnTmpl = JGitText.get().packHandleIsStale;
+			warnTemplate = JGitText.get().packHandleIsStale;
 			remove(p);
 		} else {
 			transientErrorCount = p.incrementTransientErrorCount();
 		}
-		if (warnTmpl != null) {
-			LOG.warn(MessageFormat.format(warnTmpl,
+		if (warnTemplate != null) {
+			LOG.warn(MessageFormat.format(warnTemplate,
 					p.getPackFile().getAbsolutePath()), e);
+		} else if (debugTemplate != null) {
+			LOG.debug(MessageFormat.format(debugTemplate,
+				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,
+				LOG.error(MessageFormat.format(errorTemplate,
 						p.getPackFile().getAbsolutePath(),
 						Integer.valueOf(transientErrorCount)), e);
 			}
@@ -362,8 +361,26 @@ private boolean doLogExponentialBackoff(int n) {
 	}
 
 	boolean searchPacksAgain(PackList old) {
-		return (!trustFolderStat || old.snapshot.isModified(directory))
-				&& old != scanPacks(old);
+		switch (trustPackStat) {
+		case NEVER:
+			break;
+		case AFTER_OPEN:
+			try (InputStream stream = Files
+					.newInputStream(directory.toPath())) {
+				// open the pack directory to refresh attributes (on some NFS clients)
+			} catch (IOException e) {
+				// ignore
+			}
+			//$FALL-THROUGH$
+		case ALWAYS:
+			if (!old.snapshot.isModified(directory)) {
+				return false;
+			}
+			break;
+		case INHERIT:
+			// only used in CoreConfig internally
+		}
+		return old != scanPacks(old);
 	}
 
 	void insert(Pack pack) {
@@ -460,12 +477,9 @@ private PackList scanPacksImpl(PackList old) {
 					&& !oldPack.getFileSnapshot().isModified(packFile)) {
 				forReuse.remove(packFile.getName());
 				list.add(oldPack);
-				try {
-					if(oldPack.getBitmapIndex() == null) {
-						oldPack.refreshBitmapIndex(packFilesByExt.get(BITMAP_INDEX));
-					}
-				} catch (IOException e) {
-					LOG.warn(JGitText.get().bitmapAccessErrorForPackfile, oldPack.getPackName(), e);
+				PackFile bitMaps = packFilesByExt.get(BITMAP_INDEX);
+				if (bitMaps != null) {
+					oldPack.setBitmapIndexFile(bitMaps);
 				}
 				continue;
 			}
@@ -484,9 +498,7 @@ private PackList scanPacksImpl(PackList old) {
 			return old;
 		}
 
-		for (Pack p : forReuse.values()) {
-			p.close();
-		}
+		Pack.close(new HashSet<>(forReuse.values()));
 
 		if (list.isEmpty()) {
 			return new PackList(snapshot, NO_PACKS.packs);
@@ -545,7 +557,7 @@ private Map<String, Map<PackExt, PackFile>> getPackFilesByExtById() {
 		for (String name : nameList) {
 			try {
 				PackFile pack = new PackFile(directory, name);
-				if (pack.getPackExt() != null) {
+				if (pack.getPackExt() != null && !pack.isTmpGCFile()) {
 					Map<PackExt, PackFile> packByExt = packFilesByExtById
 							.get(pack.getId());
 					if (packByExt == null) {
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 19979d0..c9b05ad 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
@@ -27,6 +27,7 @@ public class PackFile extends File {
 	private static final long serialVersionUID = 1L;
 
 	private static final String PREFIX = "pack-"; //$NON-NLS-1$
+	private static final String TMP_GC_PREFIX = ".tmp-"; //$NON-NLS-1$
 
 	private final String base; // PREFIX + id i.e.
 								// pack-0123456789012345678901234567890123456789
@@ -126,6 +127,13 @@ public PackExt getPackExt() {
 	}
 
 	/**
+	 * @return whether the file is a temporary GC file
+	 */
+	public boolean isTmpGCFile() {
+		return id.startsWith(TMP_GC_PREFIX);
+	}
+
+	/**
 	 * Create a new similar PackFile with the given extension instead.
 	 *
 	 * @param ext
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 c2c3775..b3e4efb 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
@@ -42,8 +42,8 @@
  * by ObjectId.
  * </p>
  */
-public abstract class PackIndex
-		implements Iterable<PackIndex.MutableEntry>, ObjectIdSet {
+public interface PackIndex
+		extends Iterable<PackIndex.MutableEntry>, ObjectIdSet {
 	/**
 	 * Open an existing pack <code>.idx</code> file for reading.
 	 * <p>
@@ -61,7 +61,7 @@ public abstract class PackIndex
 	 *             the file exists but could not be read due to security errors,
 	 *             unrecognized data version, or unexpected data corruption.
 	 */
-	public static PackIndex open(File idxFile) throws IOException {
+	static PackIndex open(File idxFile) throws IOException {
 		try (SilentFileInputStream fd = new SilentFileInputStream(
 				idxFile)) {
 			return read(fd);
@@ -92,7 +92,7 @@ public static PackIndex open(File idxFile) throws IOException {
 	 * @throws org.eclipse.jgit.errors.CorruptObjectException
 	 *             the stream does not contain a valid pack index.
 	 */
-	public static PackIndex read(InputStream fd) throws IOException,
+	static PackIndex read(InputStream fd) throws IOException,
 			CorruptObjectException {
 		final byte[] hdr = new byte[8];
 		IO.readFully(fd, hdr, 0, hdr.length);
@@ -109,16 +109,13 @@ public static PackIndex read(InputStream fd) throws IOException,
 	}
 
 	private static boolean isTOC(byte[] h) {
-		final byte[] toc = PackIndexWriter.TOC;
+		final byte[] toc = BasePackIndexWriter.TOC;
 		for (int i = 0; i < toc.length; i++)
 			if (h[i] != toc[i])
 				return false;
 		return true;
 	}
 
-	/** Footer checksum applied on the bottom of the pack file. */
-	protected byte[] packChecksum;
-
 	/**
 	 * Determine if an object is contained within the pack file.
 	 *
@@ -126,12 +123,12 @@ private static boolean isTOC(byte[] h) {
 	 *            the object to look for. Must not be null.
 	 * @return true if the object is listed in this index; false otherwise.
 	 */
-	public boolean hasObject(AnyObjectId id) {
+	default boolean hasObject(AnyObjectId id) {
 		return findOffset(id) != -1;
 	}
 
 	@Override
-	public boolean contains(AnyObjectId id) {
+	default boolean contains(AnyObjectId id) {
 		return findOffset(id) != -1;
 	}
 
@@ -147,7 +144,7 @@ public boolean contains(AnyObjectId id) {
 	 * </p>
 	 */
 	@Override
-	public abstract Iterator<MutableEntry> iterator();
+	Iterator<MutableEntry> iterator();
 
 	/**
 	 * Obtain the total number of objects described by this index.
@@ -155,7 +152,7 @@ public boolean contains(AnyObjectId id) {
 	 * @return number of objects in this index, and likewise in the associated
 	 *         pack that this index was generated from.
 	 */
-	public abstract long getObjectCount();
+	long getObjectCount();
 
 	/**
 	 * Obtain the total number of objects needing 64 bit offsets.
@@ -163,7 +160,7 @@ public boolean contains(AnyObjectId id) {
 	 * @return number of objects in this index using a 64 bit offset; that is an
 	 *         object positioned after the 2 GB position within the file.
 	 */
-	public abstract long getOffset64Count();
+	long getOffset64Count();
 
 	/**
 	 * Get ObjectId for the n-th object entry returned by {@link #iterator()}.
@@ -185,7 +182,7 @@ public boolean contains(AnyObjectId id) {
 	 *            is 0, the second is 1, etc.
 	 * @return the ObjectId for the corresponding entry.
 	 */
-	public abstract ObjectId getObjectId(long nthPosition);
+	ObjectId getObjectId(long nthPosition);
 
 	/**
 	 * Get ObjectId for the n-th object entry returned by {@link #iterator()}.
@@ -209,7 +206,7 @@ public boolean contains(AnyObjectId id) {
 	 *            negative, but still valid.
 	 * @return the ObjectId for the corresponding entry.
 	 */
-	public final ObjectId getObjectId(int nthPosition) {
+	default ObjectId getObjectId(int nthPosition) {
 		if (nthPosition >= 0)
 			return getObjectId((long) nthPosition);
 		final int u31 = nthPosition >>> 1;
@@ -228,7 +225,7 @@ public final ObjectId getObjectId(int nthPosition) {
 	 *            etc. Positions past 2**31-1 are negative, but still valid.
 	 * @return the offset in a pack for the corresponding entry.
 	 */
-	protected abstract long getOffset(long nthPosition);
+	long getOffset(long nthPosition);
 
 	/**
 	 * Locate the file offset position for the requested object.
@@ -239,7 +236,7 @@ public final ObjectId getObjectId(int nthPosition) {
 	 *         object does not exist in this index and is thus not stored in the
 	 *         associated pack.
 	 */
-	public abstract long findOffset(AnyObjectId objId);
+	long findOffset(AnyObjectId objId);
 
 	/**
 	 * Locate the position of this id in the list of object-ids in the index
@@ -250,7 +247,7 @@ public final ObjectId getObjectId(int nthPosition) {
 	 *         of ids stored in this index; -1 if the object does not exist in
 	 *         this index and is thus not stored in the associated pack.
 	 */
-	public abstract int findPosition(AnyObjectId objId);
+	int findPosition(AnyObjectId objId);
 
 	/**
 	 * Retrieve stored CRC32 checksum of the requested object raw-data
@@ -264,7 +261,7 @@ public final ObjectId getObjectId(int nthPosition) {
 	 * @throws java.lang.UnsupportedOperationException
 	 *             when this index doesn't support CRC32 checksum
 	 */
-	public abstract long findCRC32(AnyObjectId objId)
+	long findCRC32(AnyObjectId objId)
 			throws MissingObjectException, UnsupportedOperationException;
 
 	/**
@@ -272,7 +269,7 @@ public abstract long findCRC32(AnyObjectId objId)
 	 *
 	 * @return true if CRC32 is stored, false otherwise
 	 */
-	public abstract boolean hasCRC32Support();
+	boolean hasCRC32Support();
 
 	/**
 	 * Find objects matching the prefix abbreviation.
@@ -288,8 +285,8 @@ public abstract long findCRC32(AnyObjectId objId)
 	 * @throws java.io.IOException
 	 *             the index cannot be read.
 	 */
-	public abstract void resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
-			int matchLimit) throws IOException;
+	void resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
+				 int matchLimit) throws IOException;
 
 	/**
 	 * Get pack checksum
@@ -297,18 +294,18 @@ public abstract void resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
 	 * @return the checksum of the pack; caller must not modify it
 	 * @since 5.5
 	 */
-	public byte[] getChecksum() {
-		return packChecksum;
-	}
+	byte[] getChecksum();
 
 	/**
 	 * Represent mutable entry of pack index consisting of object id and offset
 	 * in pack (both mutable).
 	 *
 	 */
-	public static class MutableEntry {
+	class MutableEntry {
+		/** Buffer of the ObjectId visited by the EntriesIterator. */
 		final MutableObjectId idBuffer = new MutableObjectId();
 
+		/** Offset into the packfile of the current object. */
 		long offset;
 
 		/**
@@ -326,7 +323,6 @@ public long getOffset() {
 		 * @return hex string describing the object id of this entry.
 		 */
 		public String name() {
-			ensureId();
 			return idBuffer.name();
 		}
 
@@ -336,7 +332,6 @@ public String name() {
 		 * @return a copy of the object id.
 		 */
 		public ObjectId toObjectId() {
-			ensureId();
 			return idBuffer.toObjectId();
 		}
 
@@ -347,27 +342,64 @@ public ObjectId toObjectId() {
 		 */
 		public MutableEntry cloneEntry() {
 			final MutableEntry r = new MutableEntry();
-			ensureId();
 			r.idBuffer.fromObjectId(idBuffer);
 			r.offset = offset;
 			return r;
 		}
 
-		void ensureId() {
-			// Override in implementations.
+		/**
+		 * Similar to {@link Comparable#compareTo(Object)}, using only the
+		 * object id in the entry.
+		 *
+		 * @param other
+		 *            Another mutable entry (probably from another index)
+		 *
+		 * @return a negative integer, zero, or a positive integer as this
+		 *         object is less than, equal to, or greater than the specified
+		 *         object.
+		 */
+		public int compareBySha1To(MutableEntry other) {
+			return idBuffer.compareTo(other.idBuffer);
+		}
+
+		/**
+		 * Copy the current ObjectId to dest
+		 * <p>
+		 * Like {@link #toObjectId()}, but reusing the destination instead of
+		 * creating a new ObjectId instance.
+		 *
+		 * @param dest
+		 *            destination for the object id
+		 */
+		public void copyOidTo(MutableObjectId dest) {
+			dest.fromObjectId(idBuffer);
 		}
 	}
 
+	/**
+	 * Base implementation of the iterator over index entries.
+	 */
 	abstract class EntriesIterator implements Iterator<MutableEntry> {
-		protected final MutableEntry entry = initEntry();
+		private final long objectCount;
 
-		protected long returnedNumber = 0;
+		private final MutableEntry entry = new MutableEntry();
 
-		protected abstract MutableEntry initEntry();
+		/** Counts number of entries accessed so far. */
+		private long returnedNumber = 0;
+
+		/**
+		 * Construct an iterator that can move objectCount times forward.
+		 *
+		 * @param objectCount
+		 *            the number of objects in the PackFile.
+		 */
+		protected EntriesIterator(long objectCount) {
+			this.objectCount = objectCount;
+		}
 
 		@Override
 		public boolean hasNext() {
-			return returnedNumber < getObjectCount();
+			return returnedNumber < objectCount;
 		}
 
 		/**
@@ -375,7 +407,55 @@ public boolean hasNext() {
 		 * element.
 		 */
 		@Override
-		public abstract MutableEntry next();
+		public MutableEntry next() {
+			readNext();
+			returnedNumber++;
+			return entry;
+		}
+
+		/**
+		 * Used by subclasses to load the next entry into the MutableEntry.
+		 * <p>
+		 * Subclasses are expected to populate the entry with
+		 * {@link #setIdBuffer} and {@link #setOffset}.
+		 */
+		protected abstract void readNext();
+
+		/**
+		 * Copies to the entry an {@link ObjectId} from the int buffer and
+		 * position idx
+		 *
+		 * @param raw
+		 *            the raw data
+		 * @param idx
+		 *            the index into {@code raw}
+		 */
+		protected void setIdBuffer(int[] raw, int idx) {
+			entry.idBuffer.fromRaw(raw, idx);
+		}
+
+		/**
+		 * Copies to the entry an {@link ObjectId} from the byte array at
+		 * position idx.
+		 *
+		 * @param raw
+		 *            the raw data
+		 * @param idx
+		 *            the index into {@code raw}
+		 */
+		protected void setIdBuffer(byte[] raw, int idx) {
+			entry.idBuffer.fromRaw(raw, idx);
+		}
+
+		/**
+		 * Sets the {@code offset} to the entry
+		 *
+		 * @param offset
+		 *            the offset in the pack file
+		 */
+		protected void setOffset(long offset) {
+			entry.offset = offset;
+		}
 
 		@Override
 		public void remove() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java
index 5180df4..be48358 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV1.java
@@ -29,13 +29,16 @@
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.NB;
 
-class PackIndexV1 extends PackIndex {
+class PackIndexV1 implements PackIndex {
 	private static final int IDX_HDR_LEN = 256 * 4;
 
 	private static final int RECORD_SIZE = 4 + Constants.OBJECT_ID_LENGTH;
 
 	private final long[] idxHeader;
 
+	/** Footer checksum applied on the bottom of the pack file. */
+	protected byte[] packChecksum;
+
 	byte[][] idxdata;
 
 	private long objectCnt;
@@ -118,7 +121,7 @@ public ObjectId getObjectId(long nthPosition) {
 	}
 
 	@Override
-	protected long getOffset(long nthPosition) {
+	public long getOffset(long nthPosition) {
 		final int levelOne = findLevelOne(nthPosition);
 		final int levelTwo = getLevelTwo(nthPosition, levelOne);
 		final int p = (4 + Constants.OBJECT_ID_LENGTH) * levelTwo;
@@ -200,7 +203,7 @@ public boolean hasCRC32Support() {
 
 	@Override
 	public Iterator<MutableEntry> iterator() {
-		return new IndexV1Iterator();
+		return new EntriesIteratorV1(this);
 	}
 
 	@Override
@@ -238,32 +241,35 @@ private static int idOffset(int mid) {
 		return (RECORD_SIZE * mid) + 4;
 	}
 
-	private class IndexV1Iterator extends EntriesIterator {
-		int levelOne;
+	@Override
+	public byte[] getChecksum() {
+		return packChecksum;
+	}
 
-		int levelTwo;
+	private static class EntriesIteratorV1 extends EntriesIterator {
+		private int levelOne;
 
-		@Override
-		protected MutableEntry initEntry() {
-			return new MutableEntry() {
-				@Override
-				protected void ensureId() {
-					idBuffer.fromRaw(idxdata[levelOne], levelTwo
-							- Constants.OBJECT_ID_LENGTH);
-				}
-			};
+		private int levelTwo;
+
+		private final PackIndexV1 packIndex;
+
+		private EntriesIteratorV1(PackIndexV1 packIndex) {
+			super(packIndex.objectCnt);
+			this.packIndex = packIndex;
 		}
 
 		@Override
-		public MutableEntry next() {
-			for (; levelOne < idxdata.length; levelOne++) {
-				if (idxdata[levelOne] == null)
+		protected void readNext() {
+			for (; levelOne < packIndex.idxdata.length; levelOne++) {
+				if (packIndex.idxdata[levelOne] == null)
 					continue;
-				if (levelTwo < idxdata[levelOne].length) {
-					entry.offset = NB.decodeUInt32(idxdata[levelOne], levelTwo);
-					levelTwo += Constants.OBJECT_ID_LENGTH + 4;
-					returnedNumber++;
-					return entry;
+				if (levelTwo < packIndex.idxdata[levelOne].length) {
+					super.setOffset(NB.decodeUInt32(packIndex.idxdata[levelOne],
+							levelTwo));
+					this.levelTwo += Constants.OBJECT_ID_LENGTH + 4;
+					super.setIdBuffer(packIndex.idxdata[levelOne],
+							levelTwo - Constants.OBJECT_ID_LENGTH);
+					return;
 				}
 				levelTwo = 0;
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java
index 751b62d..36e54fc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexV2.java
@@ -28,7 +28,7 @@
 import org.eclipse.jgit.util.NB;
 
 /** Support for the pack index v2 format. */
-class PackIndexV2 extends PackIndex {
+class PackIndexV2 implements PackIndex {
 	private static final long IS_O64 = 1L << 31;
 
 	private static final int FANOUT = 256;
@@ -37,6 +37,9 @@ class PackIndexV2 extends PackIndex {
 
 	private static final byte[] NO_BYTES = {};
 
+	/** Footer checksum applied on the bottom of the pack file. */
+	protected byte[] packChecksum;
+
 	private long objectCnt;
 
 	private final long[] fanoutTable;
@@ -221,7 +224,7 @@ public boolean hasCRC32Support() {
 
 	@Override
 	public Iterator<MutableEntry> iterator() {
-		return new EntriesIteratorV2();
+		return new EntriesIteratorV2(this);
 	}
 
 	@Override
@@ -281,37 +284,39 @@ else if (cmp == 0) {
 		return -1;
 	}
 
-	private class EntriesIteratorV2 extends EntriesIterator {
-		int levelOne;
+	@Override
+	public byte[] getChecksum() {
+		return packChecksum;
+	}
 
-		int levelTwo;
+	private static class EntriesIteratorV2 extends EntriesIterator {
+		private int levelOne = 0;
 
-		@Override
-		protected MutableEntry initEntry() {
-			return new MutableEntry() {
-				@Override
-				protected void ensureId() {
-					idBuffer.fromRaw(names[levelOne], levelTwo
-							- Constants.OBJECT_ID_LENGTH / 4);
-				}
-			};
+		private int levelTwo = 0;
+
+		private final PackIndexV2 packIndex;
+
+		private EntriesIteratorV2(PackIndexV2 packIndex) {
+			super(packIndex.objectCnt);
+			this.packIndex = packIndex;
 		}
 
 		@Override
-		public MutableEntry next() {
-			for (; levelOne < names.length; levelOne++) {
-				if (levelTwo < names[levelOne].length) {
+		protected void readNext() {
+			for (; levelOne < packIndex.names.length; levelOne++) {
+				if (levelTwo < packIndex.names[levelOne].length) {
 					int idx = levelTwo / (Constants.OBJECT_ID_LENGTH / 4) * 4;
-					long offset = NB.decodeUInt32(offset32[levelOne], idx);
+					long offset = NB.decodeUInt32(packIndex.offset32[levelOne],
+							idx);
 					if ((offset & IS_O64) != 0) {
 						idx = (8 * (int) (offset & ~IS_O64));
-						offset = NB.decodeUInt64(offset64, idx);
+						offset = NB.decodeUInt64(packIndex.offset64, idx);
 					}
-					entry.offset = offset;
-
-					levelTwo += Constants.OBJECT_ID_LENGTH / 4;
-					returnedNumber++;
-					return entry;
+					super.setOffset(offset);
+					this.levelTwo += Constants.OBJECT_ID_LENGTH / 4;
+					super.setIdBuffer(packIndex.names[levelOne],
+							levelTwo - Constants.OBJECT_ID_LENGTH / 4);
+					return;
 				}
 				levelTwo = 0;
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java
index 7e28b5e..f0b6193 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV1.java
@@ -21,10 +21,10 @@
 /**
  * Creates the version 1 (old style) pack table of contents files.
  *
- * @see PackIndexWriter
+ * @see BasePackIndexWriter
  * @see PackIndexV1
  */
-class PackIndexWriterV1 extends PackIndexWriter {
+class PackIndexWriterV1 extends BasePackIndexWriter {
 	static boolean canStore(PackedObjectInfo oe) {
 		// We are limited to 4 GB per pack as offset is 32 bit unsigned int.
 		//
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java
index fc5ef61..b72b35a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndexWriterV2.java
@@ -19,10 +19,10 @@
 /**
  * Creates the version 2 pack table of contents files.
  *
- * @see PackIndexWriter
+ * @see BasePackIndexWriter
  * @see PackIndexV2
  */
-class PackIndexWriterV2 extends PackIndexWriter {
+class PackIndexWriterV2 extends BasePackIndexWriter {
 	private static final int MAX_OFFSET_32 = 0x7fffffff;
 	private static final int IS_OFFSET_64 = 0x80000000;
 
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 1b092a3..55e047b 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
@@ -77,6 +77,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -320,7 +321,8 @@ public void flush() throws IOException {
 	private static void writePackIndex(File idx, byte[] packHash,
 			List<PackedObjectInfo> list) throws IOException {
 		try (OutputStream os = new FileOutputStream(idx)) {
-			PackIndexWriter w = PackIndexWriter.createVersion(os, INDEX_VERSION);
+			PackIndexWriter w = BasePackIndexWriter.createVersion(os,
+					INDEX_VERSION);
 			w.write(list, packHash);
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexV1.java
index a3d74be..9957f54 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexV1.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackObjectSizeIndexV1.java
@@ -12,7 +12,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
-import java.util.Arrays;
+import java.text.MessageFormat;
 
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.util.NB;
@@ -35,7 +35,7 @@ class PackObjectSizeIndexV1 implements PackObjectSizeIndex {
 
 	private final UInt24Array positions24;
 
-	private final int[] positions32;
+	private final IntArray positions32;
 
 	/**
 	 * Parallel array to concat(positions24, positions32) with the size of the
@@ -45,35 +45,37 @@ class PackObjectSizeIndexV1 implements PackObjectSizeIndex {
 	 * doesn't fit in an int and |value|-1 is the position for the size in the
 	 * size64 array e.g. a value of -1 is sizes64[0], -2 = sizes64[1], ...
 	 */
-	private final int[] sizes32;
+	private final IntArray sizes32;
 
-	private final long[] sizes64;
+	private final LongArray sizes64;
 
 	static PackObjectSizeIndex parse(InputStream in) throws IOException {
 		/** Header and version already out of the input */
-		IndexInputStreamReader stream = new IndexInputStreamReader(in);
-		int threshold = stream.readInt(); // minSize
-		int objCount = stream.readInt();
+		byte[] buffer = new byte[8];
+		in.readNBytes(buffer, 0, 8);
+		int threshold = NB.decodeInt32(buffer, 0); // minSize
+		int objCount = NB.decodeInt32(buffer, 4);
 		if (objCount == 0) {
 			return new EmptyPackObjectSizeIndex(threshold);
 		}
-		return new PackObjectSizeIndexV1(stream, threshold, objCount);
+		return new PackObjectSizeIndexV1(in, threshold, objCount);
 	}
 
-	private PackObjectSizeIndexV1(IndexInputStreamReader stream, int threshold,
+	private PackObjectSizeIndexV1(InputStream stream, int threshold,
 			int objCount) throws IOException {
 		this.threshold = threshold;
 		UInt24Array pos24 = null;
-		int[] pos32 = null;
+		IntArray pos32 = null;
 
+		StreamHelper helper = new StreamHelper();
 		byte positionEncoding;
-		while ((positionEncoding = stream.readByte()) != 0) {
+		while ((positionEncoding = helper.readByte(stream)) != 0) {
 			if (Byte.compareUnsigned(positionEncoding, BITS_24) == 0) {
-				int sz = stream.readInt();
+				int sz = helper.readInt(stream);
 				pos24 = new UInt24Array(stream.readNBytes(sz * 3));
 			} else if (Byte.compareUnsigned(positionEncoding, BITS_32) == 0) {
-				int sz = stream.readInt();
-				pos32 = stream.readIntArray(sz);
+				int sz = helper.readInt(stream);
+				pos32 = IntArray.from(stream, sz);
 			} else {
 				throw new UnsupportedEncodingException(
 						String.format(JGitText.get().unknownPositionEncoding,
@@ -81,16 +83,16 @@ private PackObjectSizeIndexV1(IndexInputStreamReader stream, int threshold,
 			}
 		}
 		positions24 = pos24 != null ? pos24 : UInt24Array.EMPTY;
-		positions32 = pos32 != null ? pos32 : new int[0];
+		positions32 = pos32 != null ? pos32 : IntArray.EMPTY;
 
-		sizes32 = stream.readIntArray(objCount);
-		int c64sizes = stream.readInt();
+		sizes32 = IntArray.from(stream, objCount);
+		int c64sizes = helper.readInt(stream);
 		if (c64sizes == 0) {
-			sizes64 = new long[0];
+			sizes64 = LongArray.EMPTY;
 			return;
 		}
-		sizes64 = stream.readLongArray(c64sizes);
-		int c128sizes = stream.readInt();
+		sizes64 = LongArray.from(stream, c64sizes);
+		int c128sizes = helper.readInt(stream);
 		if (c128sizes != 0) {
 			// this MUST be 0 (we don't support 128 bits sizes yet)
 			throw new IOException(JGitText.get().unsupportedSizesObjSizeIndex);
@@ -102,8 +104,8 @@ public long getSize(int idxOffset) {
 		int pos = -1;
 		if (!positions24.isEmpty() && idxOffset <= positions24.getLastValue()) {
 			pos = positions24.binarySearch(idxOffset);
-		} else if (positions32.length > 0 && idxOffset >= positions32[0]) {
-			int pos32 = Arrays.binarySearch(positions32, idxOffset);
+		} else if (!positions32.empty() && idxOffset >= positions32.get(0)) {
+			int pos32 = positions32.binarySearch(idxOffset);
 			if (pos32 >= 0) {
 				pos = pos32 + positions24.size();
 			}
@@ -112,17 +114,17 @@ public long getSize(int idxOffset) {
 			return -1;
 		}
 
-		int objSize = sizes32[pos];
+		int objSize = sizes32.get(pos);
 		if (objSize < 0) {
 			int secondPos = Math.abs(objSize) - 1;
-			return sizes64[secondPos];
+			return sizes64.get(secondPos);
 		}
 		return objSize;
 	}
 
 	@Override
 	public long getObjectCount() {
-		return (long) positions24.size() + positions32.length;
+		return (long) positions24.size() + positions32.size();
 	}
 
 	@Override
@@ -131,19 +133,114 @@ public int getThreshold() {
 	}
 
 	/**
-	 * Wrapper to read parsed content from the byte stream
+	 * A byte[] that should be interpreted as an int[]
 	 */
-	private static class IndexInputStreamReader {
+	private static class IntArray {
+		private static final IntArray EMPTY = new IntArray(new byte[0]);
 
-		private final byte[] buffer = new byte[8];
+		private static final int INT_SIZE = 4;
 
-		private final InputStream in;
+		private final byte[] data;
 
-		IndexInputStreamReader(InputStream in) {
-			this.in = in;
+		private final int size;
+
+		static IntArray from(InputStream in, int ints) throws IOException {
+			int expectedBytes = ints * INT_SIZE;
+			byte[] data = in.readNBytes(expectedBytes);
+			if (data.length < expectedBytes) {
+				throw new IOException(MessageFormat
+						.format(JGitText.get().unableToReadFullArray,
+								Integer.valueOf(ints)));
+			}
+			return new IntArray(data);
 		}
 
-		int readInt() throws IOException {
+		private IntArray(byte[] data) {
+			this.data = data;
+			size = data.length / INT_SIZE;
+		}
+
+		/**
+		 * Returns position of element in array, -1 if not there
+		 *
+		 * @param needle
+		 *            element to look for
+		 * @return position of the element in the array or -1 if not found
+		 */
+		int binarySearch(int needle) {
+			if (size == 0) {
+				return -1;
+			}
+			int high = size;
+			int low = 0;
+			do {
+				int mid = (low + high) >>> 1;
+				int cmp = Integer.compare(needle, get(mid));
+				if (cmp < 0)
+					high = mid;
+				else if (cmp == 0) {
+					return mid;
+				} else
+					low = mid + 1;
+			} while (low < high);
+			return -1;
+		}
+
+		int get(int position) {
+			if (position < 0 || position >= size) {
+				throw new IndexOutOfBoundsException(position);
+			}
+			return NB.decodeInt32(data, position * INT_SIZE);
+		}
+
+		boolean empty() {
+			return size == 0;
+		}
+
+		int size() {
+			return size;
+		}
+	}
+
+	/**
+	 * A byte[] that should be interpreted as an long[]
+	 */
+	private static class LongArray {
+		private static final LongArray EMPTY = new LongArray(new byte[0]);
+
+		private static final int LONG_SIZE = 8; // bytes
+
+		private final byte[] data;
+
+		private final int size;
+
+		static LongArray from(InputStream in, int longs) throws IOException {
+			byte[] data = in.readNBytes(longs * LONG_SIZE);
+			if (data.length < longs * LONG_SIZE) {
+				throw new IOException(MessageFormat
+						.format(JGitText.get().unableToReadFullArray,
+								Integer.valueOf(longs)));
+			}
+			return new LongArray(data);
+		}
+
+		private LongArray(byte[] data) {
+			this.data = data;
+			size = data.length / LONG_SIZE;
+		}
+
+		long get(int position) {
+			if (position < 0 || position >= size) {
+				throw new IndexOutOfBoundsException(position);
+			}
+			return NB.decodeInt64(data, position * LONG_SIZE);
+		}
+	}
+
+	private static class StreamHelper {
+		private final byte[] buffer = new byte[8];
+
+		int readInt(InputStream in) throws IOException {
 			int n = in.readNBytes(buffer, 0, 4);
 			if (n < 4) {
 				throw new IOException(JGitText.get().unableToReadFullInt);
@@ -151,49 +248,13 @@ int readInt() throws IOException {
 			return NB.decodeInt32(buffer, 0);
 		}
 
-		int[] readIntArray(int intsCount) throws IOException {
-			if (intsCount == 0) {
-				return new int[0];
-			}
-
-			int[] dest = new int[intsCount];
-			for (int i = 0; i < intsCount; i++) {
-				dest[i] = readInt();
-			}
-			return dest;
-		}
-
-		long readLong() throws IOException {
-			int n = in.readNBytes(buffer, 0, 8);
-			if (n < 8) {
-				throw new IOException(JGitText.get().unableToReadFullInt);
-			}
-			return NB.decodeInt64(buffer, 0);
-		}
-
-		long[] readLongArray(int longsCount) throws IOException {
-			if (longsCount == 0) {
-				return new long[0];
-			}
-
-			long[] dest = new long[longsCount];
-			for (int i = 0; i < longsCount; i++) {
-				dest[i] = readLong();
-			}
-			return dest;
-		}
-
-		byte readByte() throws IOException {
+		byte readByte(InputStream in) throws IOException {
 			int n = in.readNBytes(buffer, 0, 1);
 			if (n != 1) {
 				throw new IOException(JGitText.get().cannotReadByte);
 			}
 			return buffer[0];
 		}
-
-		byte[] readNBytes(int sz) throws IOException {
-			return in.readNBytes(sz);
-		}
 	}
 
 	private static class EmptyPackObjectSizeIndex
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 ef9753c..720a3bc 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
@@ -75,20 +75,20 @@ long findNextOffset(long offset, long maxOffset)
 			throws CorruptObjectException;
 
 	/**
-	 * Find the position in the primary index of the object at the given pack
+	 * Find the position in the reverse index of the object at the given pack
 	 * offset.
 	 *
 	 * @param offset
 	 *            the pack offset of the object
-	 * @return the position in the primary index of the object
+	 * @return the position in the reverse index of the object
 	 */
 	int findPosition(long offset);
 
 	/**
-	 * Find the object that is in the given position in the primary index.
+	 * Find the object that is in the given position in the reverse index.
 	 *
 	 * @param nthPosition
-	 *            the position of the object in the primary index
+	 *            the position of the object in the reverse index
 	 * @return the object in that position
 	 */
 	ObjectId findObjectByPosition(int nthPosition);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
index 8e57bf9..05f1ef5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
@@ -16,6 +16,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.eclipse.jgit.lib.Constants.LOGS;
+import static org.eclipse.jgit.lib.Constants.L_LOGS;
 import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
 import static org.eclipse.jgit.lib.Constants.PACKED_REFS;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
@@ -56,23 +57,25 @@
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.PackRefsCommand;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.ObjectWritingException;
 import org.eclipse.jgit.events.RefsChangedEvent;
 import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.CoreConfig.TrustLooseRefStat;
-import org.eclipse.jgit.lib.CoreConfig.TrustPackedRefsStat;
+import org.eclipse.jgit.lib.CoreConfig;
+import org.eclipse.jgit.lib.CoreConfig.TrustStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefComparator;
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefWriter;
+import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.SymbolicRef;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -124,6 +127,8 @@ public class RefDirectory extends RefDatabase {
 
 	private final File gitDir;
 
+	private final File gitCommonDir;
+
 	final File refsDir;
 
 	final File packedRefsFile;
@@ -179,24 +184,19 @@ public class RefDirectory extends RefDatabase {
 
 	private List<Integer> retrySleepMs = RETRY_SLEEP_MS;
 
-	private final boolean trustFolderStat;
-
-	private final TrustPackedRefsStat trustPackedRefsStat;
-
-	private final TrustLooseRefStat trustLooseRefStat;
+	private final CoreConfig coreConfig;
 
 	RefDirectory(RefDirectory refDb) {
 		parent = refDb.parent;
 		gitDir = refDb.gitDir;
+		gitCommonDir = refDb.gitCommonDir;
 		refsDir = refDb.refsDir;
 		logsDir = refDb.logsDir;
 		logsRefsDir = refDb.logsRefsDir;
 		packedRefsFile = refDb.packedRefsFile;
 		looseRefs.set(refDb.looseRefs.get());
 		packedRefs.set(refDb.packedRefs.get());
-		trustFolderStat = refDb.trustFolderStat;
-		trustPackedRefsStat = refDb.trustPackedRefsStat;
-		trustLooseRefStat = refDb.trustLooseRefStat;
+		coreConfig = refDb.coreConfig;
 		inProcessPackedRefsLock = refDb.inProcessPackedRefsLock;
 	}
 
@@ -204,24 +204,15 @@ public class RefDirectory extends RefDatabase {
 		final FS fs = db.getFS();
 		parent = db;
 		gitDir = db.getDirectory();
-		refsDir = fs.resolve(gitDir, R_REFS);
-		logsDir = fs.resolve(gitDir, LOGS);
-		logsRefsDir = fs.resolve(gitDir, LOGS + '/' + R_REFS);
-		packedRefsFile = fs.resolve(gitDir, PACKED_REFS);
+		gitCommonDir = db.getCommonDirectory();
+		refsDir = fs.resolve(gitCommonDir, R_REFS);
+		logsDir = fs.resolve(gitCommonDir, LOGS);
+		logsRefsDir = fs.resolve(gitCommonDir, L_LOGS + R_REFS);
+		packedRefsFile = fs.resolve(gitCommonDir, PACKED_REFS);
 
 		looseRefs.set(RefList.<LooseRef> emptyList());
 		packedRefs.set(NO_PACKED_REFS);
-		trustFolderStat = db.getConfig()
-				.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
-						ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, true);
-		trustPackedRefsStat = db.getConfig()
-				.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_TRUST_PACKED_REFS_STAT,
-						TrustPackedRefsStat.UNSET);
-		trustLooseRefStat = db.getConfig()
-				.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
-						ConfigConstants.CONFIG_KEY_TRUST_LOOSE_REF_STAT,
-						TrustLooseRefStat.ALWAYS);
+		coreConfig = db.getConfig().get(CoreConfig.KEY);
 		inProcessPackedRefsLock = new ReentrantLock(true);
 	}
 
@@ -282,6 +273,33 @@ public void refresh() {
 		clearReferences();
 	}
 
+	/**
+	 * {@inheritDoc}
+	 *
+	 * For a RefDirectory database, by default this packs non-symbolic, loose
+	 * tag refs into packed-refs. If {@code all} flag is set, this packs all the
+	 * non-symbolic, loose refs.
+	 */
+	@Override
+	public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs)
+			throws IOException {
+		String prefix = packRefs.isAll() ? R_REFS : R_TAGS;
+		Collection<Ref> refs = getRefsByPrefix(prefix);
+		List<String> refsToBePacked = new ArrayList<>(refs.size());
+		pm.beginTask(JGitText.get().packRefs, refs.size());
+		try {
+			for (Ref ref : refs) {
+				if (!ref.isSymbolic() && ref.getStorage().isLoose()) {
+					refsToBePacked.add(ref.getName());
+				}
+				pm.update(1);
+			}
+			pack(refsToBePacked);
+		} finally {
+			pm.endTask();
+		}
+	}
+
 	@Override
 	public boolean isNameConflicting(String name) throws IOException {
 		// Cannot be nested within an existing reference.
@@ -422,6 +440,11 @@ public List<Ref> getAdditionalRefs() throws IOException {
 		return ret;
 	}
 
+	@Override
+	public ReflogReader getReflogReader(Ref ref) throws IOException {
+		return new ReflogReaderImpl(getRepository(), ref.getName());
+	}
+
 	@SuppressWarnings("unchecked")
 	private RefList<Ref> upcast(RefList<? extends Ref> loose) {
 		return (RefList<Ref>) loose;
@@ -939,7 +962,7 @@ else if (0 <= (idx = packed.find(dst.getName())))
 	PackedRefList getPackedRefs() throws IOException {
 		final PackedRefList curList = packedRefs.get();
 
-		switch (trustPackedRefsStat) {
+		switch (coreConfig.getTrustPackedRefsStat()) {
 		case NEVER:
 			break;
 		case AFTER_OPEN:
@@ -955,12 +978,8 @@ PackedRefList getPackedRefs() throws IOException {
 				return curList;
 			}
 			break;
-		case UNSET:
-			if (trustFolderStat
-					&& !curList.snapshot.isModified(packedRefsFile)) {
-				return curList;
-			}
-			break;
+		case INHERIT:
+			// only used in CoreConfig internally
 		}
 
 		return refreshPackedRefs(curList);
@@ -1146,7 +1165,7 @@ private Ref readRef(String name, RefList<Ref> packed) throws IOException {
 	LooseRef scanRef(LooseRef ref, String name) throws IOException {
 		final File path = fileFor(name);
 
-		if (trustLooseRefStat.equals(TrustLooseRefStat.AFTER_OPEN)) {
+		if (coreConfig.getTrustLooseRefStat() == TrustStat.AFTER_OPEN) {
 			refreshPathToLooseRef(Paths.get(name));
 		}
 
@@ -1329,7 +1348,12 @@ File fileFor(String name) {
 			name = name.substring(R_REFS.length());
 			return new File(refsDir, name);
 		}
-		return new File(gitDir, name);
+		// HEAD needs to get resolved from git dir as resolving it from common dir
+		// would always lead back to current default branch
+		if (name.equals(HEAD)) {
+			return new File(gitDir, name);
+		}
+		return new File(gitCommonDir, name);
 	}
 
 	static int levelsIn(String name) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogReaderImpl.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogReaderImpl.java
index 21b5a54..f1888eb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogReaderImpl.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogReaderImpl.java
@@ -10,6 +10,8 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -37,7 +39,9 @@ class ReflogReaderImpl implements ReflogReader {
 	 *            {@code Ref} name
 	 */
 	ReflogReaderImpl(Repository db, String refname) {
-		logName = new File(db.getDirectory(), Constants.LOGS + '/' + refname);
+		File logBaseDir = refname.equals(HEAD) ? db.getDirectory()
+				: db.getCommonDirectory();
+		logName = new File(logBaseDir, Constants.L_LOGS + refname);
 	}
 
 	/* (non-Javadoc)
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 30f8240..15c125c 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
@@ -15,8 +15,10 @@
 import java.lang.ref.ReferenceQueue;
 import java.lang.ref.SoftReference;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -397,7 +399,11 @@ static final ByteWindow get(Pack pack, long offset)
 	}
 
 	static final void purge(Pack pack) {
-		cache.removeAll(pack);
+		purge(Collections.singleton(pack));
+	}
+
+	static final void purge(Set<Pack> packs) {
+		cache.queueRemoveAll(packs);
 	}
 
 	/** cleanup released and/or garbage collected windows. */
@@ -441,6 +447,29 @@ static final void purge(Pack pack) {
 
 	private final boolean useStrongIndexRefs;
 
+	/** Removers are purely CPU/mem bound (no I/O), so likely should not go above # CPUs */
+	private final int idealNumRemovers;
+
+	/** Number of blocks to split the Pack removal into.
+	 *
+	 * Consolidation is better with more blocks since it increases
+	 * the wait before moving to the next set by allowing more work
+	 * to accumulate in the next set. On the flip side, the more
+	 * blocks, the more synchronization overhead increasing each
+	 * removers latency.
+	 */
+	private final int numRemovalBlocks;
+
+	private final int removalBlockSize;
+
+	private Set<Pack> packsToRemove = new HashSet<>();
+
+	private Set<Pack> packsBeingRemoved;
+
+	private int numRemovers;
+
+	private int blockBeingRemoved;
+
 	private WindowCache(WindowCacheConfig cfg) {
 		tableSize = tableSize(cfg);
 		final int lockCount = lockCount(cfg);
@@ -479,6 +508,23 @@ else if (eb < 4)
 		statsRecorder = mbean;
 		publishMBean.set(cfg.getExposeStatsViaJmx());
 
+		/* Since each worker will only process up to one full set of blocks, at least 2
+		 * workers are needed anytime there are queued removals to ensure that all the
+		 * blocks will get processed. However, if workers are maxed out at only 2, then
+		 * enough newer workers will never start in order to make it safe for older
+		 * workers to quit early. At least 3 workers are needed to make older worker
+		 * relief transitions possible.
+		 */
+		idealNumRemovers = Math.max(3, Runtime.getRuntime().availableProcessors());
+
+		int bs = 1024;
+		if (tableSize < 2 * bs) {
+			bs = tableSize / 2;
+		}
+		removalBlockSize = bs;
+		numRemovalBlocks = tableSize / removalBlockSize;
+		blockBeingRemoved = numRemovalBlocks - 1;
+
 		if (maxFiles < 1)
 			throw new IllegalArgumentException(JGitText.get().openFilesMustBeAtLeast1);
 		if (maxBytes < windowSize)
@@ -708,22 +754,105 @@ private void removeAll() {
 	}
 
 	/**
-	 * Clear all entries related to a single file.
+	 * Asynchronously clear all entries related to files.
 	 * <p>
-	 * 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.
+	 * Typically this method is invoked during {@link Pack#close()}, or
+	 * {@link Pack#close(Set)}, when we know the packs are never going to be
+	 * useful to us again (for example, they no longer exist on disk after gc).
+	 * A concurrent reader loading an entry from these same packs may cause a
+	 * pack to become stuck in the cache anyway.
 	 *
-	 * @param pack
-	 *            the file to purge all entries of.
+	 * Work on clearing files will be split up into blocks so that removing
+	 * can be shared by more than one thread. This potential work sharing
+	 * can provide 2 optimizations for removals:
+	 * <ol>
+	 * <li> It provides an opportunity for separate removal requests to be
+	 * consolidated into one removal pass.</li>
+	 * <li> It can reduce removing thread latencies by sharing the removal work
+	 * with other removing threads which otherwise might not have any work to do
+	 * due to their removal request being consolidated. This makes the system
+	 * more efficient and can actually reduce latencies as system load increases
+	 * due to pack removals!</li>
+	 * </ol>
+	 * The optimizations above are all achieved without blockng threads to wait
+	 * for other threads to complete (although naturally there are some
+	 * synchronization points), and while ensuring that no threads do more work
+	 * than if they were the only thread available to perform a removal.
+	 *
+	 * @param packs
+	 *            the files to purge all entries of
 	 */
-	private void removeAll(Pack pack) {
-		for (int s = 0; s < tableSize; s++) {
+	private void queueRemoveAll(Set<Pack> packs) {
+		synchronized (this) {
+			packsToRemove.addAll(packs);
+			if (numRemovers >= idealNumRemovers) {
+				return;
+			}
+			numRemovers++;
+		}
+		for (int numRemoved = 0; removeNextBlock(numRemoved); numRemoved++) {
+			// empty
+		}
+		synchronized (this) {
+			if (numRemovers > 0) {
+				numRemovers--;
+			}
+		}
+	}
+
+	/** Determine which block to remove next, if any, and do so.
+	 *
+	 * @param numRemoved
+	 *            the number of already processed block removals by the current thread
+	 * @return whether more processing should be done by the current thread
+	 */
+	private boolean removeNextBlock(int numRemoved) {
+		Set<Pack> toRemove;
+		int block;
+		synchronized (this) {
+			if (packsBeingRemoved == null || blockBeingRemoved >= numRemovalBlocks - 1) {
+				if (packsToRemove.isEmpty()) {
+					return false;
+				}
+
+				blockBeingRemoved = 0;
+				packsBeingRemoved = packsToRemove;
+				packsToRemove = new HashSet<>();
+			} else {
+				blockBeingRemoved++;
+			}
+
+			toRemove = packsBeingRemoved;
+			block = blockBeingRemoved;
+		}
+
+		removeBlock(toRemove, block);
+		numRemoved++;
+
+		/* Ensure threads never work on more than a full set of blocks (the equivalent
+		 * of removing one Pack) */
+		boolean isLast = numRemoved >= numRemovalBlocks;
+		synchronized (this) {
+			if (numRemovers >= idealNumRemovers) {
+				isLast = true;
+			}
+		}
+		return !isLast;
+	}
+
+	/** Remove a block of entries for a Set of files
+	 * @param packs
+	 *            the files to purge all entries of
+	 * @param block
+	 *            the specific block to process removals for
+	 */
+	private void removeBlock(Set<Pack> packs, int block) {
+		int starting = block * removalBlockSize;
+		for (int s = starting; s < starting + removalBlockSize && s < tableSize; s++) {
 			final Entry e1 = table.get(s);
 			boolean hasDead = false;
 			for (Entry e = e1; e != null; e = e.next) {
-				if (e.ref.getPack() == pack) {
+				if (packs.contains(e.ref.getPack())) {
 					e.kill();
 					hasDead = true;
 				} else if (e.dead)
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 01f514b..11c4547 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
@@ -205,7 +205,7 @@ public void writeObjects(PackOutputStream out, List<ObjectToPack> list)
 	 * @param cnt
 	 *            number of bytes to copy. This value may exceed the number of
 	 *            bytes remaining in the window starting at offset
-	 *            <code>pos</code>.
+	 *            <code>position</code>.
 	 * @return number of bytes actually copied; this may be less than
 	 *         <code>cnt</code> if <code>cnt</code> exceeded the number of bytes
 	 *         available.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexConstants.java
new file mode 100644
index 0000000..6122a9a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexConstants.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.midx;
+
+class MultiPackIndexConstants {
+	static final int MIDX_SIGNATURE = 0x4d494458; /* MIDX */
+
+	static final byte MIDX_VERSION = 1;
+
+	/**
+	 * We infer the length of object IDs (OIDs) from this value:
+	 * 
+	 * <pre>
+	 * 1 => SHA-1
+	 * 2 => SHA-256
+	 * </pre>
+	 */
+	static final byte OID_HASH_VERSION = 1;
+
+	static final int MULTIPACK_INDEX_FANOUT_SIZE = 4 * 256;
+
+	/**
+	 * First 4 bytes describe the chunk id. Value 0 is a terminating label.
+	 * Other 8 bytes provide the byte-offset in current file for chunk to start.
+	 */
+	static final int CHUNK_LOOKUP_WIDTH = 12;
+
+	/** "PNAM" chunk */
+	static final int MIDX_CHUNKID_PACKNAMES = 0x504e414d;
+
+	/** "OIDF" chunk */
+	static final int MIDX_CHUNKID_OIDFANOUT = 0x4f494446;
+
+	/** "OIDL" chunk */
+	static final int MIDX_CHUNKID_OIDLOOKUP = 0x4f49444c;
+
+	/** "OOFF" chunk */
+	static final int MIDX_CHUNKID_OBJECTOFFSETS = 0x4f4f4646;
+
+	/** "LOFF" chunk */
+	static final int MIDX_CHUNKID_LARGEOFFSETS = 0x4c4f4646;
+
+	/** "RIDX" chunk */
+	static final int MIDX_CHUNKID_REVINDEX = 0x52494458;
+
+	private MultiPackIndexConstants() {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java
new file mode 100644
index 0000000..795d39e
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexPrettyPrinter.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.midx;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.NB;
+
+/**
+ * Prints a multipack index file in a human-readable format.
+ *
+ * @since 7.2
+ */
+@SuppressWarnings({ "boxing", "nls" })
+public class MultiPackIndexPrettyPrinter {
+
+	/**
+	 * Writes to out, in human-readable format, the multipack index in rawMidx
+	 *
+	 * @param rawMidx the bytes of a multipack index
+	 * @param out a writer
+	 */
+	public static void prettyPrint(byte[] rawMidx, PrintWriter out) {
+		// Header (12 bytes)
+		out.println("[ 0] Magic: " + new String(rawMidx, 0, 4, UTF_8));
+		out.println("[ 4] Version number: " + (int) rawMidx[4]);
+		out.println("[ 5] OID version: " + (int) rawMidx[5]);
+		int chunkCount = rawMidx[6];
+		out.println("[ 6] # of chunks: " + chunkCount);
+		out.println("[ 7] # of bases: " + (int) rawMidx[7]);
+		int numberOfPacks = NB.decodeInt32(rawMidx, 8);
+		out.println("[ 8] # of packs: " + numberOfPacks);
+
+		// Chunk lookup table
+		List<ChunkSegment> chunkSegments = new ArrayList<>();
+		int current = printChunkLookup(out, rawMidx, chunkCount, chunkSegments);
+
+		for (int i = 0; i < chunkSegments.size() - 1; i++) {
+			ChunkSegment segment = chunkSegments.get(i);
+			if (current != segment.startOffset()) {
+				throw new IllegalStateException(String.format(
+						"We are at byte %d, but segment should start at %d",
+						current, segment.startOffset()));
+			}
+			out.printf("Starting chunk: %s @ %d%n", segment.chunkName(),
+					segment.startOffset());
+			switch (segment.chunkName()) {
+				case "OIDF" -> current = printOIDF(out, rawMidx, current);
+				case "OIDL" -> current = printOIDL(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				case "OOFF" -> current = printOOFF(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				case "PNAM" -> current = printPNAM(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				case "RIDX" -> current = printRIDX(out, rawMidx, current,
+						chunkSegments.get(i + 1).startOffset);
+				default -> {
+					out.printf(
+							"Skipping %s (don't know how to print it yet)%n",
+							segment.chunkName());
+					current = (int) chunkSegments.get(i + 1).startOffset();
+				}
+			}
+		}
+		// Checksum is a SHA-1, use ObjectId to parse it
+		out.printf("[ %d] Checksum %s%n", current,
+				ObjectId.fromRaw(rawMidx, current).name());
+		out.printf("Total size: " + (current + 20));
+	}
+
+	private static int printChunkLookup(PrintWriter out, byte[] rawMidx, int chunkCount,
+			List<ChunkSegment> chunkSegments) {
+		out.println("Starting chunk lookup @ 12");
+		int current = 12;
+		for (int i = 0; i < chunkCount; i++) {
+			String chunkName = new String(rawMidx, current, 4, UTF_8);
+			long offset = NB.decodeInt64(rawMidx, current + 4);
+			out.printf("[ %d] |%8s|%8d|%n", current, chunkName, offset);
+			current += CHUNK_LOOKUP_WIDTH;
+			chunkSegments.add(new ChunkSegment(chunkName, offset));
+		}
+		String chunkName = "0000";
+		long offset = NB.decodeInt64(rawMidx, current + 4);
+		out.printf("[ %d] |%8s|%8d|%n", current, chunkName, offset);
+		current += CHUNK_LOOKUP_WIDTH;
+		chunkSegments.add(new ChunkSegment(chunkName, offset));
+		return current;
+	}
+
+	private static int printOIDF(PrintWriter out, byte[] rawMidx, int start) {
+		int current = start;
+		for (short i = 0; i < 256; i++) {
+			out.printf("[ %d] (%02X) %d%n", current, i,
+					NB.decodeInt32(rawMidx, current));
+			current += 4;
+		}
+		return current;
+	}
+
+	private static int printOIDL(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int i = start;
+		while (i < end) {
+			out.printf("[ %d] %s%n", i,
+					ObjectId.fromRaw(rawMidx, i).name());
+			i += 20;
+		}
+		return i;
+	}
+
+	private static int printOOFF(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int i = start;
+		while (i < end) {
+			out.printf("[ %d] %d %d%n", i, NB.decodeInt32(rawMidx, i),
+					NB.decodeInt32(rawMidx, i + 4));
+			i += 8;
+		}
+		return i;
+	}
+
+	private static int printRIDX(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int i = start;
+		while (i < end) {
+			out.printf("[ %d] %d%n", i, NB.decodeInt32(rawMidx, i));
+			i += 4;
+		}
+		return (int) end;
+	}
+
+	private static int printPNAM(PrintWriter out, byte[] rawMidx, int start, long end) {
+		int nameStart = start;
+		for (int i = start; i < end; i++) {
+			if (rawMidx[i] == 0) {
+				out
+						.println(new String(rawMidx, nameStart, i - nameStart));
+				nameStart = i + 1;
+			}
+		}
+		return (int) end;
+	}
+
+	private record ChunkSegment(String chunkName, long startOffset) {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriter.java
new file mode 100644
index 0000000..bddf3ac
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriter.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.midx;
+
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_LARGEOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_REVINDEX;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_SIGNATURE;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_VERSION;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MULTIPACK_INDEX_FANOUT_SIZE;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.OID_HASH_VERSION;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.internal.storage.io.CancellableDigestOutputStream;
+import org.eclipse.jgit.internal.storage.midx.PackIndexMerger.MidxMutableEntry;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.util.NB;
+
+/**
+ * Writes a collection of indexes as a multipack index.
+ * <p>
+ * See <a href=
+ * "https://git-scm.com/docs/pack-format#_multi_pack_index_midx_files_have_the_following_format">multipack
+ * index format spec</a>
+ *
+ * @since 7.2
+ */
+public class MultiPackIndexWriter {
+
+	private static final int LIMIT_31_BITS = (1 << 31) - 1;
+
+	private static final int MIDX_HEADER_SIZE = 12;
+
+	/**
+	 * Writes the inputs in the multipack index format in the outputStream.
+	 *
+	 * @param monitor
+	 *            progress monitor
+	 * @param outputStream
+	 *            stream to write the multipack index file
+	 * @param inputs
+	 *            pairs of name and index for each pack to include in the
+	 *            multipack index.
+	 * @throws IOException
+	 *             Error writing to the stream
+	 */
+	public void write(ProgressMonitor monitor, OutputStream outputStream,
+			Map<String, PackIndex> inputs) throws IOException {
+		PackIndexMerger data = new PackIndexMerger(inputs);
+
+		// List of chunks in the order they need to be written
+		List<ChunkHeader> chunkHeaders = createChunkHeaders(data);
+		long expectedSize = calculateExpectedSize(chunkHeaders);
+		try (CancellableDigestOutputStream out = new CancellableDigestOutputStream(
+				monitor, outputStream)) {
+			writeHeader(out, chunkHeaders.size(), data.getPackCount());
+			writeChunkLookup(out, chunkHeaders);
+
+			WriteContext ctx = new WriteContext(out, data);
+			for (ChunkHeader chunk : chunkHeaders) {
+				chunk.writerFn.write(ctx);
+			}
+			writeCheckSum(out);
+			if (expectedSize != out.length()) {
+				throw new IllegalStateException(String.format(
+						JGitText.get().multiPackIndexUnexpectedSize,
+						Long.valueOf(expectedSize),
+						Long.valueOf(out.length())));
+			}
+		} catch (InterruptedIOException e) {
+			throw new IOException(JGitText.get().multiPackIndexWritingCancelled,
+					e);
+		}
+	}
+
+	private static long calculateExpectedSize(List<ChunkHeader> chunks) {
+		int chunkLookup = (chunks.size() + 1) * CHUNK_LOOKUP_WIDTH;
+		long chunkContent = chunks.stream().mapToLong(c -> c.size).sum();
+		return /* header */ 12 + chunkLookup + chunkContent + /* CRC */ 20;
+	}
+
+	private List<ChunkHeader> createChunkHeaders(PackIndexMerger data) {
+		List<ChunkHeader> chunkHeaders = new ArrayList<>();
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_OIDFANOUT,
+				MULTIPACK_INDEX_FANOUT_SIZE, this::writeFanoutTable));
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_OIDLOOKUP,
+				(long) data.getUniqueObjectCount() * OBJECT_ID_LENGTH,
+				this::writeOidLookUp));
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_OBJECTOFFSETS,
+				8L * data.getUniqueObjectCount(), this::writeObjectOffsets));
+		if (data.needsLargeOffsetsChunk()) {
+			chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_LARGEOFFSETS,
+					8L * data.getOffsetsOver31BitsCount(),
+					this::writeObjectLargeOffsets));
+		}
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_REVINDEX,
+				4L * data.getUniqueObjectCount(), this::writeRidx));
+
+		int packNamesSize = data.getPackNames().stream()
+				.mapToInt(String::length).map(i -> i + 1 /* null at the end */)
+				.sum();
+		chunkHeaders.add(new ChunkHeader(MIDX_CHUNKID_PACKNAMES, packNamesSize,
+				this::writePackfileNames));
+		return chunkHeaders;
+	}
+
+	/**
+	 * Write the first 12 bytes of the multipack index.
+	 * <p>
+	 * These bytes include things like magic number, version, number of
+	 * chunks...
+	 *
+	 * @param out
+	 *            output stream to write
+	 * @param numChunks
+	 *            number of chunks this multipack index is going to have
+	 * @param packCount
+	 *            number of packs covered by this multipack index
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeHeader(CancellableDigestOutputStream out, int numChunks,
+			int packCount) throws IOException {
+		byte[] headerBuffer = new byte[MIDX_HEADER_SIZE];
+		NB.encodeInt32(headerBuffer, 0, MIDX_SIGNATURE);
+		byte[] buff = { MIDX_VERSION, OID_HASH_VERSION, (byte) numChunks,
+				(byte) 0 };
+		System.arraycopy(buff, 0, headerBuffer, 4, 4);
+		NB.encodeInt32(headerBuffer, 8, packCount);
+		out.write(headerBuffer, 0, headerBuffer.length);
+		out.flush();
+	}
+
+	/**
+	 * Write a table of "chunkId, start-offset", with a special value "0,
+	 * end-of-previous_chunk", to mark the end.
+	 *
+	 * @param out
+	 *            output stream to write
+	 * @param chunkHeaders
+	 *            list of chunks in the order they are expected to be written
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeChunkLookup(CancellableDigestOutputStream out,
+			List<ChunkHeader> chunkHeaders) throws IOException {
+
+		// first chunk will start at header + this lookup block
+		long chunkStart = MIDX_HEADER_SIZE
+				+ (long) (chunkHeaders.size() + 1) * CHUNK_LOOKUP_WIDTH;
+		byte[] chunkEntry = new byte[CHUNK_LOOKUP_WIDTH];
+		for (ChunkHeader chunkHeader : chunkHeaders) {
+			NB.encodeInt32(chunkEntry, 0, chunkHeader.chunkId);
+			NB.encodeInt64(chunkEntry, 4, chunkStart);
+			out.write(chunkEntry);
+			chunkStart += chunkHeader.size;
+		}
+		// Terminating label for the block
+		// (chunkid 0, offset where the next block would start)
+		NB.encodeInt32(chunkEntry, 0, 0);
+		NB.encodeInt64(chunkEntry, 4, chunkStart);
+		out.write(chunkEntry);
+	}
+
+	/**
+	 * Write the fanout table for the object ids
+	 * <p>
+	 * Table with 256 entries (one byte), where the ith entry, F[i], stores the
+	 * number of OIDs with first byte at most i. Thus, F[255] stores the total
+	 * number of objects.
+	 *
+	 * @param ctx
+	 *            write context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+
+	private void writeFanoutTable(WriteContext ctx) throws IOException {
+		byte[] tmp = new byte[4];
+		int[] fanout = new int[256];
+		Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator();
+		while (iterator.hasNext()) {
+			MidxMutableEntry e = iterator.next();
+			fanout[e.getObjectId().getFirstByte() & 0xff]++;
+		}
+		for (int i = 1; i < fanout.length; i++) {
+			fanout[i] += fanout[i - 1];
+		}
+		for (int n : fanout) {
+			NB.encodeInt32(tmp, 0, n);
+			ctx.out.write(tmp, 0, 4);
+		}
+	}
+
+	/**
+	 * Write the OID lookup chunk
+	 * <p>
+	 * A list of OIDs in sha1 order.
+	 *
+	 * @param ctx
+	 *            write context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeOidLookUp(WriteContext ctx) throws IOException {
+		byte[] tmp = new byte[OBJECT_ID_LENGTH];
+
+		Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator();
+		while (iterator.hasNext()) {
+			MidxMutableEntry e = iterator.next();
+			e.getObjectId().copyRawTo(tmp, 0);
+			ctx.out.write(tmp, 0, OBJECT_ID_LENGTH);
+		}
+	}
+
+	/**
+	 * Write the object offsets chunk
+	 * <p>
+	 * A list of offsets, parallel to the list of OIDs. If the offset is too
+	 * large (see {@link #fitsIn31bits(long)}), this contains the position in
+	 * the large offsets list (marked with a 1 in the most significant bit).
+	 *
+	 * @param ctx
+	 *            write context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeObjectOffsets(WriteContext ctx) throws IOException {
+		byte[] entry = new byte[8];
+		Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator();
+		while (iterator.hasNext()) {
+			MidxMutableEntry e = iterator.next();
+			NB.encodeInt32(entry, 0, e.getPackId());
+			if (!ctx.data.needsLargeOffsetsChunk()
+					|| fitsIn31bits(e.getOffset())) {
+				NB.encodeInt32(entry, 4, (int) e.getOffset());
+			} else {
+				int offloadedPosition = ctx.largeOffsets.append(e.getOffset());
+				NB.encodeInt32(entry, 4, offloadedPosition | (1 << 31));
+			}
+			ctx.out.write(entry);
+		}
+	}
+
+	/**
+	 * Writes the reverse index chunk
+	 * <p>
+	 * This stores the position of the objects in the main index, ordered first
+	 * by pack and then by offset
+	 *
+	 * @param ctx
+	 *            write context
+	 * @throws IOException
+	 *             erorr writing to the output stream
+	 */
+	private void writeRidx(WriteContext ctx) throws IOException {
+		Map<Integer, List<OffsetPosition>> packOffsets = new HashMap<>(
+				ctx.data.getPackCount());
+		// TODO(ifrade): Brute force solution loading all offsets/packs in
+		// memory. We could also iterate reverse indexes looking up
+		// their position in the midx (and discarding if the pack doesn't
+		// match).
+		Iterator<MidxMutableEntry> iterator = ctx.data.bySha1Iterator();
+		int midxPosition = 0;
+		while (iterator.hasNext()) {
+			MidxMutableEntry e = iterator.next();
+			OffsetPosition op = new OffsetPosition(e.getOffset(), midxPosition);
+			midxPosition++;
+			packOffsets.computeIfAbsent(Integer.valueOf(e.getPackId()),
+					k -> new ArrayList<>()).add(op);
+		}
+
+		for (int i = 0; i < ctx.data.getPackCount(); i++) {
+			List<OffsetPosition> offsetsForPack = packOffsets
+					.get(Integer.valueOf(i));
+			if (offsetsForPack.isEmpty()) {
+				continue;
+			}
+			offsetsForPack.sort(Comparator.comparing(OffsetPosition::offset));
+			byte[] ridxForPack = new byte[4 * offsetsForPack.size()];
+			for (int j = 0; j < offsetsForPack.size(); j++) {
+				NB.encodeInt32(ridxForPack, j * 4,
+						offsetsForPack.get(j).position);
+			}
+			ctx.out.write(ridxForPack);
+		}
+	}
+
+	/**
+	 * Write the large offset chunk
+	 * <p>
+	 * A list of large offsets (long). The regular offset chunk will point to a
+	 * position here.
+	 *
+	 * @param ctx
+	 *            writer context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeObjectLargeOffsets(WriteContext ctx) throws IOException {
+		ctx.out.write(ctx.largeOffsets.offsets, 0,
+				ctx.largeOffsets.bytePosition);
+	}
+
+	/**
+	 * Write the list of packfiles chunk
+	 * <p>
+	 * List of packfiles (in lexicographical order) with an \0 at the end
+	 *
+	 * @param ctx
+	 *            writer context
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writePackfileNames(WriteContext ctx) throws IOException {
+		for (String packName : ctx.data.getPackNames()) {
+			// Spec doesn't talk about encoding.
+			ctx.out.write(packName.getBytes(StandardCharsets.UTF_8));
+			ctx.out.write(0);
+		}
+	}
+
+	/**
+	 * Write final checksum of the data written to the stream
+	 *
+	 * @param out
+	 *            output stream used to write
+	 * @throws IOException
+	 *             error writing to the output stream
+	 */
+	private void writeCheckSum(CancellableDigestOutputStream out)
+			throws IOException {
+		out.write(out.getDigest());
+		out.flush();
+	}
+
+
+	private record OffsetPosition(long offset, int position) {
+	}
+
+	/**
+	 * If there is at least one offset value larger than 2^32-1, then the large
+	 * offset chunk must exist, and offsets larger than 2^31-1 must be stored in
+	 * it instead
+	 *
+	 * @param offset object offset
+	 *
+	 * @return true if the offset fits in 31 bits
+	 */
+	private static boolean fitsIn31bits(long offset) {
+		return offset <= LIMIT_31_BITS;
+	}
+
+	private static class LargeOffsets {
+		private final byte[] offsets;
+
+		private int bytePosition;
+
+		LargeOffsets(int largeOffsetsCount) {
+			offsets = new byte[largeOffsetsCount * 8];
+			bytePosition = 0;
+		}
+
+		/**
+		 * Add an offset to the large offset chunk
+		 *
+		 * @param largeOffset
+		 *            a large offset
+		 * @return the position of the just inserted offset (as in number of
+		 *         offsets, NOT in bytes)
+		 */
+		int append(long largeOffset) {
+			int at = bytePosition;
+			NB.encodeInt64(offsets, at, largeOffset);
+			bytePosition += 8;
+			return at / 8;
+		}
+	}
+
+	private record ChunkHeader(int chunkId, long size, ChunkWriter writerFn) {
+	}
+
+	@FunctionalInterface
+	private interface ChunkWriter {
+		void write(WriteContext ctx) throws IOException;
+	}
+
+	private static class WriteContext {
+		final CancellableDigestOutputStream out;
+
+		final PackIndexMerger data;
+
+		final LargeOffsets largeOffsets;
+
+		WriteContext(CancellableDigestOutputStream out, PackIndexMerger data) {
+			this.out = out;
+			this.data = data;
+			this.largeOffsets = new LargeOffsets(
+					data.getOffsetsOver31BitsCount());
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/PackIndexMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/PackIndexMerger.java
new file mode 100644
index 0000000..89814af
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/midx/PackIndexMerger.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2025, Google Inc.
+ *
+ * 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.midx;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.MutableObjectId;
+
+/**
+ * Collect the stats and offers an iterator over the union of n-pack indexes.
+ * <p>
+ * The multipack index is a list of (sha1, packid, offset) ordered by sha1. We
+ * can build it from the individual pack indexes (sha1, offset) ordered by sha1,
+ * with a simple merge ignoring duplicates.
+ * <p>
+ * This class encapsulates the merging logic and precalculates the stats that
+ * the index needs (like total count of objects). To limit memory consumption,
+ * it does the merge as it goes during the iteration and iterators use mutable
+ * entries. The stats of the combined index are calculated in an iteration at
+ * construction time.
+ */
+class PackIndexMerger {
+
+	private static final int LIMIT_31_BITS = (1 << 31) - 1;
+
+	private static final long LIMIT_32_BITS = (1L << 32) - 1;
+
+	/**
+	 * Object returned by the iterator.
+	 * <p>
+	 * The iterator returns (on each next()) the same instance with different
+	 * values, to avoid allocating many short-lived objects. Callers should not
+	 * keep a reference to that returned value.
+	 */
+	static class MidxMutableEntry {
+		// The object id
+		private final MutableObjectId oid = new MutableObjectId();
+
+		// Position of the pack in the ordered list of pack in this merger
+		private int packId;
+
+		// Offset in its pack
+		private long offset;
+
+		public AnyObjectId getObjectId() {
+			return oid;
+		}
+
+		public int getPackId() {
+			return packId;
+		}
+
+		public long getOffset() {
+			return offset;
+		}
+
+		/**
+		 * Copy values from another mutable entry
+		 *
+		 * @param packId
+		 *            packId
+		 * @param other
+		 *            another mutable entry
+		 */
+		private void fill(int packId, PackIndex.MutableEntry other) {
+			other.copyOidTo(oid);
+			this.packId = packId;
+			this.offset = other.getOffset();
+		}
+	}
+
+	private final List<String> packNames;
+
+	private final List<PackIndex> indexes;
+
+	private final boolean needsLargeOffsetsChunk;
+
+	private final int offsetsOver31BitsCount;
+
+	private final int uniqueObjectCount;
+
+	PackIndexMerger(Map<String, PackIndex> packs) {
+		this.packNames = packs.keySet().stream().sorted()
+				.collect(Collectors.toUnmodifiableList());
+
+		this.indexes = packNames.stream().map(packs::get)
+				.collect(Collectors.toUnmodifiableList());
+
+		// Iterate for duplicates
+		int objectCount = 0;
+		boolean hasLargeOffsets = false;
+		int over31bits = 0;
+		MutableObjectId lastSeen = new MutableObjectId();
+		MultiIndexIterator it = new MultiIndexIterator(indexes);
+		while (it.hasNext()) {
+			MidxMutableEntry entry = it.next();
+			if (lastSeen.equals(entry.oid)) {
+				continue;
+			}
+			// If there is at least one offset value larger than 2^32-1, then
+			// the large offset chunk must exist, and offsets larger than
+			// 2^31-1 must be stored in it instead
+			if (entry.offset > LIMIT_32_BITS) {
+				hasLargeOffsets = true;
+			}
+			if (entry.offset > LIMIT_31_BITS) {
+				over31bits++;
+			}
+
+			lastSeen.fromObjectId(entry.oid);
+			objectCount++;
+		}
+		uniqueObjectCount = objectCount;
+		offsetsOver31BitsCount = over31bits;
+		needsLargeOffsetsChunk = hasLargeOffsets;
+	}
+
+	/**
+	 * Object count of the merged index (i.e. without duplicates)
+	 *
+	 * @return object count of the merged index
+	 */
+	int getUniqueObjectCount() {
+		return uniqueObjectCount;
+	}
+
+	/**
+	 * If any object in any of the indexes has an offset over 2^32-1
+	 *
+	 * @return true if there is any object with offset > 2^32 -1
+	 */
+	boolean needsLargeOffsetsChunk() {
+		return needsLargeOffsetsChunk;
+	}
+
+	/**
+	 * How many object have offsets over 2^31-1
+	 * <p>
+	 * Per multipack index spec, IF there is large offset chunk, all this
+	 * offsets should be there.
+	 *
+	 * @return number of objects with offsets over 2^31-1
+	 */
+	int getOffsetsOver31BitsCount() {
+		return offsetsOver31BitsCount;
+	}
+
+	/**
+	 * List of pack names in alphabetical order.
+	 * <p>
+	 * Order matters: In case of duplicates, the multipack index prefers the
+	 * first package with it. This is in the same order we are using to
+	 * prioritize duplicates.
+	 *
+	 * @return List of pack names, in the order used by the merge.
+	 */
+	List<String> getPackNames() {
+		return packNames;
+	}
+
+	/**
+	 * How many packs are being merged
+	 *
+	 * @return count of packs merged
+	 */
+	int getPackCount() {
+		return packNames.size();
+	}
+
+	/**
+	 * Iterator over the merged indexes in sha1 order without duplicates
+	 * <p>
+	 * The returned entry in the iterator is mutable, callers should NOT keep a
+	 * reference to it.
+	 *
+	 * @return an iterator in sha1 order without duplicates.
+	 */
+	Iterator<MidxMutableEntry> bySha1Iterator() {
+		return new DedupMultiIndexIterator(new MultiIndexIterator(indexes),
+				getUniqueObjectCount());
+	}
+
+	/**
+	 * For testing. Iterate all entries, not skipping duplicates (stable order)
+	 *
+	 * @return an iterator of all objects in sha1 order, including duplicates.
+	 */
+	Iterator<MidxMutableEntry> rawIterator() {
+		return new MultiIndexIterator(indexes);
+	}
+
+	/**
+	 * Iterator over n-indexes in ObjectId order.
+	 * <p>
+	 * It returns duplicates if the same object id is in different indexes. Wrap
+	 * it with {@link DedupMultiIndexIterator (Iterator, int)} to avoid
+	 * duplicates.
+	 */
+	private static final class MultiIndexIterator
+			implements Iterator<MidxMutableEntry> {
+
+		private final List<PackIndexPeekIterator> indexIterators;
+
+		private final MidxMutableEntry mutableEntry = new MidxMutableEntry();
+
+		MultiIndexIterator(List<PackIndex> indexes) {
+			this.indexIterators = new ArrayList<>(indexes.size());
+			for (int i = 0; i < indexes.size(); i++) {
+				PackIndexPeekIterator it = new PackIndexPeekIterator(i,
+						indexes.get(i));
+				// Position in the first element
+				if (it.next() != null) {
+					indexIterators.add(it);
+				}
+			}
+		}
+
+		@Override
+		public boolean hasNext() {
+			return !indexIterators.isEmpty();
+		}
+
+		@Override
+		public MidxMutableEntry next() {
+			PackIndexPeekIterator winner = null;
+			for (int index = 0; index < indexIterators.size(); index++) {
+				PackIndexPeekIterator current = indexIterators.get(index);
+				if (winner == null
+						|| current.peek().compareBySha1To(winner.peek()) < 0) {
+					winner = current;
+				}
+			}
+
+			if (winner == null) {
+				throw new NoSuchElementException();
+			}
+
+			mutableEntry.fill(winner.getPackId(), winner.peek());
+			if (winner.next() == null) {
+				indexIterators.remove(winner);
+			};
+			return mutableEntry;
+		}
+	}
+
+	private static class DedupMultiIndexIterator
+			implements Iterator<MidxMutableEntry> {
+		private final MultiIndexIterator src;
+
+		private int remaining;
+
+		private final MutableObjectId lastOid = new MutableObjectId();
+
+		DedupMultiIndexIterator(MultiIndexIterator src, int totalCount) {
+			this.src = src;
+			this.remaining = totalCount;
+		}
+
+		@Override
+		public boolean hasNext() {
+			return remaining > 0;
+		}
+
+		@Override
+		public MidxMutableEntry next() {
+			MidxMutableEntry next = src.next();
+			while (next != null && lastOid.equals(next.oid)) {
+				next = src.next();
+			}
+
+			if (next == null) {
+				throw new NoSuchElementException();
+			}
+
+			lastOid.fromObjectId(next.oid);
+			remaining--;
+			return next;
+		}
+	}
+
+	/**
+	 * Convenience around the PackIndex iterator to read the current value
+	 * multiple times without consuming it.
+	 * <p>
+	 * This is used to merge indexes in the multipack index, where we need to
+	 * compare the current value between indexes multiple times to find the
+	 * next.
+	 * <p>
+	 * We could also implement this keeping the position (int) and
+	 * MutableEntry#getObjectId, but that would create an ObjectId per entry.
+	 * This implementation reuses the MutableEntry and avoid instantiations.
+	 */
+	// Visible for testing
+	static class PackIndexPeekIterator {
+		private final Iterator<PackIndex.MutableEntry> it;
+
+		private final int packId;
+
+		PackIndex.MutableEntry current;
+
+		PackIndexPeekIterator(int packId, PackIndex index) {
+			it = index.iterator();
+			this.packId = packId;
+		}
+
+		PackIndex.MutableEntry next() {
+			if (it.hasNext()) {
+				current = it.next();
+			} else {
+				current = null;
+			}
+			return current;
+		}
+
+		PackIndex.MutableEntry peek() {
+			return current;
+		}
+
+		int getPackId() {
+			return packId;
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackIndexWriter.java
new file mode 100644
index 0000000..f69e68d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackIndexWriter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024, Google LLC.
+ *
+ * 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.pack;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.transport.PackedObjectInfo;
+
+/**
+ * Represents a function that accepts a collection of objects to write into a
+ * primary pack index storage format.
+ */
+public interface PackIndexWriter {
+	/**
+	 * Write all object entries to the index stream.
+	 *
+	 * @param toStore
+	 *            sorted list of objects to store in the index. The caller must
+	 *            have previously sorted the list using
+	 *            {@link org.eclipse.jgit.transport.PackedObjectInfo}'s native
+	 *            {@link java.lang.Comparable} implementation.
+	 * @param packDataChecksum
+	 *            checksum signature of the entire pack data content. This is
+	 *            traditionally the last 20 bytes of the pack file's own stream.
+	 * @throws java.io.IOException
+	 *             an error occurred while writing to the output stream, or the
+	 *             underlying format cannot store the object data supplied.
+	 */
+	void write(List<? extends PackedObjectInfo> toStore,
+			byte[] packDataChecksum) throws IOException;
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
index 4350f97..f025d4e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
@@ -58,10 +58,10 @@
 import org.eclipse.jgit.errors.SearchForReuseTimeout;
 import org.eclipse.jgit.errors.StoredObjectRepresentationNotAvailableException;
 import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.internal.storage.file.PackIndexWriter;
+import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter;
+import org.eclipse.jgit.internal.storage.file.PackBitmapIndexBuilder;
 import org.eclipse.jgit.internal.storage.file.PackObjectSizeIndexWriter;
 import org.eclipse.jgit.internal.storage.file.PackReverseIndexWriter;
-import org.eclipse.jgit.internal.storage.file.PackBitmapIndexBuilder;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.AsyncObjectSizeQueue;
 import org.eclipse.jgit.lib.BatchingProgressMonitor;
@@ -118,7 +118,7 @@
  * {@link #preparePack(ProgressMonitor, Set, Set)}, and streaming with
  * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}. If the
  * pack is being stored as a file the matching index can be written out after
- * writing the pack by {@link #writeIndex(OutputStream)}. An optional bitmap
+ * writing the pack by {@link #writeIndex(PackIndexWriter)}. An optional bitmap
  * index can be made by calling {@link #prepareBitmapIndex(ProgressMonitor)}
  * followed by {@link #writeBitmapIndex(PackBitmapIndexWriter)}.
  * </p>
@@ -303,19 +303,6 @@ public PackWriter(Repository repo) {
 	}
 
 	/**
-	 * Create a writer to load objects from the specified reader.
-	 * <p>
-	 * Objects for packing are specified in {@link #preparePack(Iterator)} or
-	 * {@link #preparePack(ProgressMonitor, Set, Set)}.
-	 *
-	 * @param reader
-	 *            reader to read from the repository with.
-	 */
-	public PackWriter(ObjectReader reader) {
-		this(new PackConfig(), reader);
-	}
-
-	/**
 	 * Create writer for specified repository.
 	 * <p>
 	 * Objects for packing are specified in {@link #preparePack(Iterator)} or
@@ -1091,7 +1078,7 @@ public int getIndexVersion() {
 		if (indexVersion <= 0) {
 			for (BlockList<ObjectToPack> objs : objectsLists)
 				indexVersion = Math.max(indexVersion,
-						PackIndexWriter.oldestPossibleFormat(objs));
+						BasePackIndexWriter.oldestPossibleFormat(objs));
 		}
 		return indexVersion;
 	}
@@ -1112,12 +1099,28 @@ public int getIndexVersion() {
 	 *             the index data could not be written to the supplied stream.
 	 */
 	public void writeIndex(OutputStream indexStream) throws IOException {
+		writeIndex(BasePackIndexWriter.createVersion(indexStream,
+				getIndexVersion()));
+	}
+
+	/**
+	 * Create an index file to match the pack file just written.
+	 * <p>
+	 * Called after
+	 * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}.
+	 * <p>
+	 * Writing an index is only required for local pack storage. Packs sent on
+	 * the network do not need to create an index.
+	 *
+	 * @param iw
+	 *            an {@link PackIndexWriter} instance to write the index
+	 * @throws java.io.IOException
+	 *             the index data could not be written to the supplied stream.
+	 */
+	public void writeIndex(PackIndexWriter iw) throws IOException {
 		if (isIndexDisabled())
 			throw new IOException(JGitText.get().cachedPacksPreventsIndexCreation);
-
 		long writeStart = System.currentTimeMillis();
-		final PackIndexWriter iw = PackIndexWriter.createVersion(
-				indexStream, getIndexVersion());
 		iw.write(sortByName(), packcsum);
 		stats.timeWriting += System.currentTimeMillis() - writeStart;
 	}
@@ -2461,7 +2464,7 @@ private final boolean have(ObjectToPack ptr, AnyObjectId objectId) {
 	 * object graph at selected commits. Writing a bitmap index is an optional
 	 * feature that not all pack users may require.
 	 * <p>
-	 * Called after {@link #writeIndex(OutputStream)}.
+	 * Called after {@link #writeIndex(PackIndexWriter)}.
 	 * <p>
 	 * To reduce memory internal state is cleared during this method, rendering
 	 * the PackWriter instance useless for anything further than a call to write
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java
index dabc1f0..bf87c4c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java
@@ -14,6 +14,7 @@
 import static org.eclipse.jgit.revwalk.RevFlag.SEEN;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -28,16 +29,16 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.revwalk.AddUnseenToBitmapFilter;
 import org.eclipse.jgit.internal.storage.file.BitmapIndexImpl;
+import org.eclipse.jgit.internal.storage.file.BitmapIndexImpl.CompressedBitmap;
 import org.eclipse.jgit.internal.storage.file.PackBitmapIndex;
 import org.eclipse.jgit.internal.storage.file.PackBitmapIndexBuilder;
 import org.eclipse.jgit.internal.storage.file.PackBitmapIndexRemapper;
-import org.eclipse.jgit.internal.storage.file.BitmapIndexImpl.CompressedBitmap;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder;
 import org.eclipse.jgit.revwalk.BitmapWalker;
 import org.eclipse.jgit.revwalk.ObjectWalk;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -99,10 +100,10 @@ class PackWriterBitmapPreparer {
 		this.excessiveBranchCount = config.getBitmapExcessiveBranchCount();
 		this.excessiveBranchTipCount = Math.max(excessiveBranchCount,
 				config.getBitmapExcessiveBranchTipCount());
-		long now = SystemReader.getInstance().getCurrentTime();
+		Instant now = SystemReader.getInstance().now();
 		long ageInSeconds = (long) config.getBitmapInactiveBranchAgeInDays()
 				* DAY_IN_SECONDS;
-		this.inactiveBranchTimestamp = (now / 1000) - ageInSeconds;
+		this.inactiveBranchTimestamp = now.getEpochSecond() - ageInSeconds;
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java
index d07713d..e9ff027 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java
@@ -32,6 +32,8 @@
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.Arrays;
 import java.util.zip.DataFormatException;
 import java.util.zip.Inflater;
@@ -245,9 +247,9 @@ private String readValueString() {
 	private PersonIdent readPersonIdent() {
 		String name = readValueString();
 		String email = readValueString();
-		long ms = readVarint64() * 1000;
-		int tz = readInt16();
-		return new PersonIdent(name, email, ms, tz);
+		long epochSeconds = readVarint64();
+		ZoneOffset tz = ZoneOffset.ofTotalSeconds(readInt16() * 60);
+		return new PersonIdent(name, email, Instant.ofEpochSecond(epochSeconds), tz);
 	}
 
 	void readBlock(BlockSource src, long pos, int fileBlockSize)
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 3e75a9d..542d6e9 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
@@ -427,7 +427,35 @@ protected List<String> validate(String key, List<String> value) {
 		return value;
 	}
 
-	private static boolean patternMatchesHost(String pattern, String name) {
+	/**
+	 * Tells whether a given {@code name} matches the given list of patterns,
+	 * accounting for negative matches.
+	 *
+	 * @param patterns
+	 *            to test {@code name} against; any pattern starting with an
+	 *            exclamation mark is a negative pattern
+	 * @param name
+	 *            to test
+	 * @return {@code true} if the {@code name} matches at least one of the
+	 *         non-negative patterns and none of the negative patterns,
+	 *         {@code false} otherwise
+	 * @since 7.1
+	 */
+	public static boolean patternMatch(Iterable<String> patterns, String name) {
+		boolean doesMatch = false;
+		for (String pattern : patterns) {
+			if (pattern.startsWith("!")) { //$NON-NLS-1$
+				if (patternMatches(pattern.substring(1), name)) {
+					return false;
+				}
+			} else if (!doesMatch && patternMatches(pattern, name)) {
+				doesMatch = true;
+			}
+		}
+		return doesMatch;
+	}
+
+	private static boolean patternMatches(String pattern, String name) {
 		if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
 			final FileNameMatcher fn;
 			try {
@@ -680,18 +708,7 @@ public HostEntry(List<String> patterns) {
 		}
 
 		boolean matches(String hostName) {
-			boolean doesMatch = false;
-			for (String pattern : patterns) {
-				if (pattern.startsWith("!")) { //$NON-NLS-1$
-					if (patternMatchesHost(pattern.substring(1), hostName)) {
-						return false;
-					}
-				} else if (!doesMatch
-						&& patternMatchesHost(pattern, hostName)) {
-					doesMatch = true;
-				}
-			}
-			return doesMatch;
+			return patternMatch(patterns, hostName);
 		}
 
 		private static String toKey(String key) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java
index 3447f66..270b760 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java
@@ -53,24 +53,24 @@ public class Hard<T> implements Optionally<T> {
 		/**
 		 * The mutable optional object
 		 */
-		protected T element;
+		protected Optional<T> optional;
 
 		/**
 		 * @param element
 		 *            the mutable optional object
 		 */
 		public Hard(T element) {
-			this.element = element;
+			optional = Optional.ofNullable(element);
 		}
 
 		@Override
 		public void clear() {
-			element = null;
+			optional = Optional.empty();
 		}
 
 		@Override
 		public Optional<T> getOptional() {
-			return Optional.ofNullable(element);
+			return optional;
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbstractGpgSignatureVerifier.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbstractGpgSignatureVerifier.java
deleted file mode 100644
index 06a89dc..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbstractGpgSignatureVerifier.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2024, Thomas Wolf <twolf@apache.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.lib;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.util.RawParseUtils;
-
-/**
- * Provides a base implementation of
- * {@link GpgSignatureVerifier#verifySignature(RevObject, GpgConfig)}.
- *
- * @since 6.9
- */
-public abstract class AbstractGpgSignatureVerifier
-		implements GpgSignatureVerifier {
-
-	@Override
-	public SignatureVerification verifySignature(RevObject object,
-			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.nextLfSkippingSplitLines(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(config, 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(config, data, signatureData);
-		}
-		return null;
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java
index c58133a..f742e99 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java
@@ -35,23 +35,6 @@ public abstract class AnyObjectId implements Comparable<AnyObjectId> {
 	 * @param secondObjectId
 	 *            the second identifier to compare. Must not be null.
 	 * @return true if the two identifiers are the same.
-	 * @deprecated use {@link #isEqual(AnyObjectId, AnyObjectId)} instead
-	 */
-	@Deprecated
-	@SuppressWarnings("AmbiguousMethodReference")
-	public static boolean equals(final AnyObjectId firstObjectId,
-			final AnyObjectId secondObjectId) {
-		return isEqual(firstObjectId, secondObjectId);
-	}
-
-	/**
-	 * Compare two object identifier byte sequences for equality.
-	 *
-	 * @param firstObjectId
-	 *            the first identifier to compare. Must not be null.
-	 * @param secondObjectId
-	 *            the second identifier to compare. Must not be null.
-	 * @return true if the two identifiers are the same.
 	 * @since 5.4
 	 */
 	public static boolean isEqual(final AnyObjectId firstObjectId,
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 5dfb648..0c1da83 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
@@ -13,13 +13,17 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BARE;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WORKTREE;
+import static org.eclipse.jgit.lib.Constants.CONFIG;
 import static org.eclipse.jgit.lib.Constants.DOT_GIT;
+import static org.eclipse.jgit.lib.Constants.GITDIR_FILE;
 import static org.eclipse.jgit.lib.Constants.GIT_ALTERNATE_OBJECT_DIRECTORIES_KEY;
 import static org.eclipse.jgit.lib.Constants.GIT_CEILING_DIRECTORIES_KEY;
+import static org.eclipse.jgit.lib.Constants.GIT_COMMON_DIR_KEY;
 import static org.eclipse.jgit.lib.Constants.GIT_DIR_KEY;
 import static org.eclipse.jgit.lib.Constants.GIT_INDEX_FILE_KEY;
 import static org.eclipse.jgit.lib.Constants.GIT_OBJECT_DIRECTORY_KEY;
 import static org.eclipse.jgit.lib.Constants.GIT_WORK_TREE_KEY;
+import static org.eclipse.jgit.lib.Constants.OBJECTS;
 
 import java.io.File;
 import java.io.IOException;
@@ -70,7 +74,21 @@ private static boolean isSymRef(byte[] ref) {
 				&& ref[7] == ' ';
 	}
 
-	private static File getSymRef(File workTree, File dotGit, FS fs)
+	/**
+	 * Read symbolic reference file
+	 *
+	 * @param workTree
+	 *            the work tree path
+	 * @param dotGit
+	 *            the .git file
+	 * @param fs
+	 *            th FS util
+	 * @return the file read from symbolic reference file
+	 * @throws java.io.IOException
+	 *             the dotGit file is invalid reference
+	 * @since 7.0
+	 */
+	static File getSymRef(File workTree, File dotGit, FS fs)
 			throws IOException {
 		byte[] content = IO.readFully(dotGit);
 		if (!isSymRef(content)) {
@@ -102,6 +120,8 @@ private static File getSymRef(File workTree, File dotGit, FS fs)
 
 	private File gitDir;
 
+	private File gitCommonDir;
+
 	private File objectDirectory;
 
 	private List<File> alternateObjectDirectories;
@@ -172,6 +192,30 @@ public File getGitDir() {
 	}
 
 	/**
+	 * Set common dir.
+	 *
+	 * @param gitCommonDir
+	 *            {@code GIT_COMMON_DIR}, the common repository meta directory.
+	 * @return {@code this} (for chaining calls).
+	 * @since 7.0
+	 */
+	public B setGitCommonDir(File gitCommonDir) {
+		this.gitCommonDir = gitCommonDir;
+		this.config = null;
+		return self();
+	}
+
+	/**
+	 * Get common dir.
+	 *
+	 * @return common dir; null if not set.
+	 * @since 7.0
+	 */
+	public File getGitCommonDir() {
+		return gitCommonDir;
+	}
+
+	/**
 	 * Set the directory storing the repository's objects.
 	 *
 	 * @param objectDirectory
@@ -396,9 +440,9 @@ public B setInitialBranch(String branch) throws InvalidRefNameException {
 	 * Read standard Git environment variables and configure from those.
 	 * <p>
 	 * This method tries to read the standard Git environment variables, such as
-	 * {@code GIT_DIR} and {@code GIT_WORK_TREE} to configure this builder
-	 * instance. If an environment variable is set, it overrides the value
-	 * already set in this builder.
+	 * {@code GIT_DIR}, {@code GIT_COMMON_DIR}, {@code GIT_WORK_TREE} etc. to
+	 * configure this builder instance. If an environment variable is set, it
+	 * overrides the value already set in this builder.
 	 *
 	 * @return {@code this} (for chaining calls).
 	 */
@@ -410,9 +454,9 @@ public B readEnvironment() {
 	 * Read standard Git environment variables and configure from those.
 	 * <p>
 	 * This method tries to read the standard Git environment variables, such as
-	 * {@code GIT_DIR} and {@code GIT_WORK_TREE} to configure this builder
-	 * instance. If a property is already set in the builder, the environment
-	 * variable is not used.
+	 * {@code GIT_DIR}, {@code GIT_COMMON_DIR}, {@code GIT_WORK_TREE} etc. to
+	 * configure this builder instance. If a property is already set in the
+	 * builder, the environment variable is not used.
 	 *
 	 * @param sr
 	 *            the SystemReader abstraction to access the environment.
@@ -425,6 +469,13 @@ public B readEnvironment(SystemReader sr) {
 				setGitDir(new File(val));
 		}
 
+		if (getGitCommonDir() == null) {
+			String val = sr.getenv(GIT_COMMON_DIR_KEY);
+			if (val != null) {
+				setGitCommonDir(new File(val));
+			}
+		}
+
 		if (getObjectDirectory() == null) {
 			String val = sr.getenv(GIT_OBJECT_DIRECTORY_KEY);
 			if (val != null)
@@ -434,7 +485,7 @@ public B readEnvironment(SystemReader sr) {
 		if (getAlternateObjectDirectories() == null) {
 			String val = sr.getenv(GIT_ALTERNATE_OBJECT_DIRECTORIES_KEY);
 			if (val != null) {
-				for (String path : val.split(File.pathSeparator))
+				for (String path : val.split(File.pathSeparator, -1))
 					addAlternateObjectDirectory(new File(path));
 			}
 		}
@@ -454,7 +505,7 @@ public B readEnvironment(SystemReader sr) {
 		if (ceilingDirectories == null) {
 			String val = sr.getenv(GIT_CEILING_DIRECTORIES_KEY);
 			if (val != null) {
-				for (String path : val.split(File.pathSeparator))
+				for (String path : val.split(File.pathSeparator, -1))
 					addCeilingDirectory(new File(path));
 			}
 		}
@@ -601,6 +652,7 @@ public B findGitDir(File current) {
 	public B setup() throws IllegalArgumentException, IOException {
 		requireGitDirOrWorkTree();
 		setupGitDir();
+		setupCommonDir();
 		setupWorkTree();
 		setupInternals();
 		return self();
@@ -658,6 +710,20 @@ protected void setupGitDir() throws IOException {
 	}
 
 	/**
+	 * Perform standard common dir initialization.
+	 *
+	 * @throws java.io.IOException
+	 *             the repository could not be accessed
+	 * @since 7.0
+	 */
+	protected void setupCommonDir() throws IOException {
+		// no gitCommonDir? Try to get it from gitDir
+		if (getGitCommonDir() == null) {
+			setGitCommonDir(safeFS().getCommonDir(getGitDir()));
+		}
+	}
+
+	/**
 	 * Perform standard work-tree initialization.
 	 * <p>
 	 * This is a method typically invoked inside of {@link #setup()}, near the
@@ -695,8 +761,12 @@ protected void setupWorkTree() throws IOException {
 	 *             the repository could not be accessed
 	 */
 	protected void setupInternals() throws IOException {
-		if (getObjectDirectory() == null && getGitDir() != null)
-			setObjectDirectory(safeFS().resolve(getGitDir(), Constants.OBJECTS));
+		if (getObjectDirectory() == null) {
+			File commonDir = getGitCommonDir();
+			if (commonDir != null) {
+				setObjectDirectory(safeFS().resolve(commonDir, OBJECTS));
+			}
+		}
 	}
 
 	/**
@@ -723,12 +793,13 @@ protected Config getConfig() throws IOException {
 	 *             the configuration is not available.
 	 */
 	protected Config loadConfig() throws IOException {
-		if (getGitDir() != null) {
+		File commonDir = getGitCommonDir();
+		if (commonDir != null) {
 			// We only want the repository's configuration file, and not
 			// the user file, as these parameters must be unique to this
 			// repository and not inherited from other files.
 			//
-			File path = safeFS().resolve(getGitDir(), Constants.CONFIG);
+			File path = safeFS().resolve(commonDir, CONFIG);
 			FileBasedConfig cfg = new FileBasedConfig(path, safeFS());
 			try {
 				cfg.load();
@@ -749,8 +820,29 @@ private File guessWorkTreeOrFail() throws IOException {
 		//
 		String path = cfg.getString(CONFIG_CORE_SECTION, null,
 				CONFIG_KEY_WORKTREE);
-		if (path != null)
+		if (path != null) {
 			return safeFS().resolve(getGitDir(), path).getCanonicalFile();
+		}
+
+		/*
+		 * We are in worktree's $GIT_DIR folder
+		 * ".git/worktrees/&lt;worktree-name&gt;" and want to get the working
+		 * tree (checkout) path; so here we have an opposite link in file
+		 * "gitdir" showing to the ".git" file located in the working tree read
+		 * it and convert it to absolute path if it's relative
+		 */
+		File gitDirFile = new File(getGitDir(), GITDIR_FILE);
+		if (gitDirFile.isFile()) {
+			String workDirPath = new String(IO.readFully(gitDirFile)).trim();
+			File workTreeDotGitFile = new File(workDirPath);
+			if (!workTreeDotGitFile.isAbsolute()) {
+				workTreeDotGitFile = new File(getGitDir(), workDirPath)
+						.getCanonicalFile();
+			}
+			if (workTreeDotGitFile != null) {
+				return workTreeDotGitFile.getParentFile();
+			}
+		}
 
 		// If core.bare is set, honor its value. Assume workTree is
 		// the parent directory of the repository.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java
index e15c7af..7921052 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java
@@ -187,8 +187,7 @@ public boolean isRebase() {
 	 * @since 4.5
 	 */
 	public BranchRebaseMode getRebaseMode() {
-		return config.getEnum(BranchRebaseMode.values(),
-				ConfigConstants.CONFIG_BRANCH_SECTION, branchName,
+		return config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branchName,
 				ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.NONE);
 	}
 
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 ea33082..ad3c2c0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
@@ -194,19 +194,6 @@ public void addParentId(AnyObjectId additionalParent) {
 		}
 	}
 
-	/**
-	 * Set the encoding for the commit information.
-	 *
-	 * @param encodingName
-	 *            the encoding name. See
-	 *            {@link java.nio.charset.Charset#forName(String)}.
-	 * @deprecated use {@link #setEncoding(Charset)} instead.
-	 */
-	@Deprecated
-	public void setEncoding(String encodingName) {
-		setEncoding(Charset.forName(encodingName));
-	}
-
 	@Override
 	public byte[] build() throws UnsupportedEncodingException {
 		ByteArrayOutputStream os = new ByteArrayOutputStream();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
index f701a41..b1ba5df 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java
@@ -119,7 +119,7 @@ private CommitConfig(Config rc) {
 		if (!StringUtils.isEmptyOrNull(comment)) {
 			if ("auto".equalsIgnoreCase(comment)) { //$NON-NLS-1$
 				autoCommentChar = true;
-			} else {
+			} else if (comment != null) {
 				char first = comment.charAt(0);
 				if (first > ' ' && first < 127) {
 					commentCharacter = first;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
index 07c5fa4..345cb22 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
@@ -34,6 +34,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.events.ConfigChangedEvent;
 import org.eclipse.jgit.events.ConfigChangedListener;
@@ -254,9 +255,8 @@ static String escapeSubsection(String x) {
 	 *            default value to return if no value was present.
 	 * @return an integer value from the configuration, or defaultValue.
 	 */
-	public int getInt(final String section, final String name,
-			final int defaultValue) {
-		return typedGetter.getInt(this, section, null, name, defaultValue);
+	public int getInt(String section, String name, int defaultValue) {
+		return getInt(section, null, name, defaultValue);
 	}
 
 	/**
@@ -264,6 +264,23 @@ public int getInt(final String section, final String name,
 	 *
 	 * @param section
 	 *            section the key is grouped within.
+	 * @param name
+	 *            name of the key to get.
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Integer getInt(String section, String name) {
+		return getInt(section, null, name);
+	}
+
+
+	/**
+	 * Obtain an integer value from the configuration.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
 	 * @param subsection
 	 *            subsection name, such a remote or branch name.
 	 * @param name
@@ -272,10 +289,30 @@ public int getInt(final String section, final String name,
 	 *            default value to return if no value was present.
 	 * @return an integer value from the configuration, or defaultValue.
 	 */
-	public int getInt(final String section, String subsection,
-			final String name, final int defaultValue) {
+	public int getInt(String section, String subsection, String name,
+			int defaultValue) {
+		Integer v = typedGetter.getInt(this, section, subsection, name,
+				Integer.valueOf(defaultValue));
+		return v == null ? defaultValue : v.intValue();
+	}
+
+	/**
+	 * Obtain an integer value from the configuration.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Integer getInt(String section, String subsection, String name) {
 		return typedGetter.getInt(this, section, subsection, name,
-				defaultValue);
+				null);
 	}
 
 	/**
@@ -297,8 +334,30 @@ public int getInt(final String section, String subsection,
 	 */
 	public int getIntInRange(String section, String name, int minValue,
 			int maxValue, int defaultValue) {
-		return typedGetter.getIntInRange(this, section, null, name, minValue,
-				maxValue, defaultValue);
+		return getIntInRange(section, null, name,
+				minValue, maxValue, defaultValue);
+	}
+
+	/**
+	 * Obtain an integer value from the configuration which must be inside given
+	 * range.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param name
+	 *            name of the key to get.
+	 * @param minValue
+	 *            minimum value
+	 * @param maxValue
+	 *            maximum value
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Integer getIntInRange(String section, String name, int minValue,
+			int maxValue) {
+		return getIntInRange(section, null, name, minValue, maxValue);
 	}
 
 	/**
@@ -322,8 +381,34 @@ public int getIntInRange(String section, String name, int minValue,
 	 */
 	public int getIntInRange(String section, String subsection, String name,
 			int minValue, int maxValue, int defaultValue) {
+		Integer v = typedGetter.getIntInRange(this, section, subsection, name,
+				minValue, maxValue, Integer.valueOf(defaultValue));
+		return v == null ? defaultValue : v.intValue();
+	}
+
+	/**
+	 * Obtain an integer value from the configuration which must be inside given
+	 * range.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param minValue
+	 *            minimum value
+	 * @param maxValue
+	 *            maximum value
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Integer getIntInRange(String section, String subsection, String name,
+			int minValue, int maxValue) {
 		return typedGetter.getIntInRange(this, section, subsection, name,
-				minValue, maxValue, defaultValue);
+				minValue, maxValue, null);
 	}
 
 	/**
@@ -338,7 +423,23 @@ public int getIntInRange(String section, String subsection, String name,
 	 * @return an integer value from the configuration, or defaultValue.
 	 */
 	public long getLong(String section, String name, long defaultValue) {
-		return typedGetter.getLong(this, section, null, name, defaultValue);
+		return getLong(section, null, name, defaultValue);
+	}
+
+	/**
+	 * Obtain an integer value from the configuration.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param name
+	 *            name of the key to get.
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Long getLong(String section, String name) {
+		return getLong(section, null, name);
 	}
 
 	/**
@@ -355,9 +456,28 @@ public long getLong(String section, String name, long defaultValue) {
 	 * @return an integer value from the configuration, or defaultValue.
 	 */
 	public long getLong(final String section, String subsection,
-			final String name, final long defaultValue) {
-		return typedGetter.getLong(this, section, subsection, name,
-				defaultValue);
+			String name, long defaultValue) {
+		Long v = typedGetter.getLong(this, section, subsection, name,
+				Long.valueOf(defaultValue));
+		return v == null ? defaultValue : v.longValue();
+	}
+
+	/**
+	 * Obtain an integer value from the configuration.
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @return an integer value from the configuration, or {@code null} if not
+	 *         set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Long getLong(String section, String subsection, String name) {
+		return typedGetter.getLong(this, section, subsection, name, null);
 	}
 
 	/**
@@ -372,9 +492,26 @@ public long getLong(final String section, String subsection,
 	 * @return true if any value or defaultValue is true, false for missing or
 	 *         explicit false
 	 */
-	public boolean getBoolean(final String section, final String name,
-			final boolean defaultValue) {
-		return typedGetter.getBoolean(this, section, null, name, defaultValue);
+	public boolean getBoolean(String section, String name,
+			boolean defaultValue) {
+		Boolean v = typedGetter.getBoolean(this, section, null, name,
+				Boolean.valueOf(defaultValue));
+		return v == null ? defaultValue : v.booleanValue();
+	}
+
+	/**
+	 * Get a boolean value from the git config
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param name
+	 *            name of the key to get.
+	 * @return configured boolean value, or {@code null} if not set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Boolean getBoolean(String section, String name) {
+		return getBoolean(section, null, name);
 	}
 
 	/**
@@ -391,10 +528,28 @@ public boolean getBoolean(final String section, final String name,
 	 * @return true if any value or defaultValue is true, false for missing or
 	 *         explicit false
 	 */
-	public boolean getBoolean(final String section, String subsection,
-			final String name, final boolean defaultValue) {
-		return typedGetter.getBoolean(this, section, subsection, name,
-				defaultValue);
+	public boolean getBoolean(String section, String subsection, String name,
+			boolean defaultValue) {
+		Boolean v = typedGetter.getBoolean(this, section, subsection, name,
+				Boolean.valueOf(defaultValue));
+		return v == null ? defaultValue : v.booleanValue();
+	}
+
+	/**
+	 * Get a boolean value from the git config
+	 *
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @return configured boolean value, or {@code null} if not set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Boolean getBoolean(String section, String subsection, String name) {
+		return typedGetter.getBoolean(this, section, subsection, name, null);
 	}
 
 	/**
@@ -412,8 +567,8 @@ public boolean getBoolean(final String section, String subsection,
 	 *            default value to return if no value was present.
 	 * @return the selected enumeration value, or {@code defaultValue}.
 	 */
-	public <T extends Enum<?>> T getEnum(final String section,
-			final String subsection, final String name, final T defaultValue) {
+	public <T extends Enum<?>> T getEnum(String section, String subsection,
+			String name, @NonNull T defaultValue) {
 		final T[] all = allValuesOf(defaultValue);
 		return typedGetter.getEnum(this, all, section, subsection, name,
 				defaultValue);
@@ -448,14 +603,41 @@ public <T extends Enum<?>> T getEnum(final String section,
 	 * @param defaultValue
 	 *            default value to return if no value was present.
 	 * @return the selected enumeration value, or {@code defaultValue}.
+	 * @deprecated use {@link #getEnum(String, String, String, Enum)} or
+	 *             {{@link #getEnum(Enum[], String, String, String)}} instead.
 	 */
-	public <T extends Enum<?>> T getEnum(final T[] all, final String section,
-			final String subsection, final String name, final T defaultValue) {
+	@Nullable
+	@Deprecated
+	public <T extends Enum<?>> T getEnum(T[] all, String section,
+			String subsection, String name, @Nullable T defaultValue) {
 		return typedGetter.getEnum(this, all, section, subsection, name,
 				defaultValue);
 	}
 
 	/**
+	 * Parse an enumeration from the configuration.
+	 *
+	 * @param <T>
+	 *            type of the returned enum
+	 * @param all
+	 *            all possible values in the enumeration which should be
+	 *            recognized. Typically {@code EnumType.values()}.
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @return the selected enumeration value, or {@code null} if not set.
+	 * @since 7.2
+	 */
+	@Nullable
+	public <T extends Enum<?>> T getEnum(T[] all, String section,
+			String subsection, String name) {
+		return typedGetter.getEnum(this, all, section, subsection, name, null);
+	}
+
+	/**
 	 * Get string value or null if not found.
 	 *
 	 * @param section
@@ -466,8 +648,8 @@ public <T extends Enum<?>> T getEnum(final T[] all, final String section,
 	 *            the key name
 	 * @return a String value from the config, <code>null</code> if not found
 	 */
-	public String getString(final String section, String subsection,
-			final String name) {
+	@Nullable
+	public String getString(String section, String subsection, String name) {
 		return getRawString(section, subsection, name);
 	}
 
@@ -526,8 +708,34 @@ public String getString(final String section, String subsection,
 	 */
 	public long getTimeUnit(String section, String subsection, String name,
 			long defaultValue, TimeUnit wantUnit) {
+		Long v = typedGetter.getTimeUnit(this, section, subsection, name,
+				Long.valueOf(defaultValue), wantUnit);
+		return v == null ? defaultValue : v.longValue();
+
+	}
+
+	/**
+	 * Parse a numerical time unit, such as "1 minute", from the configuration.
+	 *
+	 * @param section
+	 *            section the key is in.
+	 * @param subsection
+	 *            subsection the key is in, or null if not in a subsection.
+	 * @param name
+	 *            the key name.
+	 * @param wantUnit
+	 *            the units of {@code defaultValue} and the return value, as
+	 *            well as the units to assume if the value does not contain an
+	 *            indication of the units.
+	 * @return the value, or {@code null} if not set, expressed in
+	 *         {@code units}.
+	 * @since 7.2
+	 */
+	@Nullable
+	public Long getTimeUnit(String section, String subsection, String name,
+			TimeUnit wantUnit) {
 		return typedGetter.getTimeUnit(this, section, subsection, name,
-				defaultValue, wantUnit);
+				null, wantUnit);
 	}
 
 	/**
@@ -555,8 +763,9 @@ public long getTimeUnit(String section, String subsection, String name,
 	 * @return the {@link Path}, or {@code defaultValue} if not set
 	 * @since 5.10
 	 */
+	@Nullable
 	public Path getPath(String section, String subsection, String name,
-			@NonNull FS fs, File resolveAgainst, Path defaultValue) {
+			@NonNull FS fs, File resolveAgainst, @Nullable Path defaultValue) {
 		return typedGetter.getPath(this, section, subsection, name, fs,
 				resolveAgainst, defaultValue);
 	}
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 0edf3c5..c455032 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -78,6 +78,13 @@ public final class ConfigConstants {
 	public static final String CONFIG_DFS_SECTION = "dfs";
 
 	/**
+	 * The dfs cache subsection prefix.
+	 *
+	 * @since 7.0
+	 */
+	public static final String CONFIG_DFS_CACHE_PREFIX = "dfs.";
+
+	/**
 	 * The "receive" section
 	 * @since 4.6
 	 */
@@ -199,7 +206,36 @@ public final class ConfigConstants {
 	public static final String CONFIG_KEY_SIGNINGKEY = "signingKey";
 
 	/**
+	 * The "ssh" subsection key.
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_SSH_SUBSECTION = "ssh";
+
+	/**
+	 * The "defaultKeyCommand" key.
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_KEY_SSH_DEFAULT_KEY_COMMAND = "defaultKeyCommand";
+
+	/**
+	 * The "allowedSignersFile" key.
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_KEY_SSH_ALLOWED_SIGNERS_FILE = "allowedSignersFile";
+
+	/**
+	 * The "revocationFile" key,
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_KEY_SSH_REVOCATION_FILE = "revocationFile";
+
+	/**
 	 * The "commit" section
+	 *
 	 * @since 5.2
 	 */
 	public static final String CONFIG_COMMIT_SECTION = "commit";
@@ -332,6 +368,13 @@ public final class ConfigConstants {
 	public static final String CONFIG_KEY_DELTA_BASE_CACHE_LIMIT = "deltaBaseCacheLimit";
 
 	/**
+	 * The "packExtensions" key
+	 *
+	 * @since 7.0
+	 **/
+	public static final String CONFIG_KEY_PACK_EXTENSIONS = "packExtensions";
+
+	/**
 	 * The "symlinks" key
 	 * @since 3.3
 	 */
@@ -345,12 +388,6 @@ public final class ConfigConstants {
 	public static final String CONFIG_KEY_STREAM_FILE_THRESHOLD = "streamFileThreshold";
 
 	/**
-	 * @deprecated typo, use CONFIG_KEY_STREAM_FILE_THRESHOLD instead
-	 */
-	@Deprecated(since = "6.8")
-	public static final String CONFIG_KEY_STREAM_FILE_TRESHOLD = CONFIG_KEY_STREAM_FILE_THRESHOLD;
-
-	/**
 	 * The "packedGitMmap" key
 	 * @since 5.1.13
 	 */
@@ -409,6 +446,13 @@ public final class ConfigConstants {
 	/** The "rebase" key */
 	public static final String CONFIG_KEY_REBASE = "rebase";
 
+	/**
+	 * The "checkout" key
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_KEY_CHECKOUT = "checkout";
+
 	/** The "url" key */
 	public static final String CONFIG_KEY_URL = "url";
 
@@ -556,11 +600,21 @@ public final class ConfigConstants {
 
 	/**
 	 * The "trustfolderstat" key in the "core" section
+	 *
 	 * @since 3.6
+	 * @deprecated use {CONFIG_KEY_TRUST_STAT} instead
 	 */
+	@Deprecated(since = "7.2", forRemoval = true)
 	public static final String CONFIG_KEY_TRUSTFOLDERSTAT = "trustfolderstat";
 
 	/**
+	 * The "trustfilestat" key in the "core"section
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_KEY_TRUST_STAT = "truststat";
+
+	/**
 	 * The "supportsAtomicFileCreation" key in the "core" section
 	 *
 	 * @since 4.5
@@ -979,6 +1033,27 @@ public final class ConfigConstants {
 	public static final String CONFIG_KEY_TRUST_LOOSE_REF_STAT = "trustLooseRefStat";
 
 	/**
+	 * The "trustLooseRefStat" key
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_KEY_TRUST_PACK_STAT = "trustPackStat";
+
+	/**
+	 * The "trustLooseObjectFileStat" key
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_KEY_TRUST_LOOSE_OBJECT_STAT = "trustLooseObjectStat";
+
+	/**
+	 * The "trustTablesListStat" key
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_KEY_TRUST_TABLESLIST_STAT = "trustTablesListStat";
+
+	/**
 	 * The "pack.preserveOldPacks" key
 	 *
 	 * @since 5.13.2
@@ -1012,4 +1087,32 @@ public final class ConfigConstants {
 	 * @since 6.7
 	 */
 	public static final String CONFIG_KEY_READ_CHANGED_PATHS = "readChangedPaths";
+
+	/**
+	 * The "useObjectSizeIndex" key
+	 *
+	 * @since 7.0
+	 */
+	public static final String CONFIG_KEY_USE_OBJECT_SIZE_INDEX = "useObjectSizeIndex";
+
+	/**
+	 * The "loadRevIndexInParallel" key
+	 *
+	 * @since 7.1
+	 */
+	public static final String CONFIG_KEY_LOAD_REV_INDEX_IN_PARALLEL = "loadRevIndexInParallel";
+
+	/**
+	 * The "reftable" section
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_REFTABLE_SECTION = "reftable";
+
+	/**
+	 * The "autorefresh" key
+	 *
+	 * @since 7.2
+	 */
+	public static final String CONFIG_KEY_AUTOREFRESH = "autorefresh";
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
index 60a23dd..997f4ed 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
@@ -12,10 +12,13 @@
 
 package org.eclipse.jgit.lib;
 
+import static java.nio.charset.StandardCharsets.US_ASCII;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CodingErrorAction;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.text.MessageFormat;
@@ -203,24 +206,6 @@ public final class Constants {
 	 */
 	public static final byte[] PACK_SIGNATURE = { 'P', 'A', 'C', 'K' };
 
-	/**
-	 * Native character encoding for commit messages, file names...
-	 *
-	 * @deprecated Use {@link java.nio.charset.StandardCharsets#UTF_8} directly
-	 *             instead.
-	 */
-	@Deprecated
-	public static final Charset CHARSET;
-
-	/**
-	 * Native character encoding for commit messages, file names...
-	 *
-	 * @deprecated Use {@link java.nio.charset.StandardCharsets#UTF_8} directly
-	 *             instead.
-	 */
-	@Deprecated
-	public static final String CHARACTER_ENCODING;
-
 	/** Default main branch name */
 	public static final String MASTER = "master";
 
@@ -273,6 +258,20 @@ public final class Constants {
 	public static final String INFO_REFS = "info/refs";
 
 	/**
+	 * Name of heads folder or file in refs.
+	 *
+	 * @since 7.0
+	 */
+	public static final String HEADS = "heads";
+
+	/**
+	 * Prefix for any log.
+	 *
+	 * @since 7.0
+	 */
+	public static final String L_LOGS = LOGS + "/";
+
+	/**
 	 * Info alternates file (goes under OBJECTS)
 	 * @since 5.5
 	 */
@@ -358,6 +357,14 @@ public final class Constants {
 	public static final String GIT_DIR_KEY = "GIT_DIR";
 
 	/**
+	 * The environment variable that tells us which directory is the common
+	 * ".git" directory.
+	 *
+	 * @since 7.0
+	 */
+	public static final String GIT_COMMON_DIR_KEY = "GIT_COMMON_DIR";
+
+	/**
 	 * The environment variable that tells us which directory is the working
 	 * directory.
 	 */
@@ -459,6 +466,36 @@ public final class Constants {
 	public static final String GITDIR = "gitdir: ";
 
 	/**
+	 * Name of the file (inside gitDir) that references the worktree's .git
+	 * file (opposite link).
+	 *
+	 * .git/worktrees/&lt;worktree-name&gt;/gitdir
+	 *
+	 * A text file containing the absolute path back to the .git file that
+	 * points here. This file is used to verify if the linked repository has been
+	 * manually removed in which case this directory is no longer needed.
+	 * The modification time (mtime) of this file should be updated each time
+	 * the linked repository is accessed.
+	 *
+	 * @since 7.0
+	 */
+	public static final String GITDIR_FILE = "gitdir";
+
+	/**
+	 * Name of the file (inside gitDir) that has reference to $GIT_COMMON_DIR.
+	 *
+	 * .git/worktrees/&lt;worktree-name&gt;/commondir
+	 *
+	 * If this file exists, $GIT_COMMON_DIR will be set to the path specified in
+	 * this file unless it is explicitly set. If the specified path is relative,
+	 * it is relative to $GIT_DIR. The repository with commondir is incomplete
+	 * without the repository pointed by "commondir".
+	 *
+	 * @since 7.0
+	 */
+	public static final String COMMONDIR_FILE = "commondir";
+
+	/**
 	 * Name of the folder (inside gitDir) where submodules are stored
 	 *
 	 * @since 3.6
@@ -494,6 +531,34 @@ public final class Constants {
 	public static final String ATTR_BUILTIN_BINARY_MERGER = "binary"; //$NON-NLS-1$
 
 	/**
+	 * Prefix of a GPG signature.
+	 *
+	 * @since 7.0
+	 */
+	public static final String GPG_SIGNATURE_PREFIX = "-----BEGIN PGP SIGNATURE-----"; //$NON-NLS-1$
+
+	/**
+	 * Prefix of a CMS signature (X.509, S/MIME).
+	 *
+	 * @since 7.0
+	 */
+	public static final String CMS_SIGNATURE_PREFIX = "-----BEGIN SIGNED MESSAGE-----"; //$NON-NLS-1$
+
+	/**
+	 * Prefix of an SSH signature.
+	 *
+	 * @since 7.0
+	 */
+	public static final String SSH_SIGNATURE_PREFIX = "-----BEGIN SSH SIGNATURE-----"; //$NON-NLS-1$
+
+	/**
+	 * Union built-in merge driver
+	 *
+	 * @since 6.10.1
+	 */
+	public static final String ATTR_BUILTIN_UNION_MERGE_DRIVER = "union"; //$NON-NLS-1$
+
+	/**
 	 * Create a new digest function for objects.
 	 *
 	 * @return a new digest object.
@@ -661,44 +726,32 @@ public static int decodeTypeString(final AnyObjectId id,
 	 *             the 7-bit ASCII character space.
 	 */
 	public static byte[] encodeASCII(String s) {
-		final byte[] r = new byte[s.length()];
-		for (int k = r.length - 1; k >= 0; k--) {
-			final char c = s.charAt(k);
-			if (c > 127)
-				throw new IllegalArgumentException(MessageFormat.format(JGitText.get().notASCIIString, s));
-			r[k] = (byte) c;
+		try {
+			CharsetEncoder encoder = US_ASCII.newEncoder()
+					.onUnmappableCharacter(CodingErrorAction.REPORT)
+					.onMalformedInput(CodingErrorAction.REPORT);
+			return encoder.encode(CharBuffer.wrap(s)).array();
+		} catch (CharacterCodingException e) {
+			throw new IllegalArgumentException(
+					MessageFormat.format(JGitText.get().notASCIIString, s), e);
 		}
-		return r;
 	}
 
 	/**
-	 * Convert a string to a byte array in the standard character encoding.
+	 * Convert a string to a byte array in the standard character encoding UTF8.
 	 *
 	 * @param str
 	 *            the string to convert. May contain any Unicode characters.
 	 * @return a byte array representing the requested string, encoded using the
 	 *         default character encoding (UTF-8).
-	 * @see #CHARACTER_ENCODING
 	 */
 	public static byte[] encode(String str) {
-		final ByteBuffer bb = UTF_8.encode(str);
-		final int len = bb.limit();
-		if (bb.hasArray() && bb.arrayOffset() == 0) {
-			final byte[] arr = bb.array();
-			if (arr.length == len)
-				return arr;
-		}
-
-		final byte[] arr = new byte[len];
-		bb.get(arr);
-		return arr;
+		return str.getBytes(UTF_8);
 	}
 
 	static {
 		if (OBJECT_ID_LENGTH != newMessageDigest().getDigestLength())
 			throw new LinkageError(JGitText.get().incorrectOBJECT_ID_LENGTH);
-		CHARSET = UTF_8;
-		CHARACTER_ENCODING = UTF_8.name();
 	}
 
 	/** name of the file containing the commit msg for a merge commit */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java
index 9fa5d75..0e27b27 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java
@@ -17,12 +17,16 @@
 
 import static java.util.zip.Deflater.DEFAULT_COMPRESSION;
 
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Config.SectionParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * This class keeps git repository core parameters.
  */
 public class CoreConfig {
+	private static final Logger LOG = LoggerFactory.getLogger(CoreConfig.class);
 	/** Key for {@link Config#get(SectionParser)}. */
 	public static final Config.SectionParser<CoreConfig> KEY = CoreConfig::new;
 
@@ -127,7 +131,9 @@ public enum LogRefUpdates {
 	 * Permissible values for {@code core.trustPackedRefsStat}.
 	 *
 	 * @since 6.1.1
+	 * @deprecated use {@link TrustStat} instead
 	 */
+	@Deprecated(since = "7.2", forRemoval = true)
 	public enum TrustPackedRefsStat {
 		/** Do not trust file attributes of the packed-refs file. */
 		NEVER,
@@ -135,12 +141,15 @@ public enum TrustPackedRefsStat {
 		/** Trust file attributes of the packed-refs file. */
 		ALWAYS,
 
-		/** Open and close the packed-refs file to refresh its file attributes
-		 * and then trust it. */
+		/**
+		 * Open and close the packed-refs file to refresh its file attributes
+		 * and then trust it.
+		 */
 		AFTER_OPEN,
 
-		/** {@code core.trustPackedRefsStat} defaults to this when it is
-		 * not set */
+		/**
+		 * {@code core.trustPackedRefsStat} defaults to this when it is not set
+		 */
 		UNSET
 	}
 
@@ -148,29 +157,66 @@ public enum TrustPackedRefsStat {
 	 * Permissible values for {@code core.trustLooseRefStat}.
 	 *
 	 * @since 6.9
+	 * @deprecated use {@link TrustStat} instead
 	 */
+	@Deprecated(since = "7.2", forRemoval = true)
 	public enum TrustLooseRefStat {
 
 		/** Trust file attributes of the loose ref. */
 		ALWAYS,
 
-		/** Open and close parent directories of the loose ref file until the
-		 * repository root to refresh its file attributes and then trust it. */
+		/**
+		 * Open and close parent directories of the loose ref file until the
+		 * repository root to refresh its file attributes and then trust it.
+		 */
 		AFTER_OPEN,
 	}
 
+	/**
+	 * Values for {@code core.trustXXX} options.
+	 *
+	 * @since 7.2
+	 */
+	public enum TrustStat {
+		/** Do not trust file attributes of a File. */
+		NEVER,
+
+		/** Always trust file attributes of a File. */
+		ALWAYS,
+
+		/** Open and close the File to refresh its file attributes
+		 * and then trust it. */
+		AFTER_OPEN,
+
+		/**
+		 * Used for specific options to inherit value from value set for
+		 * core.trustStat.
+		 */
+		INHERIT
+	}
+
 	private final int compression;
 
 	private final int packIndexVersion;
 
-	private final LogRefUpdates logAllRefUpdates;
-
 	private final String excludesfile;
 
 	private final String attributesfile;
 
 	private final boolean commitGraph;
 
+	private final TrustStat trustStat;
+
+	private final TrustStat trustPackedRefsStat;
+
+	private final TrustStat trustLooseRefStat;
+
+	private final TrustStat trustPackStat;
+
+	private final TrustStat trustLooseObjectStat;
+
+	private final TrustStat trustTablesListStat;
+
 	/**
 	 * Options for symlink handling
 	 *
@@ -200,14 +246,17 @@ public enum HideDotFiles {
 		DOTGITONLY
 	}
 
-	private CoreConfig(Config rc) {
+	/**
+	 * Create a new core configuration from the passed configuration.
+	 *
+	 * @param rc
+	 *            git configuration
+	 */
+	CoreConfig(Config rc) {
 		compression = rc.getInt(ConfigConstants.CONFIG_CORE_SECTION,
 				ConfigConstants.CONFIG_KEY_COMPRESSION, DEFAULT_COMPRESSION);
 		packIndexVersion = rc.getInt(ConfigConstants.CONFIG_PACK_SECTION,
 				ConfigConstants.CONFIG_KEY_INDEXVERSION, 2);
-		logAllRefUpdates = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
-				ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES,
-				LogRefUpdates.TRUE);
 		excludesfile = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, null,
 				ConfigConstants.CONFIG_KEY_EXCLUDESFILE);
 		attributesfile = rc.getString(ConfigConstants.CONFIG_CORE_SECTION,
@@ -215,6 +264,68 @@ private CoreConfig(Config rc) {
 		commitGraph = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
 				ConfigConstants.CONFIG_COMMIT_GRAPH,
 				DEFAULT_COMMIT_GRAPH_ENABLE);
+
+		trustStat = parseTrustStat(rc);
+		trustPackedRefsStat = parseTrustPackedRefsStat(rc);
+		trustLooseRefStat = parseTrustLooseRefStat(rc);
+		trustPackStat = parseTrustPackFileStat(rc);
+		trustLooseObjectStat = parseTrustLooseObjectFileStat(rc);
+		trustTablesListStat = parseTablesListStat(rc);
+	}
+
+	private static TrustStat parseTrustStat(Config rc) {
+		Boolean tfs = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
+				ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT);
+		TrustStat ts = rc.getEnum(TrustStat.values(),
+				ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_TRUST_STAT);
+		if (tfs != null) {
+			if (ts == null) {
+				LOG.warn(JGitText.get().deprecatedTrustFolderStat);
+				return tfs.booleanValue() ? TrustStat.ALWAYS : TrustStat.NEVER;
+			}
+			LOG.warn(JGitText.get().precedenceTrustConfig);
+		}
+		if (ts == null) {
+			ts = TrustStat.ALWAYS;
+		} else if (ts == TrustStat.INHERIT) {
+			LOG.warn(JGitText.get().invalidTrustStat);
+			ts = TrustStat.ALWAYS;
+		}
+		return ts;
+	}
+
+	private TrustStat parseTrustPackedRefsStat(Config rc) {
+		return inheritParseTrustStat(rc,
+				ConfigConstants.CONFIG_KEY_TRUST_PACKED_REFS_STAT);
+	}
+
+	private TrustStat parseTrustLooseRefStat(Config rc) {
+		return inheritParseTrustStat(rc,
+				ConfigConstants.CONFIG_KEY_TRUST_LOOSE_REF_STAT);
+	}
+
+	private TrustStat parseTrustPackFileStat(Config rc) {
+		return inheritParseTrustStat(rc,
+				ConfigConstants.CONFIG_KEY_TRUST_PACK_STAT);
+	}
+
+	private TrustStat parseTrustLooseObjectFileStat(Config rc) {
+		return inheritParseTrustStat(rc,
+				ConfigConstants.CONFIG_KEY_TRUST_LOOSE_OBJECT_STAT);
+	}
+
+	private TrustStat inheritParseTrustStat(Config rc, String key) {
+		TrustStat t = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, key,
+				TrustStat.INHERIT);
+		return t == TrustStat.INHERIT ? trustStat : t;
+	}
+
+	private TrustStat parseTablesListStat(Config rc) {
+		TrustStat t = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_TRUST_TABLESLIST_STAT,
+				TrustStat.INHERIT);
+		return t == TrustStat.INHERIT ? trustStat : t;
 	}
 
 	/**
@@ -236,20 +347,6 @@ public int getPackIndexVersion() {
 	}
 
 	/**
-	 * Whether to log all refUpdates
-	 *
-	 * @return whether to log all refUpdates
-	 * @deprecated since 5.6; default value depends on whether the repository is
-	 *             bare. Use
-	 *             {@link Config#getEnum(String, String, String, Enum)}
-	 *             directly.
-	 */
-	@Deprecated
-	public boolean isLogAllRefUpdates() {
-		return !LogRefUpdates.FALSE.equals(logAllRefUpdates);
-	}
-
-	/**
 	 * Get path of excludesfile
 	 *
 	 * @return path of excludesfile
@@ -279,4 +376,70 @@ public String getAttributesFile() {
 	public boolean enableCommitGraph() {
 		return commitGraph;
 	}
+
+	/**
+	 * Get how far we can trust file attributes of packed-refs file which is
+	 * used to store {@link org.eclipse.jgit.lib.Ref}s in
+	 * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}.
+	 *
+	 * @return how far we can trust file attributes of packed-refs file.
+	 *
+	 * @since 7.2
+	 */
+	public TrustStat getTrustPackedRefsStat() {
+		return trustPackedRefsStat;
+	}
+
+	/**
+	 * Get how far we can trust file attributes of loose ref files which are
+	 * used to store {@link org.eclipse.jgit.lib.Ref}s in
+	 * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}.
+	 *
+	 * @return how far we can trust file attributes of loose ref files.
+	 *
+	 * @since 7.2
+	 */
+	public TrustStat getTrustLooseRefStat() {
+		return trustLooseRefStat;
+	}
+
+	/**
+	 * Get how far we can trust file attributes of packed-refs file which is
+	 * used to store {@link org.eclipse.jgit.lib.Ref}s in
+	 * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}.
+	 *
+	 * @return how far we can trust file attributes of packed-refs file.
+	 *
+	 * @since 7.2
+	 */
+	public TrustStat getTrustPackStat() {
+		return trustPackStat;
+	}
+
+	/**
+	 * Get how far we can trust file attributes of loose ref files which are
+	 * used to store {@link org.eclipse.jgit.lib.Ref}s in
+	 * {@link org.eclipse.jgit.internal.storage.file.RefDirectory}.
+	 *
+	 * @return how far we can trust file attributes of loose ref files.
+	 *
+	 * @since 7.2
+	 */
+	public TrustStat getTrustLooseObjectStat() {
+		return trustLooseObjectStat;
+	}
+
+	/**
+	 * Get how far we can trust file attributes of the "tables.list" file which
+	 * is used to store the list of filenames of the files storing
+	 * {@link org.eclipse.jgit.internal.storage.reftable.Reftable}s in
+	 * {@link org.eclipse.jgit.internal.storage.file.FileReftableDatabase}.
+	 *
+	 * @return how far we can trust file attributes of the "tables.list" file.
+	 *
+	 * @since 7.2
+	 */
+	public TrustStat getTrustTablesListStat() {
+		return trustTablesListStat;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
index a71549c..3059f28 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
@@ -18,6 +18,7 @@
 import java.util.regex.Pattern;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Config.ConfigEnum;
 import org.eclipse.jgit.transport.RefSpec;
@@ -31,27 +32,37 @@
  */
 public class DefaultTypedConfigGetter implements TypedConfigGetter {
 
+	@SuppressWarnings("boxed")
 	@Override
 	public boolean getBoolean(Config config, String section, String subsection,
 			String name, boolean defaultValue) {
+		return neverNull(getBoolean(config, section, subsection, name,
+				Boolean.valueOf(defaultValue)));
+	}
+
+	@Nullable
+	@Override
+	public Boolean getBoolean(Config config, String section, String subsection,
+			String name, @Nullable Boolean defaultValue) {
 		String n = config.getString(section, subsection, name);
 		if (n == null) {
 			return defaultValue;
 		}
 		if (Config.isMissing(n)) {
-			return true;
+			return Boolean.TRUE;
 		}
 		try {
-			return StringUtils.toBoolean(n);
+			return Boolean.valueOf(StringUtils.toBoolean(n));
 		} catch (IllegalArgumentException err) {
 			throw new IllegalArgumentException(MessageFormat.format(
 					JGitText.get().invalidBooleanValue, section, name, n), err);
 		}
 	}
 
+	@Nullable
 	@Override
 	public <T extends Enum<?>> T getEnum(Config config, T[] all, String section,
-			String subsection, String name, T defaultValue) {
+			String subsection, String name, @Nullable T defaultValue) {
 		String value = config.getString(section, subsection, name);
 		if (value == null) {
 			return defaultValue;
@@ -107,9 +118,27 @@ public <T extends Enum<?>> T getEnum(Config config, T[] all, String section,
 	@Override
 	public int getInt(Config config, String section, String subsection,
 			String name, int defaultValue) {
-		long val = config.getLong(section, subsection, name, defaultValue);
+		return neverNull(getInt(config, section, subsection, name,
+				Integer.valueOf(defaultValue)));
+	}
+
+	@Nullable
+	@Override
+	@SuppressWarnings("boxing")
+	public Integer getInt(Config config, String section, String subsection,
+			String name, @Nullable Integer defaultValue) {
+		Long longDefault = defaultValue != null
+				? Long.valueOf(defaultValue.longValue())
+				: null;
+		Long val = config.getLong(section, subsection, name);
+		if (val == null) {
+			val = longDefault;
+		}
+		if (val == null) {
+			return null;
+		}
 		if (Integer.MIN_VALUE <= val && val <= Integer.MAX_VALUE) {
-			return (int) val;
+			return Integer.valueOf(Math.toIntExact(val));
 		}
 		throw new IllegalArgumentException(MessageFormat
 				.format(JGitText.get().integerValueOutOfRange, section, name));
@@ -118,37 +147,56 @@ public int getInt(Config config, String section, String subsection,
 	@Override
 	public int getIntInRange(Config config, String section, String subsection,
 			String name, int minValue, int maxValue, int defaultValue) {
-		int val = getInt(config, section, subsection, name, defaultValue);
+		return neverNull(getIntInRange(config, section, subsection, name,
+				minValue, maxValue, Integer.valueOf(defaultValue)));
+	}
+
+	@Override
+	@SuppressWarnings("boxing")
+	public Integer getIntInRange(Config config, String section,
+			String subsection, String name, int minValue, int maxValue,
+			Integer defaultValue) {
+		Integer val = getInt(config, section, subsection, name, defaultValue);
+		if (val == null) {
+			return null;
+		}
 		if ((val >= minValue && val <= maxValue) || val == UNSET_INT) {
 			return val;
 		}
 		if (subsection == null) {
-			throw new IllegalArgumentException(MessageFormat.format(
-					JGitText.get().integerValueNotInRange, section, name,
-					Integer.valueOf(val), Integer.valueOf(minValue),
-					Integer.valueOf(maxValue)));
+			throw new IllegalArgumentException(
+					MessageFormat.format(JGitText.get().integerValueNotInRange,
+							section, name, val, minValue, maxValue));
 		}
 		throw new IllegalArgumentException(MessageFormat.format(
 				JGitText.get().integerValueNotInRangeSubSection, section,
-				subsection, name, Integer.valueOf(val),
-				Integer.valueOf(minValue), Integer.valueOf(maxValue)));
+				subsection, name, val, minValue, maxValue));
 	}
 
 	@Override
 	public long getLong(Config config, String section, String subsection,
 			String name, long defaultValue) {
-		final String str = config.getString(section, subsection, name);
+		return neverNull(getLong(config, section, subsection, name,
+				Long.valueOf(defaultValue)));
+	}
+
+	@Nullable
+	@Override
+	public Long getLong(Config config, String section, String subsection,
+			String name, @Nullable Long defaultValue) {
+		String str = config.getString(section, subsection, name);
 		if (str == null) {
 			return defaultValue;
 		}
 		try {
-			return StringUtils.parseLongWithSuffix(str, false);
+			return Long.valueOf(StringUtils.parseLongWithSuffix(str, false));
 		} catch (StringIndexOutOfBoundsException e) {
 			// Empty
 			return defaultValue;
 		} catch (NumberFormatException nfe) {
-			throw new IllegalArgumentException(MessageFormat.format(
-					JGitText.get().invalidIntegerValue, section, name, str),
+			throw new IllegalArgumentException(
+					MessageFormat.format(JGitText.get().invalidIntegerValue,
+							section, name, str),
 					nfe);
 		}
 	}
@@ -156,6 +204,13 @@ public long getLong(Config config, String section, String subsection,
 	@Override
 	public long getTimeUnit(Config config, String section, String subsection,
 			String name, long defaultValue, TimeUnit wantUnit) {
+		return neverNull(getTimeUnit(config, section, subsection, name,
+				Long.valueOf(defaultValue), wantUnit));
+	}
+
+	@Override
+	public Long getTimeUnit(Config config, String section, String subsection,
+			String name, @Nullable Long defaultValue, TimeUnit wantUnit) {
 		String valueString = config.getString(section, subsection, name);
 
 		if (valueString == null) {
@@ -232,8 +287,8 @@ public long getTimeUnit(Config config, String section, String subsection,
 		}
 
 		try {
-			return wantUnit.convert(Long.parseLong(digits) * inputMul,
-					inputUnit);
+			return Long.valueOf(wantUnit
+					.convert(Long.parseLong(digits) * inputMul, inputUnit));
 		} catch (NumberFormatException nfe) {
 			IllegalArgumentException iae = notTimeUnit(section, subsection,
 					unitName, valueString);
@@ -274,4 +329,14 @@ public List<RefSpec> getRefSpecs(Config config, String section,
 		}
 		return result;
 	}
+
+	// Trick for the checkers. When we use this, one is never null, but
+	// they don't know.
+	@NonNull
+	private static <T> T neverNull(T one) {
+		if (one == null) {
+			throw new IllegalArgumentException();
+		}
+		return one;
+	}
 }
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 427a235..23d16db 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
@@ -24,7 +24,13 @@ public enum GpgFormat implements Config.ConfigEnum {
 		/** Value for openpgp */
 		OPENPGP("openpgp"), //$NON-NLS-1$
 		/** Value for x509 */
-		X509("x509"); //$NON-NLS-1$
+		X509("x509"), //$NON-NLS-1$
+		/**
+		 * Value for ssh.
+		 *
+		 * @since 7.0
+		 */
+		SSH("ssh"); //$NON-NLS-1$
 
 		private final String configValue;
 
@@ -55,26 +61,11 @@ public String toConfigValue() {
 
 	private final boolean forceAnnotated;
 
-	/**
-	 * 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;
-	}
+	private final String sshDefaultKeyCommand;
+
+	private final String sshAllowedSignersFile;
+
+	private final String sshRevocationFile;
 
 	/**
 	 * Create a new GPG config that reads the configuration from config.
@@ -83,18 +74,18 @@ public GpgConfig(String keySpec, GpgFormat format, String gpgProgram) {
 	 *            the config to read from
 	 */
 	public GpgConfig(Config config) {
-		keyFormat = config.getEnum(GpgFormat.values(),
-				ConfigConstants.CONFIG_GPG_SECTION, null,
+		keyFormat = config.getEnum(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) {
+		if (exe == null && GpgFormat.OPENPGP.equals(keyFormat)) {
 			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);
@@ -102,6 +93,17 @@ public GpgConfig(Config config) {
 				ConfigConstants.CONFIG_KEY_GPGSIGN, false);
 		forceAnnotated = config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION,
 				ConfigConstants.CONFIG_KEY_FORCE_SIGN_ANNOTATED, false);
+		sshDefaultKeyCommand = config.getString(
+				ConfigConstants.CONFIG_GPG_SECTION,
+				ConfigConstants.CONFIG_SSH_SUBSECTION,
+				ConfigConstants.CONFIG_KEY_SSH_DEFAULT_KEY_COMMAND);
+		sshAllowedSignersFile = config.getString(
+				ConfigConstants.CONFIG_GPG_SECTION,
+				ConfigConstants.CONFIG_SSH_SUBSECTION,
+				ConfigConstants.CONFIG_KEY_SSH_ALLOWED_SIGNERS_FILE);
+		sshRevocationFile = config.getString(ConfigConstants.CONFIG_GPG_SECTION,
+				ConfigConstants.CONFIG_SSH_SUBSECTION,
+				ConfigConstants.CONFIG_KEY_SSH_REVOCATION_FILE);
 	}
 
 	/**
@@ -165,4 +167,37 @@ public boolean isSignAllTags() {
 	public boolean isSignAnnotated() {
 		return forceAnnotated;
 	}
+
+	/**
+	 * Retrieves the value of git config {@code gpg.ssh.defaultKeyCommand}.
+	 *
+	 * @return the value of {@code gpg.ssh.defaultKeyCommand}
+	 *
+	 * @since 7.1
+	 */
+	public String getSshDefaultKeyCommand() {
+		return sshDefaultKeyCommand;
+	}
+
+	/**
+	 * Retrieves the value of git config {@code gpg.ssh.allowedSignersFile}.
+	 *
+	 * @return the value of {@code gpg.ssh.allowedSignersFile}
+	 *
+	 * @since 7.1
+	 */
+	public String getSshAllowedSignersFile() {
+		return sshAllowedSignersFile;
+	}
+
+	/**
+	 * Retrieves the value of git config {@code gpg.ssh.revocationFile}.
+	 *
+	 * @return the value of {@code gpg.ssh.revocationFile}
+	 *
+	 * @since 7.1
+	 */
+	public String getSshRevocationFile() {
+		return sshRevocationFile;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java
deleted file mode 100644
index 074f465..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 91c9bab..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifier.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.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.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 GpgSignatureVerifier} 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 config
-	 *            the {@link GpgConfig}
-	 * @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
-	 * @since 6.9
-	 */
-	default SignatureVerification verify(@NonNull GpgConfig config, byte[] data,
-			byte[] signatureData) throws IOException {
-		// Default implementation for backwards compatibility; override as
-		// appropriate
-		return verify(data, signatureData);
-	}
-
-	/**
-	 * 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
-	 * @deprecated since 6.9, use {@link #verify(GpgConfig, byte[], byte[])}
-	 *             instead
-	 */
-	@Deprecated
-	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
deleted file mode 100644
index 59775c4..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2021, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * 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 class DefaultFactory {
-
-		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;
-		}
-
-		private DefaultFactory() {
-			// No instantiation
-		}
-
-		public static GpgSignatureVerifierFactory getDefault() {
-			return defaultFactory;
-		}
-
-		/**
-		 * Sets the default factory.
-		 *
-		 * @param factory
-		 *            the new default factory
-		 */
-		public static void setDefault(GpgSignatureVerifierFactory factory) {
-			defaultFactory = factory;
-		}
-	}
-
-	/**
-	 * Retrieves the default factory.
-	 *
-	 * @return the default factory or {@code null} if none set
-	 */
-	public static GpgSignatureVerifierFactory getDefault() {
-		return DefaultFactory.getDefault();
-	}
-
-	/**
-	 * Sets the default factory.
-	 *
-	 * @param factory
-	 *            the new default factory
-	 */
-	public static void setDefault(GpgSignatureVerifierFactory factory) {
-		DefaultFactory.setDefault(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/GpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
deleted file mode 100644
index b25a61b..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2018, 2022 Salesforce and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * 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.eclipse.jgit.annotations.NonNull;
-import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.api.errors.CanceledException;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Creates GPG signatures for Git objects.
- *
- * @since 5.3
- */
-public abstract class GpgSigner {
-
-	private static final Logger LOG = LoggerFactory.getLogger(GpgSigner.class);
-
-	private static class DefaultSigner {
-
-		private static volatile GpgSigner defaultSigner = loadGpgSigner();
-
-		private static GpgSigner loadGpgSigner() {
-			try {
-				ServiceLoader<GpgSigner> loader = ServiceLoader
-						.load(GpgSigner.class);
-				Iterator<GpgSigner> iter = loader.iterator();
-				if (iter.hasNext()) {
-					return iter.next();
-				}
-			} catch (ServiceConfigurationError e) {
-				LOG.error(e.getMessage(), e);
-			}
-			return null;
-		}
-
-		private DefaultSigner() {
-			// No instantiation
-		}
-
-		public static GpgSigner getDefault() {
-			return defaultSigner;
-		}
-
-		public static void setDefault(GpgSigner signer) {
-			defaultSigner = signer;
-		}
-	}
-
-	/**
-	 * Get the default signer, or <code>null</code>.
-	 *
-	 * @return the default signer, or <code>null</code>.
-	 */
-	public static GpgSigner getDefault() {
-		return DefaultSigner.getDefault();
-	}
-
-	/**
-	 * Set the default signer.
-	 *
-	 * @param signer
-	 *            the new default signer, may be <code>null</code> to select no
-	 *            default.
-	 */
-	public static void setDefault(GpgSigner signer) {
-		DefaultSigner.setDefault(signer);
-	}
-
-	/**
-	 * Signs the specified commit.
-	 *
-	 * <p>
-	 * Implementors should obtain the payload for signing from the specified
-	 * commit via {@link CommitBuilder#build()} and create a proper
-	 * {@link GpgSignature}. The generated signature must be set on the
-	 * specified {@code commit} (see
-	 * {@link CommitBuilder#setGpgSignature(GpgSignature)}).
-	 * </p>
-	 * <p>
-	 * Any existing signature on the commit must be discarded prior obtaining
-	 * the payload via {@link CommitBuilder#build()}.
-	 * </p>
-	 *
-	 * @param commit
-	 *            the commit to sign (must not be <code>null</code> 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)
-	 * @throws CanceledException
-	 *             when signing was canceled (eg., user aborted when entering
-	 *             passphrase)
-	 */
-	public abstract void sign(@NonNull CommitBuilder commit,
-			@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
-			CredentialsProvider credentialsProvider) throws CanceledException;
-
-	/**
-	 * 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)
-	 * @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)
-	 */
-	public abstract boolean canLocateSigningKey(@Nullable String gpgSigningKey,
-			@NonNull PersonIdent committer,
-			CredentialsProvider credentialsProvider) throws CanceledException;
-
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
index 8e965c5..a99c647 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
@@ -639,7 +639,7 @@ public boolean diff(ProgressMonitor monitor, int estWorkTreeSize,
 							// submodule repository in .git/modules doesn't
 							// exist yet it isn't "missing".
 							File gitDir = new File(
-									new File(repository.getDirectory(),
+									new File(repository.getCommonDirectory(),
 											Constants.MODULES),
 									subRepoPath);
 							if (!gitDir.isDirectory()) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java
index 1c31263..1b455b9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java
@@ -15,6 +15,7 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.nio.ByteBuffer;
 
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
@@ -152,6 +153,22 @@ public static final ObjectId fromRaw(byte[] bs, int p) {
 	}
 
 	/**
+	 * Convert an ObjectId from raw binary representation
+	 *
+	 * @param bb
+	 *            a bytebuffer with the objectid encoded as 5 consecutive ints.
+	 *            This is the reverse of {@link ObjectId#copyRawTo(ByteBuffer)}
+	 *
+	 * @return the converted object id.
+	 *
+	 * @since 7.0
+	 */
+	public static final ObjectId fromRaw(ByteBuffer bb) {
+		return new ObjectId(bb.getInt(), bb.getInt(), bb.getInt(), bb.getInt(),
+				bb.getInt());
+	}
+
+	/**
 	 * Convert an ObjectId from raw binary representation.
 	 *
 	 * @param is
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
index 3ba055a..50f4a83 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
@@ -12,10 +12,15 @@
 
 package org.eclipse.jgit.lib;
 
+import static java.time.ZoneOffset.UTC;
+
 import java.io.Serializable;
-import java.text.SimpleDateFormat;
+import java.time.DateTimeException;
 import java.time.Instant;
 import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
 import java.util.Date;
 import java.util.Locale;
 import java.util.TimeZone;
@@ -40,7 +45,9 @@ public class PersonIdent implements Serializable {
 	 *            timezone offset as in {@link #getTimeZoneOffset()}.
 	 * @return time zone object for the given offset.
 	 * @since 4.1
+	 * @deprecated use {@link #getZoneId(int)} instead
 	 */
+	@Deprecated(since = "7.2")
 	public static TimeZone getTimeZone(int tzOffset) {
 		StringBuilder tzId = new StringBuilder(8);
 		tzId.append("GMT"); //$NON-NLS-1$
@@ -49,6 +56,21 @@ public static TimeZone getTimeZone(int tzOffset) {
 	}
 
 	/**
+	 * Translate a minutes offset into a ZoneId
+	 *
+	 * @param tzOffset as minutes east of UTC
+	 * @return a ZoneId for this offset (UTC if invalid)
+	 * @since 7.1
+	 */
+	public static ZoneId getZoneId(int tzOffset) {
+		try {
+			return ZoneOffset.ofHoursMinutes(tzOffset / 60, tzOffset % 60);
+		} catch (DateTimeException e) {
+			return UTC;
+		}
+	}
+
+	/**
 	 * Format a timezone offset.
 	 *
 	 * @param r
@@ -121,13 +143,17 @@ public static void appendSanitized(StringBuilder r, String str) {
 		}
 	}
 
+	// Write offsets as [+-]HHMM
+	private static final DateTimeFormatter OFFSET_FORMATTER = DateTimeFormatter
+			.ofPattern("Z", Locale.US); //$NON-NLS-1$
+
 	private final String name;
 
 	private final String emailAddress;
 
-	private final long when;
+	private final Instant when;
 
-	private final int tzOffset;
+	private final ZoneId tzOffset;
 
 	/**
 	 * Creates new PersonIdent from config info in repository, with current time.
@@ -160,7 +186,7 @@ public PersonIdent(PersonIdent pi) {
 	 *            a {@link java.lang.String} object.
 	 */
 	public PersonIdent(String aName, String aEmailAddress) {
-		this(aName, aEmailAddress, SystemReader.getInstance().getCurrentTime());
+		this(aName, aEmailAddress, SystemReader.getInstance().now());
 	}
 
 	/**
@@ -177,7 +203,7 @@ public PersonIdent(String aName, String aEmailAddress) {
 	 */
 	public PersonIdent(String aName, String aEmailAddress,
 			ProposedTimestamp when) {
-		this(aName, aEmailAddress, when.millis());
+		this(aName, aEmailAddress, when.instant());
 	}
 
 	/**
@@ -189,8 +215,25 @@ public PersonIdent(String aName, String aEmailAddress,
 	 *            local time
 	 * @param tz
 	 *            time zone
+	 * @deprecated Use {@link #PersonIdent(PersonIdent, Instant, ZoneId)} instead.
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(PersonIdent pi, Date when, TimeZone tz) {
+		this(pi.getName(), pi.getEmailAddress(), when.toInstant(), tz.toZoneId());
+	}
+
+	/**
+	 * Copy a PersonIdent, but alter the clone's time stamp
+	 *
+	 * @param pi
+	 *            original {@link org.eclipse.jgit.lib.PersonIdent}
+	 * @param when
+	 *            local time
+	 * @param tz
+	 *            time zone offset
+	 * @since 7.1
+	 */
+	public PersonIdent(PersonIdent pi, Instant when, ZoneId tz) {
 		this(pi.getName(), pi.getEmailAddress(), when, tz);
 	}
 
@@ -202,9 +245,12 @@ public PersonIdent(PersonIdent pi, Date when, TimeZone tz) {
 	 *            original {@link org.eclipse.jgit.lib.PersonIdent}
 	 * @param aWhen
 	 *            local time
+	 * @deprecated Use the variant with an Instant instead
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(PersonIdent pi, Date aWhen) {
-		this(pi.getName(), pi.getEmailAddress(), aWhen.getTime(), pi.tzOffset);
+		this(pi.getName(), pi.getEmailAddress(), aWhen.toInstant(),
+				pi.tzOffset);
 	}
 
 	/**
@@ -218,7 +264,7 @@ public PersonIdent(PersonIdent pi, Date aWhen) {
 	 * @since 6.1
 	 */
 	public PersonIdent(PersonIdent pi, Instant aWhen) {
-		this(pi.getName(), pi.getEmailAddress(), aWhen.toEpochMilli(), pi.tzOffset);
+		this(pi.getName(), pi.getEmailAddress(), aWhen, pi.tzOffset);
 	}
 
 	/**
@@ -230,11 +276,12 @@ public PersonIdent(PersonIdent pi, Instant aWhen) {
 	 *            local time stamp
 	 * @param aTZ
 	 *            time zone
+	 * @deprecated Use the variant with Instant and ZoneId instead
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(final String aName, final String aEmailAddress,
 			final Date aWhen, final TimeZone aTZ) {
-		this(aName, aEmailAddress, aWhen.getTime(), aTZ.getOffset(aWhen
-				.getTime()) / (60 * 1000));
+		this(aName, aEmailAddress, aWhen.toInstant(), aTZ.toZoneId());
 	}
 
 	/**
@@ -252,10 +299,16 @@ public PersonIdent(final String aName, final String aEmailAddress,
 	 */
 	public PersonIdent(final String aName, String aEmailAddress, Instant aWhen,
 			ZoneId zoneId) {
-		this(aName, aEmailAddress, aWhen.toEpochMilli(),
-				TimeZone.getTimeZone(zoneId)
-						.getOffset(aWhen
-				.toEpochMilli()) / (60 * 1000));
+		if (aName == null)
+			throw new IllegalArgumentException(
+					JGitText.get().personIdentNameNonNull);
+		if (aEmailAddress == null)
+			throw new IllegalArgumentException(
+					JGitText.get().personIdentEmailNonNull);
+		name = aName;
+		emailAddress = aEmailAddress;
+		when = aWhen;
+		tzOffset = zoneId;
 	}
 
 	/**
@@ -267,15 +320,18 @@ public PersonIdent(final String aName, String aEmailAddress, Instant aWhen,
 	 *            local time stamp
 	 * @param aTZ
 	 *            time zone
+	 * @deprecated Use the variant with Instant and ZoneId instead
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(PersonIdent pi, long aWhen, int aTZ) {
-		this(pi.getName(), pi.getEmailAddress(), aWhen, aTZ);
+		this(pi.getName(), pi.getEmailAddress(), Instant.ofEpochMilli(aWhen),
+				getZoneId(aTZ));
 	}
 
 	private PersonIdent(final String aName, final String aEmailAddress,
-			long when) {
+			Instant when) {
 		this(aName, aEmailAddress, when, SystemReader.getInstance()
-				.getTimezone(when));
+				.getTimeZoneAt(when));
 	}
 
 	private PersonIdent(UserConfig config) {
@@ -298,19 +354,12 @@ private PersonIdent(UserConfig config) {
 	 *            local time stamp
 	 * @param aTZ
 	 *            time zone
+	 * @deprecated Use  the variant with Instant and ZoneId instead
 	 */
+	@Deprecated(since = "7.1")
 	public PersonIdent(final String aName, final String aEmailAddress,
 			final long aWhen, final int aTZ) {
-		if (aName == null)
-			throw new IllegalArgumentException(
-					JGitText.get().personIdentNameNonNull);
-		if (aEmailAddress == null)
-			throw new IllegalArgumentException(
-					JGitText.get().personIdentEmailNonNull);
-		name = aName;
-		emailAddress = aEmailAddress;
-		when = aWhen;
-		tzOffset = aTZ;
+		this(aName, aEmailAddress, Instant.ofEpochMilli(aWhen), getZoneId(aTZ));
 	}
 
 	/**
@@ -335,9 +384,12 @@ public String getEmailAddress() {
 	 * Get timestamp
 	 *
 	 * @return timestamp
+	 *
+	 * @deprecated Use getWhenAsInstant instead
 	 */
+	@Deprecated(since = "7.1")
 	public Date getWhen() {
-		return new Date(when);
+		return Date.from(when);
 	}
 
 	/**
@@ -347,16 +399,19 @@ public Date getWhen() {
 	 * @since 6.1
 	 */
 	public Instant getWhenAsInstant() {
-		return Instant.ofEpochMilli(when);
+		return when;
 	}
 
 	/**
 	 * Get this person's declared time zone
 	 *
 	 * @return this person's declared time zone; null if time zone is unknown.
+	 *
+	 * @deprecated Use getZoneId instead
 	 */
+	@Deprecated(since = "7.1")
 	public TimeZone getTimeZone() {
-		return getTimeZone(tzOffset);
+		return TimeZone.getTimeZone(tzOffset);
 	}
 
 	/**
@@ -366,7 +421,17 @@ public TimeZone getTimeZone() {
 	 * @since 6.1
 	 */
 	public ZoneId getZoneId() {
-		return getTimeZone().toZoneId();
+		return tzOffset;
+	}
+
+	/**
+	 * Return the offset in this timezone at the specific time
+	 *
+	 * @return the offset
+	 * @since 7.1
+	 */
+	public ZoneOffset getZoneOffset() {
+		return tzOffset.getRules().getOffset(when);
 	}
 
 	/**
@@ -374,9 +439,11 @@ public ZoneId getZoneId() {
 	 *
 	 * @return this person's declared time zone as minutes east of UTC. If the
 	 *         timezone is to the west of UTC it is negative.
+	 * @deprecated Use {@link #getZoneOffset()} and read minutes from there
 	 */
+	@Deprecated(since = "7.1")
 	public int getTimeZoneOffset() {
-		return tzOffset;
+		return getZoneOffset().getTotalSeconds() / 60;
 	}
 
 	/**
@@ -388,7 +455,7 @@ public int getTimeZoneOffset() {
 	public int hashCode() {
 		int hc = getEmailAddress().hashCode();
 		hc *= 31;
-		hc += (int) (when / 1000L);
+		hc += when.hashCode();
 		return hc;
 	}
 
@@ -398,7 +465,9 @@ public boolean equals(Object o) {
 			final PersonIdent p = (PersonIdent) o;
 			return getName().equals(p.getName())
 					&& getEmailAddress().equals(p.getEmailAddress())
-					&& when / 1000L == p.when / 1000L;
+					// commmit timestamps are stored with 1 second precision
+					&& when.truncatedTo(ChronoUnit.SECONDS)
+							.equals(p.when.truncatedTo(ChronoUnit.SECONDS));
 		}
 		return false;
 	}
@@ -414,9 +483,9 @@ public String toExternalString() {
 		r.append(" <"); //$NON-NLS-1$
 		appendSanitized(r, getEmailAddress());
 		r.append("> "); //$NON-NLS-1$
-		r.append(when / 1000);
+		r.append(when.toEpochMilli() / 1000);
 		r.append(' ');
-		appendTimezone(r, tzOffset);
+		r.append(OFFSET_FORMATTER.format(getZoneOffset()));
 		return r.toString();
 	}
 
@@ -424,19 +493,16 @@ public String toExternalString() {
 	@SuppressWarnings("nls")
 	public String toString() {
 		final StringBuilder r = new StringBuilder();
-		final SimpleDateFormat dtfmt;
-		dtfmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US);
-		dtfmt.setTimeZone(getTimeZone());
-
+		DateTimeFormatter dtfmt = DateTimeFormatter
+				.ofPattern("EEE MMM d HH:mm:ss yyyy Z", Locale.US) //$NON-NLS-1$
+				.withZone(tzOffset);
 		r.append("PersonIdent[");
 		r.append(getName());
 		r.append(", ");
 		r.append(getEmailAddress());
 		r.append(", ");
-		r.append(dtfmt.format(Long.valueOf(when)));
+		r.append(dtfmt.format(when));
 		r.append("]");
-
 		return r.toString();
 	}
 }
-
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 9e05a39..49d5224 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
@@ -26,6 +26,7 @@
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.PackRefsCommand;
 
 /**
  * Abstraction of name to {@link org.eclipse.jgit.lib.ObjectId} mapping.
@@ -160,7 +161,7 @@ public Collection<String> getConflictingNames(String name)
 			if (existing.startsWith(prefix))
 				conflicting.add(existing);
 
-		return conflicting;
+		return Collections.unmodifiableList(conflicting);
 	}
 
 	/**
@@ -238,23 +239,6 @@ public boolean performsAtomicTransactions() {
 	}
 
 	/**
-	 * Compatibility synonym for {@link #findRef(String)}.
-	 *
-	 * @param name
-	 *            the name of the reference. May be a short name which must be
-	 *            searched for using the standard {@link #SEARCH_PATH}.
-	 * @return the reference (if it exists); else {@code null}.
-	 * @throws IOException
-	 *             the reference space cannot be accessed.
-	 * @deprecated Use {@link #findRef(String)} instead.
-	 */
-	@Deprecated
-	@Nullable
-	public final Ref getRef(String name) throws IOException {
-		return findRef(name);
-	}
-
-	/**
 	 * Read a single reference.
 	 * <p>
 	 * Aside from taking advantage of {@link #SEARCH_PATH}, this method may be
@@ -372,6 +356,40 @@ public List<Ref> getRefs() throws IOException {
 	}
 
 	/**
+	 * Get the reflog reader
+	 *
+	 * @param refName
+	 *            a {@link java.lang.String} object.
+	 * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied
+	 *         refname, or {@code null} if the named ref does not exist.
+	 * @throws java.io.IOException
+	 *             the ref could not be accessed.
+	 * @since 7.2
+	 */
+	@Nullable
+	public ReflogReader getReflogReader(String refName) throws IOException {
+		Ref ref = exactRef(refName);
+		if (ref == null) {
+			return null;
+		}
+		return getReflogReader(ref);
+	}
+
+	/**
+	 * Get the reflog reader.
+	 *
+	 * @param ref
+	 *            a Ref
+	 * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied ref.
+	 * @throws IOException
+	 *             if an IO error occurred
+	 * @since 7.2
+	 */
+	@NonNull
+	public abstract ReflogReader getReflogReader(@NonNull Ref ref)
+			throws IOException;
+
+	/**
 	 * Get a section of the reference namespace.
 	 *
 	 * @param prefix
@@ -610,4 +628,22 @@ public static Ref findRef(Map<String, Ref> map, String name) {
 		}
 		return null;
 	}
+
+	/**
+	 * Optimize pack ref storage.
+	 *
+	 * @param pm
+	 *            a progress monitor
+	 *
+	 * @param packRefs
+	 *            {@link PackRefsCommand} to control ref packing behavior
+	 *
+	 * @throws java.io.IOException
+	 *             if an IO error occurred
+	 * @since 7.1
+	 */
+	public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs)
+			throws IOException {
+		// nothing
+	}
 }
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 4722e29..c9dc6da 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -26,6 +26,8 @@
 import java.io.OutputStream;
 import java.io.UncheckedIOException;
 import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -33,10 +35,12 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Pattern;
 
 import org.eclipse.jgit.annotations.NonNull;
@@ -113,9 +117,12 @@ public static ListenerList getGlobalListenerList() {
 
 	final AtomicLong closedAt = new AtomicLong();
 
-	/** Metadata directory holding the repository's critical files. */
+	/** $GIT_DIR: metadata directory holding the repository's critical files. */
 	private final File gitDir;
 
+	/** $GIT_COMMON_DIR: metadata directory holding the common repository's critical files.  */
+	private final File gitCommonDir;
+
 	/** File abstraction used to resolve paths. */
 	private final FS fs;
 
@@ -129,6 +136,8 @@ public static ListenerList getGlobalListenerList() {
 
 	private final String initialBranch;
 
+	private final AtomicReference<Boolean> caseInsensitiveWorktree = new AtomicReference<>();
+
 	/**
 	 * Initialize a new repository instance.
 	 *
@@ -137,6 +146,7 @@ public static ListenerList getGlobalListenerList() {
 	 */
 	protected Repository(BaseRepositoryBuilder options) {
 		gitDir = options.getGitDir();
+		gitCommonDir = options.getGitCommonDir();
 		fs = options.getFS();
 		workTree = options.getWorkTree();
 		indexFile = options.getIndexFile();
@@ -220,6 +230,16 @@ public File getDirectory() {
 	public abstract String getIdentifier();
 
 	/**
+	 * Get common dir.
+	 *
+	 * @return $GIT_COMMON_DIR: local common metadata directory;
+	 * @since 7.0
+	 */
+	public File getCommonDirectory() {
+		return gitCommonDir;
+	}
+
+	/**
 	 * Get the object database which stores this repository's data.
 	 *
 	 * @return the object database which stores this repository's data.
@@ -293,25 +313,6 @@ public FS getFS() {
 	}
 
 	/**
-	 * Whether the specified object is stored in this repo or any of the known
-	 * shared repositories.
-	 *
-	 * @param objectId
-	 *            a {@link org.eclipse.jgit.lib.AnyObjectId} object.
-	 * @return true if the specified object is stored in this repo or any of the
-	 *         known shared repositories.
-	 * @deprecated use {@code getObjectDatabase().has(objectId)}
-	 */
-	@Deprecated
-	public boolean hasObject(AnyObjectId objectId) {
-		try {
-			return getObjectDatabase().has(objectId);
-		} catch (IOException e) {
-			throw new UncheckedIOException(e);
-		}
-	}
-
-	/**
 	 * Open an object from this repository.
 	 * <p>
 	 * This is a one-shot call interface which may be faster than allocating a
@@ -1150,11 +1151,9 @@ public Map<String, Ref> getTags() {
 	 *         new Ref object representing the same data as Ref, but isPeeled()
 	 *         will be true and getPeeledObjectId will contain the peeled object
 	 *         (or null).
-	 * @deprecated use {@code getRefDatabase().peel(ref)} instead.
 	 */
-	@Deprecated
 	@NonNull
-	public Ref peel(Ref ref) {
+	private Ref peel(Ref ref) {
 		try {
 			return getRefDatabase().peel(ref);
 		} catch (IOException e) {
@@ -1584,6 +1583,40 @@ public File getWorkTree() throws NoWorkTreeException {
 	}
 
 	/**
+	 * Tells whether the work tree is on a case-insensitive file system.
+	 *
+	 * @return {@code true} if the work tree is case-insensitive; {@code false}
+	 *         otherwise
+	 * @throws NoWorkTreeException
+	 *             if the repository is bare
+	 * @since 7.2
+	 */
+	public boolean isWorkTreeCaseInsensitive() throws NoWorkTreeException {
+		Boolean flag = caseInsensitiveWorktree.get();
+		if (flag == null) {
+			File directory = getWorkTree();
+			// See if we can find ".git" also as ".GIT".
+			File dotGit = new File(directory, Constants.DOT_GIT);
+			if (Files.exists(dotGit.toPath(), LinkOption.NOFOLLOW_LINKS)) {
+				dotGit = new File(directory,
+						Constants.DOT_GIT.toUpperCase(Locale.ROOT));
+				flag = Boolean.valueOf(Files.exists(dotGit.toPath(),
+						LinkOption.NOFOLLOW_LINKS));
+			} else {
+				// Fall back to a mostly sane default. On Mac, HFS+ and APFS
+				// partitions are case-insensitive by default but can be
+				// configured to be case-sensitive.
+				SystemReader system = SystemReader.getInstance();
+				flag = Boolean.valueOf(system.isWindows() || system.isMacOS());
+			}
+			if (!caseInsensitiveWorktree.compareAndSet(null, flag)) {
+				flag = caseInsensitiveWorktree.get();
+			}
+		}
+		return flag.booleanValue();
+	}
+
+	/**
 	 * Force a scan for changed refs. Fires an IndexChangedEvent(false) if
 	 * changes are detected.
 	 *
@@ -1699,10 +1732,13 @@ public void setGitwebDescription(@Nullable String description)
 	 * @throws java.io.IOException
 	 *             the ref could not be accessed.
 	 * @since 3.0
+	 * @deprecated use {@code #getRefDatabase().getReflogReader(String)} instead
 	 */
+	@Deprecated(since = "7.2")
 	@Nullable
-	public abstract ReflogReader getReflogReader(String refName)
-			throws IOException;
+	public ReflogReader getReflogReader(String refName) throws IOException {
+		return getRefDatabase().getReflogReader(refName);
+	}
 
 	/**
 	 * Get the reflog reader. Subclasses should override this method and provide
@@ -1710,15 +1746,17 @@ public abstract ReflogReader getReflogReader(String refName)
 	 *
 	 * @param ref
 	 *            a Ref
-	 * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied ref,
-	 *         or {@code null} if the ref does not exist.
+	 * @return a {@link org.eclipse.jgit.lib.ReflogReader} for the supplied ref.
 	 * @throws IOException
 	 *             if an IO error occurred
 	 * @since 5.13.2
+	 * @deprecated use {@code #getRefDatabase().getReflogReader(Ref)} instead
 	 */
-	public @Nullable ReflogReader getReflogReader(@NonNull	Ref ref)
+	@Deprecated(since = "7.2")
+	@NonNull
+	public ReflogReader getReflogReader(@NonNull Ref ref)
 			throws IOException {
-		return getReflogReader(ref.getName());
+		return getRefDatabase().getReflogReader(ref);
 	}
 
 	/**
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 6288447..1836654 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
@@ -450,10 +450,21 @@ public String toString() {
 		 *         Git directory.
 		 */
 		public static boolean isGitRepository(File dir, FS fs) {
-			return fs.resolve(dir, Constants.OBJECTS).exists()
-					&& fs.resolve(dir, "refs").exists() //$NON-NLS-1$
-					&& (fs.resolve(dir, Constants.REFTABLE).exists()
-							|| isValidHead(new File(dir, Constants.HEAD)));
+			// check if common-dir available or fallback to git-dir
+			File commonDir;
+			try {
+				commonDir = fs.getCommonDir(dir);
+			} catch (IOException e) {
+				commonDir = null;
+			}
+			if (commonDir == null) {
+				commonDir = dir;
+			}
+			return fs.resolve(commonDir, Constants.OBJECTS).exists()
+					&& fs.resolve(commonDir, "refs").exists() //$NON-NLS-1$
+					&& (fs.resolve(commonDir, Constants.REFTABLE).exists()
+							|| isValidHead(
+									new File(commonDir, Constants.HEAD)));
 		}
 
 		private static boolean isValidHead(File head) {
@@ -496,15 +507,31 @@ private static String readFirstLine(File head) {
 		 *         null if there is no suitable match.
 		 */
 		public static File resolve(File directory, FS fs) {
-			if (isGitRepository(directory, fs))
+			// the folder itself
+			if (isGitRepository(directory, fs)) {
 				return directory;
-			if (isGitRepository(new File(directory, Constants.DOT_GIT), fs))
-				return new File(directory, Constants.DOT_GIT);
-
-			final String name = directory.getName();
-			final File parent = directory.getParentFile();
-			if (isGitRepository(new File(parent, name + Constants.DOT_GIT_EXT), fs))
-				return new File(parent, name + Constants.DOT_GIT_EXT);
+			}
+			// the .git subfolder or file (reference)
+			File dotDir = new File(directory, Constants.DOT_GIT);
+			if (dotDir.isFile()) {
+				try {
+					File refDir = BaseRepositoryBuilder.getSymRef(directory,
+							dotDir, fs);
+					if (refDir != null && isGitRepository(refDir, fs)) {
+						return refDir;
+					}
+				} catch (IOException ignored) {
+					// Continue searching if gitdir ref isn't found
+				}
+			} else if (isGitRepository(dotDir, fs)) {
+				return dotDir;
+			}
+			// the folder extended with .git (bare)
+			File bareDir = new File(directory.getParentFile(),
+					directory.getName() + Constants.DOT_GIT_EXT);
+			if (isGitRepository(bareDir, fs)) {
+				return bareDir;
+			}
 			return null;
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifier.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifier.java
new file mode 100644
index 0000000..2ce2708
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifier.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.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.lib;
+
+import java.io.IOException;
+import java.util.Date;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+
+/**
+ * A {@code SignatureVerifier} can verify signatures on git commits and tags.
+ *
+ * @since 7.0
+ */
+public interface SignatureVerifier {
+
+	/**
+	 * Verifies a given signature for given data.
+	 *
+	 * @param repository
+	 *            the {@link Repository} the data comes from.
+	 * @param config
+	 *            the {@link GpgConfig}
+	 * @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
+	 */
+	SignatureVerification verify(@NonNull Repository repository,
+			@NonNull GpgConfig config, 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 SignatureVerifier} 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.
+	 *
+	 * @param verifierName
+	 *            the name of the verifier that created this verification result
+	 * @param creationDate
+	 *            date and time the signature was created
+	 * @param signer
+	 *            the signer as stored in the signature, or {@code null} if
+	 *            unknown
+	 * @param keyFingerprint
+	 *            fingerprint of the public key, or {@code null} if unknown
+	 * @param keyUser
+	 *            user associated with the key, or {@code null} if unknown
+	 * @param verified
+	 *            whether the signature verification was successful
+	 * @param expired
+	 *            whether the public key used for this signature verification
+	 *            was expired when the signature was created
+	 * @param trustLevel
+	 *            the trust level of the public key used to verify the signature
+	 * @param message
+	 *            human-readable message giving additional information about the
+	 *            outcome of the verification, possibly {@code null}
+	 */
+	record SignatureVerification(
+			String verifierName,
+			Date creationDate,
+			String signer,
+			String keyFingerprint,
+			String keyUser,
+			boolean verified,
+			boolean expired,
+			@NonNull TrustLevel trustLevel,
+			String message) {
+	}
+
+	/**
+	 * 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/SignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifierFactory.java
new file mode 100644
index 0000000..7844aba
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifierFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.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.lib;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A factory for {@link SignatureVerifier}s.
+ *
+ * @since 7.0
+ */
+public interface SignatureVerifierFactory {
+
+	/**
+	 * Tells what kind of {@link SignatureVerifier} this factory creates.
+	 *
+	 * @return the {@link GpgConfig.GpgFormat} of the signer
+	 */
+	@NonNull
+	GpgConfig.GpgFormat getType();
+
+	/**
+	 * Creates a new instance of a {@link SignatureVerifier} that can produce
+	 * signatures of type {@link #getType()}.
+	 *
+	 * @return a new {@link SignatureVerifier}
+	 */
+	@NonNull
+	SignatureVerifier create();
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifiers.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifiers.java
new file mode 100644
index 0000000..01c8422
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignatureVerifiers.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.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.lib;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages the available signers.
+ *
+ * @since 7.0
+ */
+public final class SignatureVerifiers {
+
+	private static final Logger LOG = LoggerFactory.getLogger(SignatureVerifiers.class);
+
+	private static final byte[] PGP_PREFIX = Constants.GPG_SIGNATURE_PREFIX
+			.getBytes(StandardCharsets.US_ASCII);
+
+	private static final byte[] X509_PREFIX = Constants.CMS_SIGNATURE_PREFIX
+			.getBytes(StandardCharsets.US_ASCII);
+
+	private static final byte[] SSH_PREFIX = Constants.SSH_SIGNATURE_PREFIX
+			.getBytes(StandardCharsets.US_ASCII);
+
+	private static final Map<GpgConfig.GpgFormat, SignatureVerifierFactory> FACTORIES = loadSignatureVerifiers();
+
+	private static final Map<GpgConfig.GpgFormat, SignatureVerifier> VERIFIERS = new ConcurrentHashMap<>();
+
+	private static Map<GpgConfig.GpgFormat, SignatureVerifierFactory> loadSignatureVerifiers() {
+		Map<GpgConfig.GpgFormat, SignatureVerifierFactory> result = new EnumMap<>(
+				GpgConfig.GpgFormat.class);
+		try {
+			for (SignatureVerifierFactory factory : ServiceLoader
+					.load(SignatureVerifierFactory.class)) {
+				GpgConfig.GpgFormat format = factory.getType();
+				SignatureVerifierFactory existing = result.get(format);
+				if (existing != null) {
+					LOG.warn("{}", //$NON-NLS-1$
+							MessageFormat.format(
+									JGitText.get().signatureServiceConflict,
+									"SignatureVerifierFactory", format, //$NON-NLS-1$
+									existing.getClass().getCanonicalName(),
+									factory.getClass().getCanonicalName()));
+				} else {
+					result.put(format, factory);
+				}
+			}
+		} catch (ServiceConfigurationError e) {
+			LOG.error(e.getMessage(), e);
+		}
+		return result;
+	}
+
+	private SignatureVerifiers() {
+		// No instantiation
+	}
+
+	/**
+	 * Retrieves a {@link Signer} that can produce signatures of the given type
+	 * {@code format}.
+	 *
+	 * @param format
+	 *            {@link GpgConfig.GpgFormat} the signer must support
+	 * @return a {@link Signer}, or {@code null} if none is available
+	 */
+	public static SignatureVerifier get(@NonNull GpgConfig.GpgFormat format) {
+		return VERIFIERS.computeIfAbsent(format, f -> {
+			SignatureVerifierFactory factory = FACTORIES.get(format);
+			if (factory == null) {
+				return null;
+			}
+			return factory.create();
+		});
+	}
+
+	/**
+	 * Sets a specific signature verifier to use for a specific signature type.
+	 *
+	 * @param format
+	 *            signature type to set the {@code verifier} for
+	 * @param verifier
+	 *            the {@link SignatureVerifier} to use for signatures of type
+	 *            {@code format}; if {@code null}, a default implementation, if
+	 *            available, may be used.
+	 */
+	public static void set(@NonNull GpgConfig.GpgFormat format,
+			SignatureVerifier verifier) {
+		SignatureVerifier previous;
+		if (verifier == null) {
+			previous = VERIFIERS.remove(format);
+		} else {
+			previous = VERIFIERS.put(format, verifier);
+		}
+		if (previous != null) {
+			previous.clear();
+		}
+	}
+
+	/**
+	 * Verifies the signature on a signed commit or tag.
+	 *
+	 * @param repository
+	 *            the {@link Repository} the object is from
+	 * @param config
+	 *            the {@link GpgConfig} to use
+	 * @param object
+	 *            to verify
+	 * @return a {@link SignatureVerifier.SignatureVerification} describing the
+	 *         outcome of the verification, or {@code null} if the object does
+	 *         not have a signature of a known type
+	 * @throws IOException
+	 *             if an error occurs getting a public key
+	 * @throws org.eclipse.jgit.api.errors.JGitInternalException
+	 *             if signature verification fails
+	 */
+	@Nullable
+	public static SignatureVerifier.SignatureVerification verify(
+			@NonNull Repository repository, @NonNull GpgConfig config,
+			@NonNull RevObject object) 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.nextLfSkippingSplitLines(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(repository, config, 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(repository, config, data, signatureData);
+		}
+		return null;
+	}
+
+	/**
+	 * Verifies a given signature for some give data.
+	 *
+	 * @param repository
+	 *            the {@link Repository} the object is from
+	 * @param config
+	 *            the {@link GpgConfig} to use
+	 * @param data
+	 *            to verify the signature of
+	 * @param signature
+	 *            the given signature of the {@code data}
+	 * @return a {@link SignatureVerifier.SignatureVerification} describing the
+	 *         outcome of the verification, or {@code null} if the signature
+	 *         type is unknown
+	 * @throws IOException
+	 *             if an error occurs getting a public key
+	 * @throws org.eclipse.jgit.api.errors.JGitInternalException
+	 *             if signature verification fails
+	 */
+	@Nullable
+	public static SignatureVerifier.SignatureVerification verify(
+			@NonNull Repository repository, @NonNull GpgConfig config,
+			byte[] data, byte[] signature) throws IOException {
+		GpgConfig.GpgFormat format = getFormat(signature);
+		if (format == null) {
+			return null;
+		}
+		SignatureVerifier verifier = get(format);
+		if (verifier == null) {
+			return null;
+		}
+		return verifier.verify(repository, config, data, signature);
+	}
+
+	/**
+	 * Determines the type of a given signature.
+	 *
+	 * @param signature
+	 *            to get the type of
+	 * @return the signature type, or {@code null} if unknown
+	 */
+	@Nullable
+	public static GpgConfig.GpgFormat getFormat(byte[] signature) {
+		if (RawParseUtils.match(signature, 0, PGP_PREFIX) > 0) {
+			return GpgConfig.GpgFormat.OPENPGP;
+		}
+		if (RawParseUtils.match(signature, 0, X509_PREFIX) > 0) {
+			return GpgConfig.GpgFormat.X509;
+		}
+		if (RawParseUtils.match(signature, 0, SSH_PREFIX) > 0) {
+			return GpgConfig.GpgFormat.SSH;
+		}
+		return null;
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signer.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signer.java
new file mode 100644
index 0000000..3bb7464
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signer.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.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.lib;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import org.eclipse.jgit.annotations.NonNull;
+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.transport.CredentialsProvider;
+
+/**
+ * Creates signatures for Git objects.
+ *
+ * @since 7.0
+ */
+public interface Signer {
+
+	/**
+	 * 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 is 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 repository
+	 *            {@link Repository} the object belongs to
+	 * @param config
+	 *            GPG settings from the git config
+	 * @param object
+	 *            the object to sign (must not be {@code null} and must be
+	 *            complete to allow proper calculation of payload)
+	 * @param committer
+	 *            the signing identity (to help with key lookup in case signing
+	 *            key is not specified)
+	 * @param signingKey
+	 *            if non-{@code null} overrides the signing key from the config
+	 * @param credentialsProvider
+	 *            provider to use when querying for signing key credentials (eg.
+	 *            passphrase)
+	 * @throws CanceledException
+	 *             when signing was canceled (eg., user aborted when entering
+	 *             passphrase)
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws UnsupportedSigningFormatException
+	 *             if a config is given and the wanted key format is not
+	 *             supported
+	 */
+	default void signObject(@NonNull Repository repository,
+			@NonNull GpgConfig config, @NonNull ObjectBuilder object,
+			@NonNull PersonIdent committer, String signingKey,
+			CredentialsProvider credentialsProvider)
+			throws CanceledException, IOException,
+			UnsupportedSigningFormatException {
+		try {
+			object.setGpgSignature(sign(repository, config, object.build(),
+					committer, signingKey, credentialsProvider));
+		} catch (UnsupportedEncodingException e) {
+			throw new JGitInternalException(e.getMessage(), e);
+		}
+	}
+
+	/**
+	 * Signs arbitrary data.
+	 *
+	 * @param repository
+	 *            {@link Repository} the signature is created in
+	 * @param config
+	 *            GPG settings from the git config
+	 * @param data
+	 *            the data to sign
+	 * @param committer
+	 *            the signing identity (to help with key lookup in case signing
+	 *            key is not specified)
+	 * @param signingKey
+	 *            if non-{@code null} overrides the signing key from the config
+	 * @param credentialsProvider
+	 *            provider to use when querying for signing key credentials (eg.
+	 *            passphrase)
+	 * @return the signature for {@code data}
+	 * @throws CanceledException
+	 *             when signing was canceled (eg., user aborted when entering
+	 *             passphrase)
+	 * @throws IOException
+	 *             if an I/O error occurs
+	 * @throws UnsupportedSigningFormatException
+	 *             if a config is given and the wanted key format is not
+	 *             supported
+	 */
+	GpgSignature sign(@NonNull Repository repository, @NonNull GpgConfig config,
+			byte[] data, @NonNull PersonIdent committer, String signingKey,
+			CredentialsProvider credentialsProvider) throws CanceledException,
+			IOException, UnsupportedSigningFormatException;
+
+	/**
+	 * Indicates if a signing key is available for the specified committer
+	 * and/or signing key.
+	 *
+	 * @param repository
+	 *            the current {@link Repository}
+	 * @param config
+	 *            GPG settings from the git config
+	 * @param committer
+	 *            the signing identity (to help with key lookup in case signing
+	 *            key is not specified)
+	 * @param signingKey
+	 *            if non-{@code null} overrides the signing key from the config
+	 * @param credentialsProvider
+	 *            provider to use when querying for signing key credentials (eg.
+	 *            passphrase)
+	 * @return {@code true} if a signing key is available, {@code false}
+	 *         otherwise
+	 * @throws CanceledException
+	 *             when signing was canceled (eg., user aborted when entering
+	 *             passphrase)
+	 */
+	boolean canLocateSigningKey(@NonNull Repository repository,
+			@NonNull GpgConfig config, @NonNull PersonIdent committer,
+			String signingKey, CredentialsProvider credentialsProvider)
+			throws CanceledException;
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignerFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignerFactory.java
new file mode 100644
index 0000000..125d25e
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SignerFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.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.lib;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A factory for {@link Signer}s.
+ *
+ * @since 7.0
+ */
+public interface SignerFactory {
+
+	/**
+	 * Tells what kind of {@link Signer} this factory creates.
+	 *
+	 * @return the {@link GpgConfig.GpgFormat} of the signer
+	 */
+	@NonNull
+	GpgConfig.GpgFormat getType();
+
+	/**
+	 * Creates a new instance of a {@link Signer} that can produce signatures of
+	 * type {@link #getType()}.
+	 *
+	 * @return a new {@link Signer}
+	 */
+	@NonNull
+	Signer create();
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signers.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signers.java
new file mode 100644
index 0000000..7771b07
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Signers.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 Thomas Wolf <twolf@apache.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.lib;
+
+import java.text.MessageFormat;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.JGitText;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages the available signers.
+ *
+ * @since 7.0
+ */
+public final class Signers {
+
+	private static final Logger LOG = LoggerFactory.getLogger(Signers.class);
+
+	private static final Map<GpgConfig.GpgFormat, SignerFactory> SIGNER_FACTORIES = loadSigners();
+
+	private static final Map<GpgConfig.GpgFormat, Signer> SIGNERS = new ConcurrentHashMap<>();
+
+	private static Map<GpgConfig.GpgFormat, SignerFactory> loadSigners() {
+		Map<GpgConfig.GpgFormat, SignerFactory> result = new EnumMap<>(
+				GpgConfig.GpgFormat.class);
+		try {
+			for (SignerFactory factory : ServiceLoader
+					.load(SignerFactory.class)) {
+				GpgConfig.GpgFormat format = factory.getType();
+				SignerFactory existing = result.get(format);
+				if (existing != null) {
+					LOG.warn("{}", //$NON-NLS-1$
+							MessageFormat.format(
+									JGitText.get().signatureServiceConflict,
+									"SignerFactory", format, //$NON-NLS-1$
+									existing.getClass().getCanonicalName(),
+									factory.getClass().getCanonicalName()));
+				} else {
+					result.put(format, factory);
+				}
+			}
+		} catch (ServiceConfigurationError e) {
+			LOG.error(e.getMessage(), e);
+		}
+		return result;
+	}
+
+	private Signers() {
+		// No instantiation
+	}
+
+	/**
+	 * Retrieves a {@link Signer} that can produce signatures of the given type
+	 * {@code format}.
+	 *
+	 * @param format
+	 *            {@link GpgConfig.GpgFormat} the signer must support
+	 * @return a {@link Signer}, or {@code null} if none is available
+	 */
+	public static Signer get(@NonNull GpgConfig.GpgFormat format) {
+		return SIGNERS.computeIfAbsent(format, f -> {
+			SignerFactory factory = SIGNER_FACTORIES.get(format);
+			if (factory == null) {
+				return null;
+			}
+			return factory.create();
+		});
+	}
+
+	/**
+	 * Sets a specific signer to use for a specific signature type.
+	 *
+	 * @param format
+	 *            signature type to set the {@code signer} for
+	 * @param signer
+	 *            the {@link Signer} to use for signatures of type
+	 *            {@code format}; if {@code null}, a default implementation, if
+	 *            available, may be used.
+	 */
+	public static void set(@NonNull GpgConfig.GpgFormat format, Signer signer) {
+		if (signer == null) {
+			SIGNERS.remove(format);
+		} else {
+			SIGNERS.put(format, signer);
+		}
+	}
+}
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 bbc6144..ea73d95 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java
@@ -206,23 +206,6 @@ public void setTagger(PersonIdent taggerIdent) {
 		return os.toByteArray();
 	}
 
-	/**
-	 * 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, or {@code null} if the tag cannot be
-	 *         encoded
-	 * @deprecated since 5.11; use {@link #build()} instead
-	 */
-	@Deprecated
-	public byte[] toByteArray() {
-		try {
-			return build();
-		} catch (UnsupportedEncodingException e) {
-			return null;
-		}
-	}
-
 	@SuppressWarnings("nls")
 	@Override
 	public String toString() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java
index 0c03adc..3d4e0d1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java
@@ -17,6 +17,7 @@
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.util.FS;
 
@@ -50,11 +51,36 @@ public interface TypedConfigGetter {
 	 *            default value to return if no value was present.
 	 * @return true if any value or defaultValue is true, false for missing or
 	 *         explicit false
+	 * @deprecated use
+	 *             {@link #getBoolean(Config, String, String, String, Boolean)}
+	 *             instead
 	 */
+	@Deprecated
 	boolean getBoolean(Config config, String section, String subsection,
 			String name, boolean defaultValue);
 
 	/**
+	 * Get a boolean value from a git {@link Config}.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param defaultValue
+	 *            default value to return if no value was present.
+	 * @return true if any value or defaultValue is true, false for missing or
+	 *         explicit false
+	 * @since 7.2
+	 */
+	@Nullable
+	Boolean getBoolean(Config config, String section, String subsection,
+			String name, @Nullable Boolean defaultValue);
+
+	/**
 	 * Parse an enumeration from a git {@link Config}.
 	 *
 	 * @param <T>
@@ -74,8 +100,9 @@ boolean getBoolean(Config config, String section, String subsection,
 	 *            default value to return if no value was present.
 	 * @return the selected enumeration value, or {@code defaultValue}.
 	 */
+	@Nullable
 	<T extends Enum<?>> T getEnum(Config config, T[] all, String section,
-			String subsection, String name, T defaultValue);
+			String subsection, String name, @Nullable T defaultValue);
 
 	/**
 	 * Obtain an integer value from a git {@link Config}.
@@ -91,11 +118,34 @@ <T extends Enum<?>> T getEnum(Config config, T[] all, String section,
 	 * @param defaultValue
 	 *            default value to return if no value was present.
 	 * @return an integer value from the configuration, or defaultValue.
+	 * @deprecated use {@link #getInt(Config, String, String, String, Integer)}
+	 *             instead
 	 */
+	@Deprecated
 	int getInt(Config config, String section, String subsection, String name,
 			int defaultValue);
 
 	/**
+	 * Obtain an integer value from a git {@link Config}.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param defaultValue
+	 *            default value to return if no value was present.
+	 * @return an integer value from the configuration, or defaultValue.
+	 * @since 7.2
+	 */
+	@Nullable
+	Integer getInt(Config config, String section, String subsection,
+			String name, @Nullable Integer defaultValue);
+
+	/**
 	 * Obtain an integer value from a git {@link Config} which must be in given
 	 * range.
 	 *
@@ -117,11 +167,43 @@ int getInt(Config config, String section, String subsection, String name,
 	 * @return an integer value from the configuration, or defaultValue.
 	 *         {@code #UNSET_INT} if unset.
 	 * @since 6.1
+	 * @deprecated use
+	 *             {@link #getIntInRange(Config, String, String, String, int, int, Integer)}
+	 *             instead
 	 */
+	@Deprecated
 	int getIntInRange(Config config, String section, String subsection,
 			String name, int minValue, int maxValue, int defaultValue);
 
 	/**
+	 * Obtain an integer value from a git {@link Config} which must be in given
+	 * range.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param minValue
+	 *            minimal value
+	 * @param maxValue
+	 *            maximum value
+	 * @param defaultValue
+	 *            default value to return if no value was present. Use
+	 *            {@code #UNSET_INT} to set the default to unset.
+	 * @return an integer value from the configuration, or defaultValue.
+	 *         {@code #UNSET_INT} if unset.
+	 * @since 7.2
+	 */
+	@Nullable
+	Integer getIntInRange(Config config, String section, String subsection,
+			String name, int minValue, int maxValue,
+			@Nullable Integer defaultValue);
+
+	/**
 	 * Obtain a long value from a git {@link Config}.
 	 *
 	 * @param config
@@ -135,11 +217,34 @@ int getIntInRange(Config config, String section, String subsection,
 	 * @param defaultValue
 	 *            default value to return if no value was present.
 	 * @return a long value from the configuration, or defaultValue.
+	 * @deprecated use {@link #getLong(Config, String, String, String, Long)}
+	 *             instead
 	 */
+	@Deprecated
 	long getLong(Config config, String section, String subsection, String name,
 			long defaultValue);
 
 	/**
+	 * Obtain a long value from a git {@link Config}.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is grouped within.
+	 * @param subsection
+	 *            subsection name, such a remote or branch name.
+	 * @param name
+	 *            name of the key to get.
+	 * @param defaultValue
+	 *            default value to return if no value was present.
+	 * @return a long value from the configuration, or defaultValue.
+	 * @since 7.2
+	 */
+	@Nullable
+	Long getLong(Config config, String section, String subsection, String name,
+			@Nullable Long defaultValue);
+
+	/**
 	 * Parse a numerical time unit, such as "1 minute", from a git
 	 * {@link Config}.
 	 *
@@ -159,11 +264,41 @@ long getLong(Config config, String section, String subsection, String name,
 	 *            indication of the units.
 	 * @return the value, or {@code defaultValue} if not set, expressed in
 	 *         {@code units}.
+	 * @deprecated use
+	 *             {@link #getTimeUnit(Config, String, String, String, Long, TimeUnit)}
+	 *             instead
 	 */
+	@Deprecated
 	long getTimeUnit(Config config, String section, String subsection,
 			String name, long defaultValue, TimeUnit wantUnit);
 
 	/**
+	 * Parse a numerical time unit, such as "1 minute", from a git
+	 * {@link Config}.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @param section
+	 *            section the key is in.
+	 * @param subsection
+	 *            subsection the key is in, or null if not in a subsection.
+	 * @param name
+	 *            the key name.
+	 * @param defaultValue
+	 *            default value to return if no value was present.
+	 * @param wantUnit
+	 *            the units of {@code defaultValue} and the return value, as
+	 *            well as the units to assume if the value does not contain an
+	 *            indication of the units.
+	 * @return the value, or {@code defaultValue} if not set, expressed in
+	 *         {@code units}.
+	 * @since 7.2
+	 */
+	@Nullable
+	Long getTimeUnit(Config config, String section, String subsection,
+			String name, @Nullable Long defaultValue, TimeUnit wantUnit);
+
+	/**
 	 * Parse a string value from a git {@link Config} and treat it as a file
 	 * path, replacing a ~/ prefix by the user's home directory.
 	 * <p>
@@ -189,9 +324,10 @@ long getTimeUnit(Config config, String section, String subsection,
 	 * @return the {@link Path}, or {@code defaultValue} if not set
 	 * @since 5.10
 	 */
+	@Nullable
 	default Path getPath(Config config, String section, String subsection,
 			String name, @NonNull FS fs, File resolveAgainst,
-			Path defaultValue) {
+			@Nullable Path defaultValue) {
 		String value = config.getString(section, subsection, name);
 		if (value == null) {
 			return defaultValue;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java
index 6d56864..a835a1d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java
@@ -23,5 +23,12 @@ public enum ContentMergeStrategy {
 	OURS,
 
 	/** Resolve the conflict hunk using the theirs version. */
-	THEIRS
-}
\ No newline at end of file
+	THEIRS,
+
+	/**
+	 * Resolve the conflict hunk using a union of both ours and theirs versions.
+	 *
+	 * @since 6.10.1
+	 */
+	UNION
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
index 5734a25..d0d4d36 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java
@@ -120,6 +120,7 @@ public <S extends Sequence> MergeResult<S> merge(
 						result.add(1, 0, 0, ConflictState.NO_CONFLICT);
 						break;
 					case THEIRS:
+					case UNION:
 						result.add(2, 0, theirs.size(),
 								ConflictState.NO_CONFLICT);
 						break;
@@ -148,6 +149,7 @@ public <S extends Sequence> MergeResult<S> merge(
 				// we modified, they deleted
 				switch (strategy) {
 				case OURS:
+				case UNION:
 					result.add(1, 0, ours.size(), ConflictState.NO_CONFLICT);
 					break;
 				case THEIRS:
@@ -158,7 +160,7 @@ public <S extends Sequence> MergeResult<S> merge(
 					result.add(1, 0, ours.size(),
 							ConflictState.FIRST_CONFLICTING_RANGE);
 					result.add(0, 0, base.size(),
-						ConflictState.BASE_CONFLICTING_RANGE);
+							ConflictState.BASE_CONFLICTING_RANGE);
 					result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE);
 					break;
 				}
@@ -333,6 +335,15 @@ public <S extends Sequence> MergeResult<S> merge(
 								theirsEndB - commonSuffix,
 								ConflictState.NO_CONFLICT);
 						break;
+					case UNION:
+						result.add(1, oursBeginB + commonPrefix,
+								oursEndB - commonSuffix,
+								ConflictState.NO_CONFLICT);
+
+						result.add(2, theirsBeginB + commonPrefix,
+								theirsEndB - commonSuffix,
+								ConflictState.NO_CONFLICT);
+						break;
 					default:
 						result.add(1, oursBeginB + commonPrefix,
 								oursEndB - commonSuffix,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java
index a35b30e..079db4a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java
@@ -22,37 +22,6 @@
  * A class to convert merge results into a Git conformant textual presentation
  */
 public class MergeFormatter {
-	/**
-	 * Formats the results of a merge of {@link org.eclipse.jgit.diff.RawText}
-	 * objects in a Git conformant way. This method also assumes that the
-	 * {@link org.eclipse.jgit.diff.RawText} objects being merged are line
-	 * oriented files which use LF as delimiter. This method will also use LF to
-	 * separate chunks and conflict metadata, therefore it fits only to texts
-	 * that are LF-separated lines.
-	 *
-	 * @param out
-	 *            the output stream where to write the textual presentation
-	 * @param res
-	 *            the merge result which should be presented
-	 * @param seqName
-	 *            When a conflict is reported each conflicting range will get a
-	 *            name. This name is following the "&lt;&lt;&lt;&lt;&lt;&lt;&lt;
-	 *            " or "&gt;&gt;&gt;&gt;&gt;&gt;&gt; " conflict markers. The
-	 *            names for the sequences are given in this list
-	 * @param charsetName
-	 *            the name of the character set used when writing conflict
-	 *            metadata
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 * @deprecated Use
-	 *             {@link #formatMerge(OutputStream, MergeResult, List, Charset)}
-	 *             instead.
-	 */
-	@Deprecated
-	public void formatMerge(OutputStream out, MergeResult<RawText> res,
-			List<String> seqName, String charsetName) throws IOException {
-		formatMerge(out, res, seqName, Charset.forName(charsetName));
-	}
 
 	/**
 	 * Formats the results of a merge of {@link org.eclipse.jgit.diff.RawText}
@@ -129,40 +98,6 @@ public void formatMergeDiff3(OutputStream out,
 	 *            the name ranges from ours should get
 	 * @param theirsName
 	 *            the name ranges from theirs should get
-	 * @param charsetName
-	 *            the name of the character set used when writing conflict
-	 *            metadata
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 * @deprecated use
-	 *             {@link #formatMerge(OutputStream, MergeResult, String, String, String, Charset)}
-	 *             instead.
-	 */
-	@Deprecated
-	public void formatMerge(OutputStream out, MergeResult res, String baseName,
-			String oursName, String theirsName, String charsetName) throws IOException {
-		formatMerge(out, res, baseName, oursName, theirsName,
-				Charset.forName(charsetName));
-	}
-
-	/**
-	 * Formats the results of a merge of exactly two
-	 * {@link org.eclipse.jgit.diff.RawText} objects in a Git conformant way.
-	 * This convenience method accepts the names for the three sequences (base
-	 * and the two merged sequences) as explicit parameters and doesn't require
-	 * the caller to specify a List
-	 *
-	 * @param out
-	 *            the {@link java.io.OutputStream} where to write the textual
-	 *            presentation
-	 * @param res
-	 *            the merge result which should be presented
-	 * @param baseName
-	 *            the name ranges from the base should get
-	 * @param oursName
-	 *            the name ranges from ours should get
-	 * @param theirsName
-	 *            the name ranges from theirs should get
 	 * @param charset
 	 *            the character set used when writing conflict metadata
 	 * @throws java.io.IOException
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
index e0c083f..039d7d8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
@@ -92,24 +92,6 @@ public String format(List<Ref> refsToMerge, Ref target) {
 	}
 
 	/**
-	 * Add section with conflicting paths to merge message. Lines are prefixed
-	 * with a hash.
-	 *
-	 * @param message
-	 *            the original merge message
-	 * @param conflictingPaths
-	 *            the paths with conflicts
-	 * @return merge message with conflicting paths added
-	 * @deprecated since 6.1; use
-	 *             {@link #formatWithConflicts(String, Iterable, char)} instead
-	 */
-	@Deprecated
-	public String formatWithConflicts(String message,
-			List<String> conflictingPaths) {
-		return formatWithConflicts(message, conflictingPaths, '#');
-	}
-
-	/**
 	 * Add section with conflicting paths to merge message.
 	 *
 	 * @param message
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java
index 1162a61..f58ef4f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java
@@ -18,10 +18,11 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.time.Instant;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
-import java.util.TimeZone;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -185,12 +186,15 @@ protected RevCommit getBaseCommit(RevCommit a, RevCommit b, int callDepth)
 				if (mergeTrees(bcTree, currentBase.getTree(),
 						nextBase.getTree(), true))
 					currentBase = createCommitForTree(resultTree, parents);
-				else
+				else {
+					String failedPaths = failingPathsMessage();
 					throw new NoMergeBaseException(
 							NoMergeBaseException.MergeBaseFailureReason.CONFLICTS_DURING_MERGE_BASE_CALCULATION,
 							MessageFormat.format(
 									JGitText.get().mergeRecursiveConflictsWhenMergingCommonAncestors,
-									currentBase.getName(), nextBase.getName()));
+									currentBase.getName(), nextBase.getName(),
+									failedPaths));
+				}
 			}
 		} finally {
 			inCore = oldIncore;
@@ -229,11 +233,23 @@ private RevCommit createCommitForTree(ObjectId tree, List<RevCommit> parents)
 	private static PersonIdent mockAuthor(List<RevCommit> parents) {
 		String name = RecursiveMerger.class.getSimpleName();
 		int time = 0;
-		for (RevCommit p : parents)
+		for (RevCommit p : parents) {
 			time = Math.max(time, p.getCommitTime());
-		return new PersonIdent(
-				name, name + "@JGit", //$NON-NLS-1$
-				new Date((time + 1) * 1000L),
-				TimeZone.getTimeZone("GMT+0000")); //$NON-NLS-1$
+		}
+		return new PersonIdent(name, name + "@JGit", //$NON-NLS-1$
+				Instant.ofEpochSecond(time+1), ZoneOffset.UTC);
+	}
+
+	private String failingPathsMessage() {
+		int max = 25;
+		String failedPaths = failingPaths.entrySet().stream().limit(max)
+				.map(entry -> entry.getKey() + ":" + entry.getValue()) //$NON-NLS-1$
+				.collect(Collectors.joining("\n")); //$NON-NLS-1$
+
+		if (failingPaths.size() > max) {
+			failedPaths = String.format("%s\n... (%s failing paths omitted)", //$NON-NLS-1$
+					failedPaths, Integer.valueOf(failingPaths.size() - max));
+		}
+		return failedPaths;
 	}
 }
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 1ad41be..dc96f65 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -41,6 +41,7 @@
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.attributes.Attribute;
 import org.eclipse.jgit.attributes.Attributes;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
 import org.eclipse.jgit.diff.DiffAlgorithm;
 import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
 import org.eclipse.jgit.diff.RawText;
@@ -837,6 +838,13 @@ public enum MergeFailureReason {
 	@NonNull
 	private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT;
 
+	/**
+	 * The {@link AttributesNodeProvider} to use while merging trees.
+	 *
+	 * @since 6.10.1
+	 */
+	protected AttributesNodeProvider attributesNodeProvider;
+
 	private static MergeAlgorithm getMergeAlgorithm(Config config) {
 		SupportedAlgorithm diffAlg = config.getEnum(
 				CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM,
@@ -1273,6 +1281,13 @@ protected boolean processEntry(CanonicalTreeParser base,
 					default:
 						break;
 				}
+				if (ignoreConflicts) {
+					// If the path is selected to be treated as binary via attributes, we do not perform
+					// content merge. When ignoreConflicts = true, we simply keep OURS to allow virtual commit
+					// to be built.
+					keep(ourDce);
+					return true;
+				}
 				// add the conflicting path to merge result
 				String currentPath = tw.getPathString();
 				MergeResult<RawText> result = new MergeResult<>(
@@ -1312,8 +1327,12 @@ protected boolean processEntry(CanonicalTreeParser base,
 					addToCheckout(currentPath, null, attributes);
 					return true;
 				} catch (BinaryBlobException e) {
-					// if the file is binary in either OURS, THEIRS or BASE
-					// here, we don't have an option to ignore conflicts
+					// The file is binary in either OURS, THEIRS or BASE
+					if (ignoreConflicts) {
+						// When ignoreConflicts = true, we simply keep OURS to allow virtual commit to be built.
+						keep(ourDce);
+						return true;
+					}
 				}
 			}
 			switch (getContentMergeStrategy()) {
@@ -1354,6 +1373,8 @@ protected boolean processEntry(CanonicalTreeParser base,
 					}
 				}
 			} else {
+				// This is reachable if contentMerge() call above threw BinaryBlobException, so we don't
+				// need to check ignoreConflicts here, since it's already handled above.
 				result.setContainsConflicts(true);
 				addConflict(base, ours, theirs);
 				unmergedPaths.add(currentPath);
@@ -1489,11 +1510,26 @@ private MergeResult<RawText> contentMerge(CanonicalTreeParser base,
 				: getRawText(ours.getEntryObjectId(), attributes[T_OURS]);
 		RawText theirsText = theirs == null ? RawText.EMPTY_TEXT
 				: getRawText(theirs.getEntryObjectId(), attributes[T_THEIRS]);
-		mergeAlgorithm.setContentMergeStrategy(strategy);
+		mergeAlgorithm.setContentMergeStrategy(
+				getAttributesContentMergeStrategy(attributes[T_OURS],
+						strategy));
 		return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText,
 				ourText, theirsText);
 	}
 
+	private ContentMergeStrategy getAttributesContentMergeStrategy(
+			Attributes attributes, ContentMergeStrategy strategy) {
+		Attribute attr = attributes.get(Constants.ATTR_MERGE);
+		if (attr != null) {
+			String attrValue = attr.getValue();
+			if (attrValue != null && attrValue
+					.equals(Constants.ATTR_BUILTIN_UNION_MERGE_DRIVER)) {
+				return ContentMergeStrategy.UNION;
+			}
+		}
+		return strategy;
+	}
+
 	private boolean isIndexDirty() {
 		if (inCore) {
 			return false;
@@ -1824,6 +1860,18 @@ public void setWorkingTreeIterator(WorkingTreeIterator workingTreeIterator) {
 		this.workingTreeIterator = workingTreeIterator;
 	}
 
+	/**
+	 * Sets the {@link AttributesNodeProvider} to be used by this merger.
+	 *
+	 * @param attributesNodeProvider
+	 *            the attributeNodeProvider to set
+	 * @since 6.10.1
+	 */
+	public void setAttributesNodeProvider(
+			AttributesNodeProvider attributesNodeProvider) {
+		this.attributesNodeProvider = attributesNodeProvider;
+	}
+
 
 	/**
 	 * The resolve conflict way of three way merging
@@ -1868,6 +1916,9 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree,
 					WorkTreeUpdater.createWorkTreeUpdater(db, dircache);
 			dircache = workTreeUpdater.getLockedDirCache();
 			tw = new NameConflictTreeWalk(db, reader);
+			if (attributesNodeProvider != null) {
+				tw.setAttributesNodeProvider(attributesNodeProvider);
+			}
 
 			tw.addTree(baseTree);
 			tw.setHead(tw.addTree(headTree));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java
index 79ceb13..30512c1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java
@@ -199,7 +199,7 @@ private void addIfNotNull(FanoutBucket b, int cell, NoteBucket child)
 		if (child == null)
 			return;
 		if (child instanceof InMemoryNoteBucket)
-			b.setBucket(cell, ((InMemoryNoteBucket) child).writeTree(inserter));
+			b.setBucket(cell, child.writeTree(inserter));
 		else
 			b.setBucket(cell, child.getTreeId());
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java
index a327095..23e09b9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java
@@ -23,6 +23,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
@@ -33,12 +34,13 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.zip.InflaterInputStream;
+
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.FilterFailedException;
-import org.eclipse.jgit.api.errors.PatchFormatException;
 import org.eclipse.jgit.attributes.Attribute;
 import org.eclipse.jgit.attributes.Attributes;
 import org.eclipse.jgit.attributes.FilterCommand;
@@ -101,11 +103,12 @@
  * @since 6.4
  */
 public class PatchApplier {
-
 	private static final byte[] NO_EOL = "\\ No newline at end of file" //$NON-NLS-1$
 			.getBytes(StandardCharsets.US_ASCII);
 
-	/** The tree before applying the patch. Only non-null for inCore operation. */
+	/**
+	 * The tree before applying the patch. Only non-null for inCore operation.
+	 */
 	@Nullable
 	private final RevTree beforeTree;
 
@@ -115,10 +118,14 @@ public class PatchApplier {
 
 	private final ObjectReader reader;
 
+	private final Charset charset;
+
 	private WorkingTreeOptions workingTreeOptions;
 
 	private int inCoreSizeLimit;
 
+	private boolean allowConflicts;
+
 	/**
 	 * @param repo
 	 *            repository to apply the patch in
@@ -128,7 +135,8 @@ public PatchApplier(Repository repo) {
 		inserter = repo.newObjectInserter();
 		reader = inserter.newReader();
 		beforeTree = null;
-
+		allowConflicts = false;
+		charset = StandardCharsets.UTF_8;
 		Config config = repo.getConfig();
 		workingTreeOptions = config.get(WorkingTreeOptions.KEY);
 		inCoreSizeLimit = config.getInt(ConfigConstants.CONFIG_MERGE_SECTION,
@@ -143,11 +151,14 @@ public PatchApplier(Repository repo) {
 	 * @param oi
 	 *            to be used for modifying objects
 	 */
-	public PatchApplier(Repository repo, RevTree beforeTree, ObjectInserter oi)  {
+	public PatchApplier(Repository repo, RevTree beforeTree,
+			ObjectInserter oi) {
 		this.repo = repo;
 		this.beforeTree = beforeTree;
 		inserter = oi;
 		reader = oi.newReader();
+		allowConflicts = false;
+		charset = StandardCharsets.UTF_8;
 	}
 
 	/**
@@ -157,7 +168,6 @@ public PatchApplier(Repository repo, RevTree beforeTree, ObjectInserter oi)  {
 	 * @since 6.3
 	 */
 	public static class Result {
-
 		/**
 		 * A wrapper for a patch applying error that affects a given file.
 		 *
@@ -166,28 +176,68 @@ public static class Result {
 		// TODO(ms): rename this class in next major release
 		@SuppressWarnings("JavaLangClash")
 		public static class Error {
+			final String msg;
 
-			private String msg;
-			private String oldFileName;
-			private @Nullable HunkHeader hh;
+			final String oldFileName;
 
-			private Error(String msg, String oldFileName,
-					@Nullable HunkHeader hh) {
+			@Nullable
+			final HunkHeader hh;
+
+			final boolean isGitConflict;
+
+			Error(String msg, String oldFileName, @Nullable HunkHeader hh,
+					boolean isGitConflict) {
 				this.msg = msg;
 				this.oldFileName = oldFileName;
 				this.hh = hh;
+				this.isGitConflict = isGitConflict;
+			}
+
+			/**
+			 * Signals if as part of encountering this error, conflict markers
+			 * were added to the file.
+			 *
+			 * @return {@code true} if conflict markers were added for this
+			 *         error.
+			 *
+			 * @since 6.10
+			 */
+			public boolean isGitConflict() {
+				return isGitConflict;
 			}
 
 			@Override
 			public String toString() {
 				if (hh != null) {
-					return MessageFormat.format(JGitText.get().patchApplyErrorWithHunk,
-							oldFileName, hh, msg);
+					return MessageFormat.format(
+							JGitText.get().patchApplyErrorWithHunk, oldFileName,
+							hh, msg);
 				}
-				return MessageFormat.format(JGitText.get().patchApplyErrorWithoutHunk,
-						oldFileName, msg);
+				return MessageFormat.format(
+						JGitText.get().patchApplyErrorWithoutHunk, oldFileName,
+						msg);
 			}
 
+			@Override
+			public boolean equals(Object o) {
+				if (this == o) {
+					return true;
+				}
+				if (o == null || !(o instanceof Error)) {
+					return false;
+				}
+				Error error = (Error) o;
+				return Objects.equals(msg, error.msg)
+						&& Objects.equals(oldFileName, error.oldFileName)
+						&& Objects.equals(hh, error.hh)
+						&& isGitConflict == error.isGitConflict;
+			}
+
+			@Override
+			public int hashCode() {
+				return Objects.hash(msg, oldFileName, hh,
+						Boolean.valueOf(isGitConflict));
+			}
 		}
 
 		private ObjectId treeId;
@@ -225,35 +275,15 @@ public List<Error> getErrors() {
 			return errors;
 		}
 
-		private void addError(String msg,String oldFileName, @Nullable HunkHeader hh) {
-			errors.add(new Error(msg, oldFileName, hh));
+		private void addError(String msg, String oldFileName,
+				@Nullable HunkHeader hh) {
+			errors.add(new Error(msg, oldFileName, hh, false));
 		}
-	}
 
-	/**
-	 * Applies the given patch
-	 *
-	 * @param patchInput
-	 *            the patch to apply.
-	 * @return the result of the patch
-	 * @throws PatchFormatException
-	 *             if the patch cannot be parsed
-	 * @throws IOException
-	 *             if the patch read fails
-	 * @deprecated use {@link #applyPatch(Patch)} instead
-	 */
-	@Deprecated
-	public Result applyPatch(InputStream patchInput)
-			throws PatchFormatException, IOException {
-		Patch p = new Patch();
-		try (InputStream inStream = patchInput) {
-			p.parse(inStream);
-
-			if (!p.getErrors().isEmpty()) {
-				throw new PatchFormatException(p.getErrors());
-			}
+		private void addErrorWithGitConflict(String msg, String oldFileName,
+				@Nullable HunkHeader hh) {
+			errors.add(new Error(msg, oldFileName, hh, true));
 		}
-		return applyPatch(p);
 	}
 
 	/**
@@ -357,6 +387,17 @@ else if (!dirCacheBuilder.commit()) {
 		return result;
 	}
 
+	/**
+	 * Sets up the {@link PatchApplier} to apply patches even if they conflict.
+	 *
+	 * @return the {@link PatchApplier} to apply any patches
+	 * @since 6.10
+	 */
+	public PatchApplier allowConflicts() {
+		allowConflicts = true;
+		return this;
+	}
+
 	private File getFile(String path) {
 		return inCore() ? null : new File(repo.getWorkTree(), path);
 	}
@@ -439,6 +480,7 @@ private boolean validGitPath(String path) {
 			return false;
 		}
 	}
+
 	private static final int FILE_TREE_INDEX = 1;
 
 	/**
@@ -539,7 +581,9 @@ private void apply(String pathWithOriginalContent, DirCache dirCache,
 					convertCrLf);
 			resultStreamLoader = applyText(raw, fh, result);
 		}
-		if (resultStreamLoader == null || !result.getErrors().isEmpty()) {
+		if (resultStreamLoader == null
+				|| (!result.getErrors().isEmpty() && result.getErrors().stream()
+						.anyMatch(e -> !e.msg.equals("cannot apply hunk")))) { //$NON-NLS-1$
 			return;
 		}
 
@@ -961,9 +1005,51 @@ && canApplyAt(hunkLines, newLines, 0)) {
 				}
 			}
 			if (!applies) {
-				result.addError(JGitText.get().applyTextPatchCannotApplyHunk,
-						fh.getOldPath(), hh);
-				return null;
+				if (!allowConflicts) {
+					result.addError(
+							JGitText.get().applyTextPatchCannotApplyHunk,
+							fh.getOldPath(), hh);
+					return null;
+				}
+				// Insert conflict markers. This is best-guess because the
+				// file might have changed completely. But at least we give
+				// the user a graceful state that they can resolve manually.
+				// An alternative to this is using the 3-way merger. This
+				// only works if the pre-image SHA is contained in the repo.
+				// If that was the case, cherry-picking the original commit
+				// should be preferred to apply a patch.
+				result.addErrorWithGitConflict("cannot apply hunk", fh.getOldPath(), hh); //$NON-NLS-1$
+				newLines.add(Math.min(applyAt++, newLines.size()),
+						asBytes("<<<<<<< HEAD")); //$NON-NLS-1$
+				applyAt += hh.getOldImage().lineCount;
+				newLines.add(Math.min(applyAt++, newLines.size()),
+						asBytes("=======")); //$NON-NLS-1$
+
+				int sz = hunkLines.size();
+				for (int j = 1; j < sz; j++) {
+					ByteBuffer hunkLine = hunkLines.get(j);
+					if (!hunkLine.hasRemaining()) {
+						// Completely empty line; accept as empty context
+						// line
+						applyAt++;
+						lastWasRemoval = false;
+						continue;
+					}
+					switch (hunkLine.array()[hunkLine.position()]) {
+					case ' ':
+					case '+':
+						newLines.add(Math.min(applyAt++, newLines.size()),
+								slice(hunkLine, 1));
+						break;
+					case '-':
+					case '\\':
+					default:
+						break;
+					}
+				}
+				newLines.add(Math.min(applyAt++, newLines.size()),
+						asBytes(">>>>>>> PATCH")); //$NON-NLS-1$
+				continue;
 			}
 			// Hunk applies at applyAt. Apply it, and update afterLastHunk and
 			// lineNumberShift
@@ -1010,7 +1096,11 @@ && canApplyAt(hunkLines, newLines, 0)) {
 		} else if (!rt.isMissingNewlineAtEnd()) {
 			newLines.add(null);
 		}
+		return toContentStreamLoader(newLines);
+	}
 
+	private static ContentStreamLoader toContentStreamLoader(
+			List<ByteBuffer> newLines) throws IOException {
 		// We could check if old == new, but the short-circuiting complicates
 		// logic for inCore patching, so just write the new thing regardless.
 		TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
@@ -1034,6 +1124,10 @@ && canApplyAt(hunkLines, newLines, 0)) {
 		}
 	}
 
+	private ByteBuffer asBytes(String str) {
+		return ByteBuffer.wrap(str.getBytes(charset));
+	}
+
 	@SuppressWarnings("ByteBufferBackingArray")
 	private boolean canApplyAt(List<ByteBuffer> hunkLines,
 			List<ByteBuffer> newLines, int line) {
@@ -1123,4 +1217,4 @@ public void close() throws IOException {
 			in.close();
 		}
 	}
-}
+}
\ No newline at end of file
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 82671d9..7c763bc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
@@ -43,7 +43,7 @@
  * Tree and blob objects reachable from interesting commits are automatically
  * scheduled for inclusion in the results of {@link #nextObject()}, returning
  * each object exactly once. Objects are sorted and returned according to the
- * the commits that reference them and the order they appear within a tree.
+ * commits that reference them and the order they appear within a tree.
  * Ordering can be affected by changing the
  * {@link org.eclipse.jgit.revwalk.RevSort} used to order the commits that are
  * returned first.
@@ -164,29 +164,6 @@ private ObjectWalk(ObjectReader or, boolean closeReader) {
 	}
 
 	/**
-	 * Create an object reachability checker that will use bitmaps if possible.
-	 *
-	 * This reachability checker accepts any object as target. For checks
-	 * exclusively between commits, see
-	 * {@link RevWalk#createReachabilityChecker()}.
-	 *
-	 * @return an object reachability checker, using bitmaps if possible.
-	 *
-	 * @throws IOException
-	 *             when the index fails to load.
-	 *
-	 * @since 5.8
-	 * @deprecated use
-	 *             {@code ObjectReader#createObjectReachabilityChecker(ObjectWalk)}
-	 *             instead.
-	 */
-	@Deprecated
-	public final ObjectReachabilityChecker createObjectReachabilityChecker()
-			throws IOException {
-		return reader.createObjectReachabilityChecker(this);
-	}
-
-	/**
 	 * Mark an object or commit to start graph traversal from.
 	 * <p>
 	 * Callers are encouraged to use
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java
index 1a869a0..5afb669 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java
@@ -26,40 +26,6 @@
  * @since 5.4
  */
 public interface ReachabilityChecker {
-
-	/**
-	 * Check if all targets are reachable from the {@code starters} commits.
-	 * <p>
-	 * Caller should parse the objectIds (preferably with
-	 * {@code walk.parseCommit()} and handle missing/incorrect type objects
-	 * before calling this method.
-	 *
-	 * @param targets
-	 *            commits to reach.
-	 * @param starters
-	 *            known starting points.
-	 * @return An unreachable target if at least one of the targets is
-	 *         unreachable. An empty optional if all targets are reachable from
-	 *         the starters.
-	 *
-	 * @throws MissingObjectException
-	 *             if any of the incoming objects doesn't exist in the
-	 *             repository.
-	 * @throws IncorrectObjectTypeException
-	 *             if any of the incoming objects is not a commit or a tag.
-	 * @throws IOException
-	 *             if any of the underlying indexes or readers can not be
-	 *             opened.
-	 *
-	 * @deprecated see {{@link #areAllReachable(Collection, Stream)}
-	 */
-	@Deprecated
-	default Optional<RevCommit> areAllReachable(Collection<RevCommit> targets,
-                       Collection<RevCommit> starters) throws MissingObjectException,
-			IncorrectObjectTypeException, IOException {
-		return areAllReachable(targets, starters.stream());
-	}
-
 	/**
 	 * Check if all targets are reachable from the {@code starters} commits.
 	 * <p>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java
index 743a8cc..871545f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java
@@ -1,6 +1,6 @@
 /*
- * Copyright (C) 2008-2009, Google Inc.
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2009 Google Inc.
+ * Copyright (C) 2008, 2024 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
@@ -401,13 +401,13 @@ public RevCommit getParent(int nth) {
 	 * @since 5.1
 	 */
 	public final byte[] getRawGpgSignature() {
-		final byte[] raw = buffer;
-		final byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' };
-		final int start = RawParseUtils.headerStart(header, raw, 0);
+		byte[] raw = buffer;
+		byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' };
+		int start = RawParseUtils.headerStart(header, raw, 0);
 		if (start < 0) {
 			return null;
 		}
-		final int end = RawParseUtils.headerEnd(raw, start);
+		int end = RawParseUtils.nextLfSkippingSplitLines(raw, start);
 		return RawParseUtils.headerValue(raw, start, end);
 	}
 
@@ -524,6 +524,30 @@ static boolean hasLF(byte[] r, int b, int e) {
 	}
 
 	/**
+	 * Parse the commit message and return its first line, i.e., everything up
+	 * to but not including the first newline, if any.
+	 *
+	 * @return the first line of the decoded commit message as a string; never
+	 *         {@code null}.
+	 * @since 7.2
+	 */
+	public final String getFirstMessageLine() {
+		int msgB = RawParseUtils.commitMessage(buffer, 0);
+		if (msgB < 0) {
+			return ""; //$NON-NLS-1$
+		}
+		int msgE = msgB;
+		byte[] raw = buffer;
+		while (msgE < raw.length && raw[msgE] != '\n') {
+			msgE++;
+		}
+		if (msgE > msgB && msgE > 0 && raw[msgE - 1] == '\r') {
+			msgE--;
+		}
+		return RawParseUtils.decode(guessEncoding(buffer), buffer, msgB, msgE);
+	}
+
+	/**
 	 * Determine the encoding of the commit message buffer.
 	 * <p>
 	 * Locates the "encoding" header (if present) and returns its value. Due to
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 75dbd57..0737a78 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
@@ -38,8 +38,17 @@
  */
 public class RevTag extends RevObject {
 
-	private static final byte[] hSignature = Constants
-			.encodeASCII("-----BEGIN PGP SIGNATURE-----"); //$NON-NLS-1$
+	private static final byte[] SIGNATURE_START = Constants
+			.encodeASCII("-----BEGIN"); //$NON-NLS-1$
+
+	private static final byte[] GPG_SIGNATURE_START = Constants
+			.encodeASCII(Constants.GPG_SIGNATURE_PREFIX);
+
+	private static final byte[] CMS_SIGNATURE_START = Constants
+			.encodeASCII(Constants.CMS_SIGNATURE_PREFIX);
+
+	private static final byte[] SSH_SIGNATURE_START = Constants
+			.encodeASCII(Constants.SSH_SIGNATURE_PREFIX);
 
 	/**
 	 * Parse an annotated tag from its canonical format.
@@ -208,20 +217,27 @@ private int getSignatureStart() {
 			return msgB;
 		}
 		// Find the last signature start and return the rest
-		int start = nextStart(hSignature, raw, msgB);
+		int start = nextStart(SIGNATURE_START, raw, msgB);
 		if (start < 0) {
 			return start;
 		}
 		int next = RawParseUtils.nextLF(raw, start);
 		while (next < raw.length) {
-			int newStart = nextStart(hSignature, raw, next);
+			int newStart = nextStart(SIGNATURE_START, raw, next);
 			if (newStart < 0) {
 				break;
 			}
 			start = newStart;
 			next = RawParseUtils.nextLF(raw, start);
 		}
-		return start;
+		// SIGNATURE_START is just a prefix. Check that it is one of the known
+		// full signature start tags.
+		if (RawParseUtils.match(raw, start, GPG_SIGNATURE_START) > 0
+				|| RawParseUtils.match(raw, start, CMS_SIGNATURE_START) > 0
+				|| RawParseUtils.match(raw, start, SSH_SIGNATURE_START) > 0) {
+			return start;
+		}
+		return -1;
 	}
 
 	/**
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 76c14e9..41f98ba 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
@@ -19,9 +19,14 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Optional;
+import java.util.Map;
+import java.util.
+Optional;
+import java.util.Set;
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
@@ -31,9 +36,9 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RevWalkException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.AsyncObjectLoaderQueue;
-import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -279,23 +284,6 @@ public ObjectReader getObjectReader() {
 	}
 
 	/**
-	 * Get a reachability checker for commits over this revwalk.
-	 *
-	 * @return the most efficient reachability checker for this repository.
-	 * @throws IOException
-	 *             if it cannot open any of the underlying indices.
-	 *
-	 * @since 5.4
-	 * @deprecated use {@code ObjectReader#createReachabilityChecker(RevWalk)}
-	 *             instead.
-	 */
-	@Deprecated
-	public final ReachabilityChecker createReachabilityChecker()
-			throws IOException {
-		return reader.createReachabilityChecker(this);
-	}
-
-	/**
 	 * {@inheritDoc}
 	 * <p>
 	 * Release any resources used by this walker's reader.
@@ -540,6 +528,27 @@ public boolean isMergedIntoAny(RevCommit commit, Collection<Ref> refs)
 	}
 
 	/**
+	 * Determine if a <code>commit</code> is merged into any of the given
+	 * <code>revs</code>.
+	 *
+	 * @param commit
+	 *            commit the caller thinks is reachable from <code>revs</code>.
+	 * @param revs
+	 *            commits to start iteration from, and which is most likely a
+	 *            descendant (child) of <code>commit</code>.
+	 * @return true if commit is merged into any of the revs; false otherwise.
+	 * @throws java.io.IOException
+	 *             a pack file or loose object could not be read.
+	 * @since 6.10.1
+	 */
+	public boolean isMergedIntoAnyCommit(RevCommit commit, Collection<RevCommit> revs)
+			throws IOException {
+		return getCommitsMergedInto(commit, revs,
+				GetMergedIntoStrategy.RETURN_ON_FIRST_FOUND,
+				NullProgressMonitor.INSTANCE).size() > 0;
+	}
+
+	/**
 	 * Determine if a <code>commit</code> is merged into all of the given
 	 * <code>refs</code>.
 	 *
@@ -562,7 +571,26 @@ public boolean isMergedIntoAll(RevCommit commit, Collection<Ref> refs)
 
 	private List<Ref> getMergedInto(RevCommit needle, Collection<Ref> haystacks,
 			Enum returnStrategy, ProgressMonitor monitor) throws IOException {
+		Map<RevCommit, List<Ref>> refsByCommit = new HashMap<>();
+		for (Ref r : haystacks) {
+			RevObject o = peel(parseAny(r.getObjectId()));
+			if (!(o instanceof RevCommit)) {
+				continue;
+			}
+			refsByCommit.computeIfAbsent((RevCommit) o, c -> new ArrayList<>()).add(r);
+		}
+		monitor.update(1);
 		List<Ref> result = new ArrayList<>();
+		for (RevCommit c : getCommitsMergedInto(needle, refsByCommit.keySet(),
+				returnStrategy, monitor)) {
+			result.addAll(refsByCommit.get(c));
+		}
+		return result;
+	}
+
+	private Set<RevCommit> getCommitsMergedInto(RevCommit needle, Collection<RevCommit> haystacks,
+			Enum returnStrategy, ProgressMonitor monitor) throws IOException {
+		Set<RevCommit> result = new HashSet<>();
 		List<RevCommit> uninteresting = new ArrayList<>();
 		List<RevCommit> marked = new ArrayList<>();
 		RevFilter oldRF = filter;
@@ -578,16 +606,11 @@ private List<Ref> getMergedInto(RevCommit needle, Collection<Ref> haystacks,
 				needle.parseHeaders(this);
 			}
 			int cutoff = needle.getGeneration();
-			for (Ref r : haystacks) {
+			for (RevCommit c : haystacks) {
 				if (monitor.isCancelled()) {
 					return result;
 				}
 				monitor.update(1);
-				RevObject o = peel(parseAny(r.getObjectId()));
-				if (!(o instanceof RevCommit)) {
-					continue;
-				}
-				RevCommit c = (RevCommit) o;
 				reset(UNINTERESTING | TEMP_MARK);
 				markStart(c);
 				boolean commitFound = false;
@@ -599,7 +622,7 @@ private List<Ref> getMergedInto(RevCommit needle, Collection<Ref> haystacks,
 					}
 					if (References.isSameObject(next, needle)
 							|| (next.flags & TEMP_MARK) != 0) {
-						result.add(r);
+						result.add(c);
 						if (returnStrategy == GetMergedIntoStrategy.RETURN_ON_FIRST_FOUND) {
 							return result;
 						}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitTimeRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitTimeRevFilter.java
index 4100e87..c9186b5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitTimeRevFilter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitTimeRevFilter.java
@@ -12,6 +12,7 @@
 package org.eclipse.jgit.revwalk.filter;
 
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Date;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -30,9 +31,24 @@ public abstract class CommitTimeRevFilter extends RevFilter {
 	 * @param ts
 	 *            the point in time to cut on.
 	 * @return a new filter to select commits on or before <code>ts</code>.
+	 *
+	 * @deprecated Use {@link #before(Instant)} instead.
 	 */
+	@Deprecated(since="7.2")
 	public static final RevFilter before(Date ts) {
-		return before(ts.getTime());
+		return before(ts.toInstant());
+	}
+
+	/**
+	 * Create a new filter to select commits before a given date/time.
+	 *
+	 * @param ts
+	 *            the point in time to cut on.
+	 * @return a new filter to select commits on or before <code>ts</code>.
+	 * @since 7.2
+	 */
+	public static RevFilter before(Instant ts) {
+		return new Before(ts);
 	}
 
 	/**
@@ -43,7 +59,7 @@ public static final RevFilter before(Date ts) {
 	 * @return a new filter to select commits on or before <code>ts</code>.
 	 */
 	public static final RevFilter before(long ts) {
-		return new Before(ts);
+		return new Before(Instant.ofEpochMilli(ts));
 	}
 
 	/**
@@ -52,9 +68,24 @@ public static final RevFilter before(long ts) {
 	 * @param ts
 	 *            the point in time to cut on.
 	 * @return a new filter to select commits on or after <code>ts</code>.
+	 *
+	 * @deprecated Use {@link #after(Instant)} instead.
 	 */
+	@Deprecated(since="7.2")
 	public static final RevFilter after(Date ts) {
-		return after(ts.getTime());
+		return after(ts.toInstant());
+	}
+
+	/**
+	 * Create a new filter to select commits after a given date/time.
+	 *
+	 * @param ts
+	 *            the point in time to cut on.
+	 * @return a new filter to select commits on or after <code>ts</code>.
+	 * @since 7.2
+	 */
+	public static RevFilter after(Instant ts) {
+		return new After(ts);
 	}
 
 	/**
@@ -65,7 +96,7 @@ public static final RevFilter after(Date ts) {
 	 * @return a new filter to select commits on or after <code>ts</code>.
 	 */
 	public static final RevFilter after(long ts) {
-		return new After(ts);
+		return after(Instant.ofEpochMilli(ts));
 	}
 
 	/**
@@ -75,9 +106,28 @@ public static final RevFilter after(long ts) {
 	 * @param since the point in time to cut on.
 	 * @param until the point in time to cut off.
 	 * @return a new filter to select commits between the given date/times.
+	 *
+	 * @deprecated Use {@link #between(Instant, Instant)} instead.
 	 */
+	@Deprecated(since="7.2")
 	public static final RevFilter between(Date since, Date until) {
-		return between(since.getTime(), until.getTime());
+		return between(since.toInstant(), until.toInstant());
+	}
+
+	/**
+	 * Create a new filter to select commits after or equal a given date/time
+	 * <code>since</code> and before or equal a given date/time
+	 * <code>until</code>.
+	 *
+	 * @param since
+	 *            the point in time to cut on.
+	 * @param until
+	 *            the point in time to cut off.
+	 * @return a new filter to select commits between the given date/times.
+	 * @since 7.2
+	 */
+	public static RevFilter between(Instant since, Instant until) {
+		return new Between(since, until);
 	}
 
 	/**
@@ -87,9 +137,12 @@ public static final RevFilter between(Date since, Date until) {
 	 * @param since the point in time to cut on, in milliseconds.
 	 * @param until the point in time to cut off, in millisconds.
 	 * @return a new filter to select commits between the given date/times.
+	 *
+	 * @deprecated Use {@link #between(Instant, Instant)} instead.
 	 */
+	@Deprecated(since="7.2")
 	public static final RevFilter between(long since, long until) {
-		return new Between(since, until);
+		return new Between(Instant.ofEpochMilli(since), Instant.ofEpochMilli(until));
 	}
 
 	final int when;
@@ -98,6 +151,10 @@ public static final RevFilter between(long since, long until) {
 		when = (int) (ts / 1000);
 	}
 
+	CommitTimeRevFilter(Instant t) {
+		when = (int) t.getEpochSecond();
+	}
+
 	@Override
 	public RevFilter clone() {
 		return this;
@@ -109,8 +166,8 @@ public boolean requiresCommitBody() {
 	}
 
 	private static class Before extends CommitTimeRevFilter {
-		Before(long ts) {
-			super(ts);
+		Before(Instant t) {
+			super(t);
 		}
 
 		@Override
@@ -123,14 +180,12 @@ public boolean include(RevWalk walker, RevCommit cmit)
 		@SuppressWarnings("nls")
 		@Override
 		public String toString() {
-			return super.toString() + "(" + new Date(when * 1000L) + ")";
+			return super.toString() + "(" + Instant.ofEpochSecond(when) + ")";
 		}
 	}
 
 	private static class After extends CommitTimeRevFilter {
-		After(long ts) {
-			super(ts);
-		}
+		After(Instant t) { super(t); }
 
 		@Override
 		public boolean include(RevWalk walker, RevCommit cmit)
@@ -148,16 +203,16 @@ public boolean include(RevWalk walker, RevCommit cmit)
 		@SuppressWarnings("nls")
 		@Override
 		public String toString() {
-			return super.toString() + "(" + new Date(when * 1000L) + ")";
+			return super.toString() + "(" + Instant.ofEpochSecond(when) + ")";
 		}
 	}
 
 	private static class Between extends CommitTimeRevFilter {
 		private final int until;
 
-		Between(long since, long until) {
+		Between(Instant since, Instant until) {
 			super(since);
-			this.until = (int) (until / 1000);
+			this.until = (int) until.getEpochSecond();
 		}
 
 		@Override
@@ -170,8 +225,8 @@ public boolean include(RevWalk walker, RevCommit cmit)
 		@SuppressWarnings("nls")
 		@Override
 		public String toString() {
-			return super.toString() + "(" + new Date(when * 1000L) + " - "
-					+ new Date(until * 1000L) + ")";
+			return super.toString() + "(" + Instant.ofEpochSecond(when) + " - "
+					+ Instant.ofEpochSecond(until) + ")";
 		}
 
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheStats.java
index 7cb8618..668b92c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheStats.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/WindowCacheStats.java
@@ -22,27 +22,6 @@
  */
 @MXBean
 public interface WindowCacheStats {
-	/**
-	 * Get number of open files
-	 *
-	 * @return the number of open files.
-	 * @deprecated use {@link #getOpenFileCount()} instead
-	 */
-	@Deprecated
-	public static int getOpenFiles() {
-		return (int) WindowCache.getInstance().getStats().getOpenFileCount();
-	}
-
-	/**
-	 * Get number of open bytes
-	 *
-	 * @return the number of open bytes.
-	 * @deprecated use {@link #getOpenByteCount()} instead
-	 */
-	@Deprecated
-	public static long getOpenBytes() {
-		return WindowCache.getInstance().getStats().getOpenByteCount();
-	}
 
 	/**
 	 * Get cache statistics
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java
index 8373d68..863b794 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java
@@ -50,7 +50,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.zip.Deflater;
 
-import org.eclipse.jgit.internal.storage.file.PackIndexWriter;
+import org.eclipse.jgit.internal.storage.file.BasePackIndexWriter;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 
@@ -995,7 +995,7 @@ public void setExecutor(Executor executor) {
 	 *
 	 * @return the index version, the special version 0 designates the oldest
 	 *         (most compatible) format available for the objects.
-	 * @see PackIndexWriter
+	 * @see BasePackIndexWriter
 	 */
 	public int getIndexVersion() {
 		return indexVersion;
@@ -1009,7 +1009,7 @@ public int getIndexVersion() {
 	 * @param version
 	 *            the version to write. The special version 0 designates the
 	 *            oldest (most compatible) format available for the objects.
-	 * @see PackIndexWriter
+	 * @see BasePackIndexWriter
 	 */
 	public void setIndexVersion(int version) {
 		indexVersion = version;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
index becc808..105cba7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
@@ -787,14 +787,14 @@ public IgnoreSubmoduleMode getModulesIgnore() throws IOException,
 		IgnoreSubmoduleMode mode = repoConfig.getEnum(
 				IgnoreSubmoduleMode.values(),
 				ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(),
-				ConfigConstants.CONFIG_KEY_IGNORE, null);
+				ConfigConstants.CONFIG_KEY_IGNORE);
 		if (mode != null) {
 			return mode;
 		}
 		lazyLoadModulesConfig();
-		return modulesConfig.getEnum(IgnoreSubmoduleMode.values(),
-				ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(),
-				ConfigConstants.CONFIG_KEY_IGNORE, IgnoreSubmoduleMode.NONE);
+		return modulesConfig.getEnum(ConfigConstants.CONFIG_SUBMODULE_SECTION,
+				getModuleName(), ConfigConstants.CONFIG_KEY_IGNORE,
+				IgnoreSubmoduleMode.NONE);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java
index b873925..aaf9f8a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java
@@ -757,8 +757,10 @@ void list() throws IOException {
 
 					final XMLReader xr;
 					try {
-						xr = SAXParserFactory.newInstance().newSAXParser()
-								.getXMLReader();
+						SAXParserFactory saxParserFactory = SAXParserFactory
+								.newInstance();
+						saxParserFactory.setNamespaceAware(true);
+						xr = saxParserFactory.newSAXParser().getXMLReader();
 					} catch (SAXException | ParserConfigurationException e) {
 						throw new IOException(
 								JGitText.get().noXMLParserAvailable, e);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
index 469a3d6..be0d37b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
@@ -12,10 +12,10 @@
 
 package org.eclipse.jgit.transport;
 
-import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DELIM;
 import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN;
 import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_NOT;
 import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DEEPEN_SINCE;
+import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DELIM;
 import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DONE;
 import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_END;
 import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ERR;
@@ -32,7 +32,6 @@
 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.List;
@@ -715,7 +714,7 @@ private void markReachable(Collection<Ref> want, Set<ObjectId> have,
 			// wind up later matching up against things we want and we
 			// can avoid asking for something we already happen to have.
 			//
-			final Date maxWhen = new Date(maxTime * 1000L);
+			Instant maxWhen = Instant.ofEpochSecond(maxTime);
 			walk.sort(RevSort.COMMIT_TIME_DESC);
 			walk.markStart(reachableCommits);
 			walk.setRevFilter(CommitTimeRevFilter.after(maxWhen));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
index 73eddb8..f10b7bf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
@@ -302,8 +302,7 @@ private void init(Config config, URIish uri) {
 		int postBufferSize = config.getInt(HTTP, POST_BUFFER_KEY,
 				1 * 1024 * 1024);
 		boolean sslVerifyFlag = config.getBoolean(HTTP, SSL_VERIFY_KEY, true);
-		HttpRedirectMode followRedirectsMode = config.getEnum(
-				HttpRedirectMode.values(), HTTP, null,
+		HttpRedirectMode followRedirectsMode = config.getEnum(HTTP, null,
 				FOLLOW_REDIRECTS_KEY, HttpRedirectMode.INITIAL);
 		int redirectLimit = config.getInt(HTTP, MAX_REDIRECTS_KEY,
 				MAX_REDIRECTS);
@@ -335,8 +334,8 @@ private void init(Config config, URIish uri) {
 					postBufferSize);
 			sslVerifyFlag = config.getBoolean(HTTP, match, SSL_VERIFY_KEY,
 					sslVerifyFlag);
-			followRedirectsMode = config.getEnum(HttpRedirectMode.values(),
-					HTTP, match, FOLLOW_REDIRECTS_KEY, followRedirectsMode);
+			followRedirectsMode = config.getEnum(HTTP, match,
+					FOLLOW_REDIRECTS_KEY, followRedirectsMode);
 			int newMaxRedirects = config.getInt(HTTP, match, MAX_REDIRECTS_KEY,
 					redirectLimit);
 			if (newMaxRedirects >= 0) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
index 7224405..e1f2b19 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
@@ -530,7 +530,7 @@ public PackLock parse(ProgressMonitor receiving, ProgressMonitor resolving)
 			receiving.beginTask(JGitText.get().receivingObjects,
 					(int) expectedObjectCount);
 			try {
-				for (int done = 0; done < expectedObjectCount; done++) {
+				for (long done = 0; done < expectedObjectCount; done++) {
 					indexOneObject();
 					receiving.update(1);
 					if (receiving.isCancelled())
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 ed33eae..614ad88 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java
@@ -43,24 +43,13 @@ public class PacketLineIn {
 
 	/**
 	 * Magic return from {@link #readString()} when a flush packet is found.
-	 *
-	 * @deprecated Callers should use {@link #isEnd(String)} to check if a
-	 *             string is the end marker, or
-	 *             {@link PacketLineIn#readStrings()} to iterate over all
-	 *             strings in the input stream until the marker is reached.
 	 */
-	@Deprecated
-	public static final String END = new String(); /* must not string pool */
+	private static final String END = new String(); /* must not string pool */
 
 	/**
 	 * Magic return from {@link #readString()} when a delim packet is found.
-	 *
-	 * @since 5.0
-	 * @deprecated Callers should use {@link #isDelimiter(String)} to check if a
-	 *             string is the delimiter.
 	 */
-	@Deprecated
-	public static final String DELIM = new String(); /* must not string pool */
+	private static final String DELIM = new String(); /* must not string pool */
 
 	enum AckNackResult {
 		/** NAK */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java
index a9e93b6..6bdaf0e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java
@@ -24,6 +24,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -329,7 +330,7 @@ public RefUpdate.Result save() throws IOException {
 		if (newId == null) {
 			return RefUpdate.Result.NO_CHANGE;
 		}
-		try (ObjectInserter inserter = db.newObjectInserter()) {
+		try {
 			RefUpdate.Result result = updateRef(newId);
 			switch (result) {
 				case FAST_FORWARD:
@@ -404,8 +405,8 @@ private ObjectId write() throws IOException {
 	}
 
 	private static void sortPending(List<PendingCert> pending) {
-		Collections.sort(pending, (PendingCert a, PendingCert b) -> Long.signum(
-				a.ident.getWhen().getTime() - b.ident.getWhen().getTime()));
+		Collections.sort(pending,
+				Comparator.comparing((PendingCert a) -> a.ident.getWhenAsInstant()));
 	}
 
 	private DirCache newDirCache() throws IOException {
@@ -503,7 +504,7 @@ private static String buildMessage(PushCertificate cert) {
 		} else {
 			sb.append(MessageFormat.format(
 					JGitText.get().storePushCertMultipleRefs,
-					Integer.valueOf(cert.getCommands().size())));
+					cert.getCommands().size()));
 		}
 		return sb.append('\n').toString();
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
index ddde603..6f211e0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
@@ -88,52 +88,6 @@
  * Implements the server side of a push connection, receiving objects.
  */
 public class ReceivePack {
-	/**
-	 * Data in the first line of a request, the line itself plus capabilities.
-	 *
-	 * @deprecated Use {@link FirstCommand} instead.
-	 * @since 5.6
-	 */
-	@Deprecated
-	public static class FirstLine {
-		private final FirstCommand command;
-
-		/**
-		 * Parse the first line of a receive-pack request.
-		 *
-		 * @param line
-		 *            line from the client.
-		 */
-		public FirstLine(String line) {
-			command = FirstCommand.fromLine(line);
-		}
-
-		/**
-		 * Get non-capabilities part of the line
-		 *
-		 * @return non-capabilities part of the line.
-		 */
-		public String getLine() {
-			return command.getLine();
-		}
-
-		/**
-		 * Get capabilities parsed from the line
-		 *
-		 * @return capabilities parsed from the line.
-		 */
-		public Set<String> getCapabilities() {
-			Set<String> reconstructedCapabilites = new HashSet<>();
-			for (Map.Entry<String, String> e : command.getCapabilities()
-					.entrySet()) {
-				String cap = e.getValue() == null ? e.getKey()
-						: e.getKey() + "=" + e.getValue(); //$NON-NLS-1$
-				reconstructedCapabilites.add(cap);
-			}
-
-			return reconstructedCapabilites;
-		}
-	}
 
 	/** Database we write the stored objects into. */
 	private final Repository db;
@@ -2149,22 +2103,6 @@ public void setUnpackErrorHandler(UnpackErrorHandler unpackErrorHandler) {
 	}
 
 	/**
-	 * Set whether this class will report command failures as warning messages
-	 * before sending the command results.
-	 *
-	 * @param echo
-	 *            if true this class will report command failures as warning
-	 *            messages before sending the command results. This is usually
-	 *            not necessary, but may help buggy Git clients that discard the
-	 *            errors when all branches fail.
-	 * @deprecated no widely used Git versions need this any more
-	 */
-	@Deprecated
-	public void setEchoCommandFailures(boolean echo) {
-		// No-op.
-	}
-
-	/**
 	 * Get the client session-id
 	 *
 	 * @return The client session-id.
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 f72c421..3d4bea2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
@@ -178,7 +178,6 @@ public void setUseProtocolV2(boolean b) {
 	 *
 	 * This method must be invoked prior to any of the following:
 	 * <ul>
-	 * <li>{@link #send(Map)}</li>
 	 * <li>{@link #send(Collection)}</li>
 	 * </ul>
 	 *
@@ -195,7 +194,6 @@ public void setDerefTags(boolean deref) {
 	 * <p>
 	 * This method must be invoked prior to any of the following:
 	 * <ul>
-	 * <li>{@link #send(Map)}</li>
 	 * <li>{@link #send(Collection)}</li>
 	 * <li>{@link #advertiseHave(AnyObjectId)}</li>
 	 * </ul>
@@ -230,7 +228,6 @@ public void advertiseCapability(String name, String value) {
 	 * <p>
 	 * This method must be invoked prior to any of the following:
 	 * <ul>
-	 * <li>{@link #send(Map)}</li>
 	 * <li>{@link #send(Collection)}</li>
 	 * <li>{@link #advertiseHave(AnyObjectId)}</li>
 	 * </ul>
@@ -260,24 +257,6 @@ public void addSymref(String from, String to) {
 	 * @throws java.io.IOException
 	 *             the underlying output stream failed to write out an
 	 *             advertisement record.
-	 * @deprecated use {@link #send(Collection)} instead.
-	 */
-	@Deprecated
-	public Set<ObjectId> send(Map<String, Ref> refs) throws IOException {
-		return send(refs.values());
-	}
-
-	/**
-	 * Format an advertisement for the supplied refs.
-	 *
-	 * @param refs
-	 *            zero or more refs to format for the client. The collection is
-	 *            sorted before display if necessary, and therefore may appear
-	 *            in any order.
-	 * @return set of ObjectIds that were advertised to the client.
-	 * @throws java.io.IOException
-	 *             the underlying output stream failed to write out an
-	 *             advertisement record.
 	 * @since 5.0
 	 */
 	public Set<ObjectId> send(Collection<Ref> refs) throws IOException {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
index a0194ea..8120df0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java
@@ -11,8 +11,6 @@
 
 package org.eclipse.jgit.transport;
 
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.util.Iterator;
 import java.util.ServiceLoader;
 
@@ -99,9 +97,8 @@ public static void setInstance(SshSessionFactory newFactory) {
 	 * @since 5.2
 	 */
 	public static String getLocalUserName() {
-		return AccessController
-				.doPrivileged((PrivilegedAction<String>) () -> SystemReader
-						.getInstance().getProperty(Constants.OS_USER_NAME_KEY));
+		return SystemReader.getInstance()
+				.getProperty(Constants.OS_USER_NAME_KEY);
 	}
 
 	/**
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 b335675..ac76e83 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
@@ -1121,28 +1121,6 @@ public void setRemoveDeletedRefs(boolean remove) {
 	}
 
 	/**
-	 * @return the blob limit value set with {@link #setFilterBlobLimit} or
-	 *         {@link #setFilterSpec(FilterSpec)}, or -1 if no blob limit value
-	 *         was set
-	 * @since 5.0
-	 * @deprecated Use {@link #getFilterSpec()} instead
-	 */
-	@Deprecated
-	public final long getFilterBlobLimit() {
-		return filterSpec.getBlobLimit();
-	}
-
-	/**
-	 * @param bytes exclude blobs of size greater than this
-	 * @since 5.0
-	 * @deprecated Use {@link #setFilterSpec(FilterSpec)} instead
-	 */
-	@Deprecated
-	public final void setFilterBlobLimit(long bytes) {
-		setFilterSpec(FilterSpec.withBlobLimit(bytes));
-	}
-
-	/**
 	 * Get filter spec
 	 *
 	 * @return the last filter spec set with {@link #setFilterSpec(FilterSpec)},
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 0fc9710..f77b041 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java
@@ -254,6 +254,12 @@ private ProcessBuilder createProcess(List<String> args,
 				pb.environment().put(Constants.GIT_DIR_KEY,
 						directory.getPath());
 			}
+			File commonDirectory = local != null ? local.getCommonDirectory()
+					: null;
+			if (commonDirectory != null) {
+				pb.environment().put(Constants.GIT_COMMON_DIR_KEY,
+						commonDirectory.getPath());
+			}
 			return pb;
 		}
 
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 3a06ce5..1b9431c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java
@@ -225,6 +225,7 @@ private Process spawn(String cmd,
 			env.remove("GIT_CONFIG"); //$NON-NLS-1$
 			env.remove("GIT_CONFIG_PARAMETERS"); //$NON-NLS-1$
 			env.remove("GIT_DIR"); //$NON-NLS-1$
+			env.remove("GIT_COMMON_DIR"); //$NON-NLS-1$
 			env.remove("GIT_WORK_TREE"); //$NON-NLS-1$
 			env.remove("GIT_GRAFT_FILE"); //$NON-NLS-1$
 			env.remove("GIT_INDEX_FILE"); //$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
index 4de6ff8..7b5842b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
@@ -82,7 +82,7 @@ public class URIish implements Serializable {
 	 * Part of a pattern which matches a relative path. Relative paths don't
 	 * start with slash or drive letters. Defines no capturing group.
 	 */
-	private static final String RELATIVE_PATH_P = "(?:(?:[^\\\\/]+[\\\\/]+)*[^\\\\/]+[\\\\/]*)"; //$NON-NLS-1$
+	private static final String RELATIVE_PATH_P = "(?:(?:[^\\\\/]+[\\\\/]+)*+[^\\\\/]*)"; //$NON-NLS-1$
 
 	/**
 	 * Part of a pattern which matches a relative or absolute path. Defines no
@@ -120,7 +120,7 @@ public class URIish implements Serializable {
 	 * path (maybe even containing windows drive-letters) or a relative path.
 	 */
 	private static final Pattern LOCAL_FILE = Pattern.compile("^" // //$NON-NLS-1$
-			+ "([\\\\/]?" + PATH_P + ")" // //$NON-NLS-1$ //$NON-NLS-2$
+			+ "([\\\\/]?+" + PATH_P + ")" // //$NON-NLS-1$ //$NON-NLS-2$
 			+ "$"); //$NON-NLS-1$
 
 	/**
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 9318871..41ab8ac 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
@@ -30,11 +30,11 @@
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_NO_DONE;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_NO_PROGRESS;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_OFS_DELTA;
+import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SESSION_ID;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SHALLOW;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDEBAND_ALL;
 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_SESSION_ID;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_THIN_PACK;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_WAIT_FOR_DONE;
 import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ACK;
@@ -80,7 +80,6 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.CachedPackUriProvider;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
-import org.eclipse.jgit.internal.transport.parser.FirstWant;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
@@ -118,13 +117,13 @@ public class UploadPack implements Closeable {
 	/** Policy the server uses to validate client requests */
 	public enum RequestPolicy {
 		/** Client may only ask for objects the server advertised a reference for. */
-		ADVERTISED,
+		ADVERTISED(0x08),
 
 		/**
 		 * Client may ask for any commit reachable from a reference advertised by
 		 * the server.
 		 */
-		REACHABLE_COMMIT,
+		REACHABLE_COMMIT(0x02),
 
 		/**
 		 * Client may ask for objects that are the tip of any reference, even if not
@@ -134,18 +133,36 @@ public enum RequestPolicy {
 		 *
 		 * @since 3.1
 		 */
-		TIP,
+		TIP(0x01),
 
 		/**
 		 * Client may ask for any commit reachable from any reference, even if that
-		 * reference wasn't advertised.
+		 * reference wasn't advertised, implies REACHABLE_COMMIT and TIP.
 		 *
 		 * @since 3.1
 		 */
-		REACHABLE_COMMIT_TIP,
+		REACHABLE_COMMIT_TIP(0x03),
 
-		/** Client may ask for any SHA-1 in the repository. */
-		ANY;
+		/** Client may ask for any SHA-1 in the repository, implies REACHABLE_COMMIT_TIP. */
+		ANY(0x07);
+
+		private final int bitmask;
+
+		RequestPolicy(int bitmask) {
+			this.bitmask = bitmask;
+		}
+
+		/**
+		 * Check if the current policy implies another, based on its bitmask.
+		 *
+		 * @param implied
+		 *            the implied policy based on its bitmask.
+		 * @return true if the policy is implied.
+		 * @since 6.10.1
+		 */
+		public boolean implies(RequestPolicy implied) {
+			return (bitmask & implied.bitmask) != 0;
+		}
 	}
 
 	/**
@@ -172,52 +189,6 @@ void checkWants(UploadPack up, List<ObjectId> wants)
 				throws PackProtocolException, IOException;
 	}
 
-	/**
-	 * Data in the first line of a want-list, the line itself plus options.
-	 *
-	 * @deprecated Use {@link FirstWant} instead
-	 */
-	@Deprecated
-	public static class FirstLine {
-
-		private final FirstWant firstWant;
-
-		/**
-		 * @param line
-		 *            line from the client.
-		 */
-		public FirstLine(String line) {
-			try {
-				firstWant = FirstWant.fromLine(line);
-			} catch (PackProtocolException e) {
-				throw new UncheckedIOException(e);
-			}
-		}
-
-		/**
-		 * Get non-capabilities part of the line
-		 *
-		 * @return non-capabilities part of the line.
-		 */
-		public String getLine() {
-			return firstWant.getLine();
-		}
-
-		/**
-		 * Get capabilities parsed from the line
-		 *
-		 * @return capabilities parsed from the line.
-		 */
-		public Set<String> getOptions() {
-			if (firstWant.getAgent() != null) {
-				Set<String> caps = new HashSet<>(firstWant.getCapabilities());
-				caps.add(OPTION_AGENT + '=' + firstWant.getAgent());
-				return caps;
-			}
-			return firstWant.getCapabilities();
-		}
-	}
-
 	/*
 	 * {@link java.util.function.Consumer} doesn't allow throwing checked
 	 * exceptions. Define our own to propagate IOExceptions.
@@ -1423,6 +1394,7 @@ private List<String> getV2CapabilityAdvertisement() {
 		if (transferConfig.isAdvertiseObjectInfo()) {
 			caps.add(COMMAND_OBJECT_INFO);
 		}
+		caps.add(OPTION_AGENT + "=" + UserAgent.get());
 
 		return caps;
 	}
@@ -1629,13 +1601,9 @@ public void sendAdvertisedRefs(RefAdvertiser adv,
 		if (!biDirectionalPipe)
 			adv.advertiseCapability(OPTION_NO_DONE);
 		RequestPolicy policy = getRequestPolicy();
-		if (policy == RequestPolicy.TIP
-				|| policy == RequestPolicy.REACHABLE_COMMIT_TIP
-				|| policy == null)
+		if (policy == null || policy.implies(RequestPolicy.TIP))
 			adv.advertiseCapability(OPTION_ALLOW_TIP_SHA1_IN_WANT);
-		if (policy == RequestPolicy.REACHABLE_COMMIT
-				|| policy == RequestPolicy.REACHABLE_COMMIT_TIP
-				|| policy == null)
+		if (policy == null || policy.implies(RequestPolicy.REACHABLE_COMMIT))
 			adv.advertiseCapability(OPTION_ALLOW_REACHABLE_SHA1_IN_WANT);
 		adv.advertiseCapability(OPTION_AGENT, UserAgent.get());
 		if (transferConfig.isAllowFilter()) {
@@ -1693,18 +1661,6 @@ public int getDepth() {
 	}
 
 	/**
-	 * Deprecated synonym for {@code getFilterSpec().getBlobLimit()}.
-	 *
-	 * @return filter blob limit requested by the client, or -1 if no limit
-	 * @since 5.3
-	 * @deprecated Use {@link #getFilterSpec()} instead
-	 */
-	@Deprecated
-	public final long getFilterBlobLimit() {
-		return getFilterSpec().getBlobLimit();
-	}
-
-	/**
 	 * Returns the filter spec for the current request. Valid only after
 	 * calling recvWants(). This may be a no-op filter spec, but it won't be
 	 * null.
@@ -1996,10 +1952,9 @@ public static final class AdvertisedRequestValidator
 		@Override
 		public void checkWants(UploadPack up, List<ObjectId> wants)
 				throws PackProtocolException, IOException {
-			if (!up.isBiDirectionalPipe())
+			if (!up.isBiDirectionalPipe() || !wants.isEmpty()) {
 				new ReachableCommitRequestValidator().checkWants(up, wants);
-			else if (!wants.isEmpty())
-				throw new WantNotValidException(wants.iterator().next());
+			}
 		}
 	}
 
@@ -2271,7 +2226,7 @@ private boolean wantSatisfied(RevObject want) throws IOException {
 		walk.resetRetain(SAVE);
 		walk.markStart((RevCommit) want);
 		if (oldestTime != 0)
-			walk.setRevFilter(CommitTimeRevFilter.after(oldestTime * 1000L));
+			walk.setRevFilter(CommitTimeRevFilter.after(Instant.ofEpochSecond(oldestTime)));
 		for (;;) {
 			final RevCommit c = walk.next();
 			if (c == null)
@@ -2429,7 +2384,8 @@ else if (ref.getName().startsWith(Constants.R_HEADS))
 						: req.getDepth() - 1;
 				pw.setShallowPack(req.getDepth(), unshallowCommits);
 
-				// Ownership is transferred below
+				// dw borrows the reader from walk which is closed by #close
+				@SuppressWarnings("resource")
 				DepthWalk.RevWalk dw = new DepthWalk.RevWalk(
 						walk.getObjectReader(), walkDepth);
 				dw.setDeepenSince(req.getDeepenSince());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java
index 7b052ad..b23ee97 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java
@@ -10,10 +10,6 @@
 
 package org.eclipse.jgit.transport;
 
-import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
-
-import java.util.Set;
-
 import org.eclipse.jgit.util.StringUtils;
 
 /**
@@ -91,43 +87,6 @@ public static void set(String agent) {
 		userAgent = StringUtils.isEmptyOrNull(agent) ? null : clean(agent);
 	}
 
-	/**
-	 *
-	 * @param options
-	 *            options
-	 * @param transportAgent
-	 *            name of transport agent
-	 * @return The transport agent.
-	 * @deprecated Capabilities with &lt;key&gt;=&lt;value&gt; shape are now
-	 *             parsed alongside other capabilities in the ReceivePack flow.
-	 */
-	@Deprecated
-	static String getAgent(Set<String> options, String transportAgent) {
-		if (options == null || options.isEmpty()) {
-			return transportAgent;
-		}
-		for (String o : options) {
-			if (o.startsWith(OPTION_AGENT)
-					&& o.length() > OPTION_AGENT.length()
-					&& o.charAt(OPTION_AGENT.length()) == '=') {
-				return o.substring(OPTION_AGENT.length() + 1);
-			}
-		}
-		return transportAgent;
-	}
-
-	/**
-	 *
-	 * @param options
-	 *            options
-	 * @return True if the transport agent is set. False otherwise.
-	 * @deprecated Capabilities with &lt;key&gt;=&lt;value&gt; shape are now
-	 *             parsed alongside other capabilities in the ReceivePack flow.
-	 */
-	@Deprecated
-	static boolean hasAgent(Set<String> options) {
-		return getAgent(options, null) != null;
-	}
 
 	private UserAgent() {
 	}
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 3da76f3..b7bb0cb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
@@ -22,8 +22,10 @@
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
 import org.eclipse.jgit.errors.CompoundException;
@@ -122,7 +124,7 @@ class WalkFetchConnection extends BaseFetchConnection {
 	private final Deque<WalkRemoteObjectDatabase> noAlternatesYet;
 
 	/** Packs we have discovered, but have not yet fetched locally. */
-	private final Deque<RemotePack> unfetchedPacks;
+	private final Map<String, RemotePack> unfetchedPacks;
 
 	/**
 	 * Packs whose indexes we have looked at in {@link #unfetchedPacks}.
@@ -164,7 +166,7 @@ class WalkFetchConnection extends BaseFetchConnection {
 		remotes = new ArrayList<>();
 		remotes.add(w);
 
-		unfetchedPacks = new ArrayDeque<>();
+		unfetchedPacks = new LinkedHashMap<>();
 		packsConsidered = new HashSet<>();
 
 		noPacksYet = new ArrayDeque<>();
@@ -227,7 +229,7 @@ public void setPackLockMessage(String message) {
 	public void close() {
 		inserter.close();
 		reader.close();
-		for (RemotePack p : unfetchedPacks) {
+		for (RemotePack p : unfetchedPacks.values()) {
 			if (p.tmpIdx != null)
 				p.tmpIdx.delete();
 		}
@@ -422,8 +424,9 @@ private void downloadObject(ProgressMonitor pm, AnyObjectId id)
 				if (packNameList == null || packNameList.isEmpty())
 					continue;
 				for (String packName : packNameList) {
-					if (packsConsidered.add(packName))
-						unfetchedPacks.add(new RemotePack(wrr, packName));
+					if (packsConsidered.add(packName)) {
+						unfetchedPacks.put(packName, new RemotePack(wrr, packName));
+					}
 				}
 				if (downloadPackedObject(pm, id))
 					return;
@@ -466,15 +469,27 @@ private boolean alreadyHave(AnyObjectId id) throws TransportException {
 		}
 	}
 
+	private boolean downloadPackedObject(ProgressMonitor monitor,
+			AnyObjectId id) throws TransportException {
+		Set<String> brokenPacks = new HashSet<>();
+		try {
+			return downloadPackedObject(monitor, id, brokenPacks);
+		} finally {
+			brokenPacks.forEach(unfetchedPacks::remove);
+		}
+	}
+
 	@SuppressWarnings("Finally")
 	private boolean downloadPackedObject(final ProgressMonitor monitor,
-			final AnyObjectId id) throws TransportException {
+			final AnyObjectId id, Set<String> brokenPacks) throws TransportException {
 		// Search for the object in a remote pack whose index we have,
 		// but whose pack we do not yet have.
 		//
-		final Iterator<RemotePack> packItr = unfetchedPacks.iterator();
-		while (packItr.hasNext() && !monitor.isCancelled()) {
-			final RemotePack pack = packItr.next();
+		for (Entry<String, RemotePack> entry : unfetchedPacks.entrySet()) {
+			if (monitor.isCancelled()) {
+				break;
+			}
+			final RemotePack pack = entry.getValue();
 			try {
 				pack.openIndex(monitor);
 			} catch (IOException err) {
@@ -484,7 +499,7 @@ private boolean downloadPackedObject(final ProgressMonitor monitor,
 				// another source, so don't consider it a failure.
 				//
 				recordError(id, err);
-				packItr.remove();
+				brokenPacks.add(entry.getKey());
 				continue;
 			}
 
@@ -535,7 +550,7 @@ private boolean downloadPackedObject(final ProgressMonitor monitor,
 					}
 					throw new TransportException(e.getMessage(), e);
 				}
-				packItr.remove();
+				brokenPacks.add(entry.getKey());
 			}
 
 			if (!alreadyHave(id)) {
@@ -550,11 +565,9 @@ private boolean downloadPackedObject(final ProgressMonitor monitor,
 
 			// Complete any other objects that we can.
 			//
-			final Iterator<ObjectId> pending = swapFetchQueue();
-			while (pending.hasNext()) {
-				final ObjectId p = pending.next();
+			final Deque<ObjectId> pending = swapFetchQueue();
+			for (ObjectId p : pending) {
 				if (pack.index.hasObject(p)) {
-					pending.remove();
 					process(p);
 				} else {
 					workQueue.add(p);
@@ -566,8 +579,8 @@ private boolean downloadPackedObject(final ProgressMonitor monitor,
 		return false;
 	}
 
-	private Iterator<ObjectId> swapFetchQueue() {
-		final Iterator<ObjectId> r = workQueue.iterator();
+	private Deque<ObjectId> swapFetchQueue() {
+		final Deque<ObjectId> r = workQueue;
 		workQueue = new ArrayDeque<>();
 		return r;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java
index 125ee6c..95b8221 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java
@@ -36,29 +36,39 @@
  */
 public interface HttpConnection {
 	/**
+	 * HttpURLConnection#HTTP_OK
+	 *
 	 * @see HttpURLConnection#HTTP_OK
 	 */
 	int HTTP_OK = java.net.HttpURLConnection.HTTP_OK;
 
 	/**
+	 * HttpURLConnection#HTTP_NOT_AUTHORITATIVE
+	 *
 	 * @see HttpURLConnection#HTTP_NOT_AUTHORITATIVE
 	 * @since 5.8
 	 */
 	int HTTP_NOT_AUTHORITATIVE = java.net.HttpURLConnection.HTTP_NOT_AUTHORITATIVE;
 
 	/**
+	 * HttpURLConnection#HTTP_MOVED_PERM
+	 *
 	 * @see HttpURLConnection#HTTP_MOVED_PERM
 	 * @since 4.7
 	 */
 	int HTTP_MOVED_PERM = java.net.HttpURLConnection.HTTP_MOVED_PERM;
 
 	/**
+	 * HttpURLConnection#HTTP_MOVED_TEMP
+	 *
 	 * @see HttpURLConnection#HTTP_MOVED_TEMP
 	 * @since 4.9
 	 */
 	int HTTP_MOVED_TEMP = java.net.HttpURLConnection.HTTP_MOVED_TEMP;
 
 	/**
+	 * HttpURLConnection#HTTP_SEE_OTHER
+	 *
 	 * @see HttpURLConnection#HTTP_SEE_OTHER
 	 * @since 4.9
 	 */
@@ -85,16 +95,22 @@ public interface HttpConnection {
 	int HTTP_11_MOVED_PERM = 308;
 
 	/**
+	 * HttpURLConnection#HTTP_NOT_FOUND
+	 *
 	 * @see HttpURLConnection#HTTP_NOT_FOUND
 	 */
 	int HTTP_NOT_FOUND = java.net.HttpURLConnection.HTTP_NOT_FOUND;
 
 	/**
+	 * HttpURLConnection#HTTP_UNAUTHORIZED
+	 *
 	 * @see HttpURLConnection#HTTP_UNAUTHORIZED
 	 */
 	int HTTP_UNAUTHORIZED = java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
 
 	/**
+	 * HttpURLConnection#HTTP_FORBIDDEN
+	 *
 	 * @see HttpURLConnection#HTTP_FORBIDDEN
 	 */
 	int HTTP_FORBIDDEN = java.net.HttpURLConnection.HTTP_FORBIDDEN;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
index 36fa720..0cac374 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
@@ -371,12 +371,6 @@ public long getLength() {
 			return attributes.getLength();
 		}
 
-		@Override
-		@Deprecated
-		public long getLastModified() {
-			return attributes.getLastModifiedInstant().toEpochMilli();
-		}
-
 		/**
 		 * @since 5.1.9
 		 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
index aaac2a7..31c216b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
@@ -38,12 +38,12 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
@@ -1587,10 +1587,16 @@ public String getSmudgeCommand(Attributes attributes) throws IOException {
 	 */
 	private String getFilterCommandDefinition(String filterDriverName,
 			String filterCommandType) {
+		if (config == null) {
+			return null;
+		}
 		String key = filterDriverName + "." + filterCommandType; //$NON-NLS-1$
 		String filterCommand = filterCommandsByNameDotType.get(key);
 		if (filterCommand != null)
 			return filterCommand;
+		if (config == null) {
+			return null;
+		}
 		filterCommand = config.getString(ConfigConstants.CONFIG_FILTER_SECTION,
 				filterDriverName, filterCommandType);
 		boolean useBuiltin = config.getBoolean(
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 73a3dda..f16d800 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
@@ -498,6 +498,8 @@ private InputStream filterClean(InputStream in)
 			filterProcessBuilder.directory(repository.getWorkTree());
 			filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY,
 					repository.getDirectory().getAbsolutePath());
+			filterProcessBuilder.environment().put(Constants.GIT_COMMON_DIR_KEY,
+					repository.getCommonDirectory().getAbsolutePath());
 			ExecutionResult result;
 			try {
 				result = fs.execute(filterProcessBuilder, in);
@@ -620,18 +622,6 @@ public long getEntryContentLength() throws IOException {
 	/**
 	 * Get the last modified time of this entry.
 	 *
-	 * @return last modified time of this file, in milliseconds since the epoch
-	 *         (Jan 1, 1970 UTC).
-	 * @deprecated use {@link #getEntryLastModifiedInstant()} instead
-	 */
-	@Deprecated
-	public long getEntryLastModified() {
-		return current().getLastModified();
-	}
-
-	/**
-	 * Get the last modified time of this entry.
-	 *
 	 * @return last modified time of this file
 	 * @since 5.1.9
 	 */
@@ -1229,21 +1219,6 @@ public String toString() {
 		 * needs to compute the value they should cache the reference within an
 		 * instance member instead.
 		 *
-		 * @return time since the epoch (in ms) of the last change.
-		 * @deprecated use {@link #getLastModifiedInstant()} instead
-		 */
-		@Deprecated
-		public abstract long getLastModified();
-
-		/**
-		 * Get the last modified time of this entry.
-		 * <p>
-		 * <b>Note: Efficient implementation required.</b>
-		 * <p>
-		 * The implementation of this method must be efficient. If a subclass
-		 * needs to compute the value they should cache the reference within an
-		 * instance member instead.
-		 *
 		 * @return time of the last change.
 		 * @since 5.1.9
 		 */
@@ -1332,7 +1307,7 @@ IgnoreNode load(IgnoreNode parent) throws IOException {
 
 			IgnoreNode infoExclude = new IgnoreNodeWithParent(
 					coreExclude);
-			File exclude = fs.resolve(repository.getDirectory(),
+			File exclude = fs.resolve(repository.getCommonDirectory(),
 					Constants.INFO_EXCLUDE);
 			if (fs.exists(exclude)) {
 				loadRulesFromFile(infoExclude, exclude);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java
index bcf79a2..33db6ea 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/ByteArraySet.java
@@ -13,12 +13,12 @@
 
 package org.eclipse.jgit.treewalk.filter;
 
-import org.eclipse.jgit.util.RawParseUtils;
-
 import java.util.Arrays;
 import java.util.Set;
 import java.util.stream.Collectors;
 
+import org.eclipse.jgit.util.RawParseUtils;
+
 /**
  * Specialized set for byte arrays, interpreted as strings for use in
  * {@link PathFilterGroup.Group}. Most methods assume the hash is already know
@@ -141,13 +141,19 @@ boolean contains(byte[] toFind, int length, int hash) {
 	}
 
 	/**
+	 * Returns number of arrays in the set
+	 *
 	 * @return number of arrays in the set
 	 */
 	int size() {
 		return size;
 	}
 
-	/** @return true if {@link #size()} is 0. */
+	/**
+	 * Returns true if {@link #size()} is 0
+	 *
+	 * @return true if {@link #size()} is 0
+	 */
 	boolean isEmpty() {
 		return size == 0;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java
index 12af374..c8421d6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java
@@ -86,8 +86,8 @@ public static ObjectId computeChangeId(final ObjectId treeId,
 		}
 	}
 
-	private static final Pattern issuePattern = Pattern
-			.compile("^(Bug|Issue)[a-zA-Z0-9-]*:.*$"); //$NON-NLS-1$
+	private static final Pattern signedOffByPattern = Pattern
+			.compile("^Signed-off-by:.*$"); //$NON-NLS-1$
 
 	private static final Pattern footerPattern = Pattern
 			.compile("(^[a-zA-Z0-9-]+:(?!//).*$)"); //$NON-NLS-1$
@@ -159,7 +159,7 @@ public static String insertId(String message, ObjectId changeId,
 		int footerFirstLine = indexOfFirstFooterLine(lines);
 		int insertAfter = footerFirstLine;
 		for (int i = footerFirstLine; i < lines.length; ++i) {
-			if (issuePattern.matcher(lines[i]).matches()) {
+			if (!signedOffByPattern.matcher(lines[i]).matches()) {
 				insertAfter = i + 1;
 				continue;
 			}
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 a8e1dae..59bbacf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
@@ -30,7 +30,6 @@
 import java.nio.file.Path;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.attribute.FileTime;
-import java.security.AccessControlException;
 import java.text.MessageFormat;
 import java.time.Duration;
 import java.time.Instant;
@@ -262,31 +261,6 @@ public static final class FileStoreAttributes {
 		private static final AtomicInteger threadNumber = new AtomicInteger(1);
 
 		/**
-		 * Don't use the default thread factory of the ForkJoinPool for the
-		 * CompletableFuture; it runs without any privileges, which causes
-		 * trouble if a SecurityManager is present.
-		 * <p>
-		 * Instead use normal daemon threads. They'll belong to the
-		 * SecurityManager's thread group, or use the one of the calling thread,
-		 * as appropriate.
-		 * </p>
-		 *
-		 * @see java.util.concurrent.Executors#newCachedThreadPool()
-		 */
-		private static final ExecutorService FUTURE_RUNNER = new ThreadPoolExecutor(
-				5, 5, 30L, TimeUnit.SECONDS,
-				new LinkedBlockingQueue<>(),
-				runnable -> {
-					Thread t = new Thread(runnable,
-							"JGit-FileStoreAttributeReader-" //$NON-NLS-1$
-							+ threadNumber.getAndIncrement());
-					// Make sure these threads don't prevent application/JVM
-					// shutdown.
-					t.setDaemon(true);
-					return t;
-				});
-
-		/**
 		 * Use a separate executor with at most one thread to synchronize
 		 * writing to the config. We write asynchronously since the config
 		 * itself might be on a different file system, which might otherwise
@@ -463,7 +437,7 @@ private static FileStoreAttributes getFileStoreAttributes(Path dir) {
 								locks.remove(s);
 							}
 							return attributes;
-						}, FUTURE_RUNNER);
+						});
 				f = f.exceptionally(e -> {
 					LOG.error(e.getLocalizedMessage(), e);
 					return Optional.empty();
@@ -898,21 +872,6 @@ public static FS detect() {
 	}
 
 	/**
-	 * Whether FileStore attributes should be determined asynchronously
-	 *
-	 * @param asynch
-	 *            whether FileStore attributes should be determined
-	 *            asynchronously. If false access to cached attributes may block
-	 *            for some seconds for the first call per FileStore
-	 * @since 5.1.9
-	 * @deprecated Use {@link FileStoreAttributes#setBackground} instead
-	 */
-	@Deprecated
-	public static void setAsyncFileStoreAttributes(boolean asynch) {
-		FileStoreAttributes.setBackground(asynch);
-	}
-
-	/**
 	 * Auto-detect the appropriate file system abstraction, taking into account
 	 * the presence of a Cygwin installation on the system. Using jgit in
 	 * combination with Cygwin requires a more elaborate (and possibly slower)
@@ -1085,24 +1044,6 @@ private void detectSymlinkSupport() {
 	 * symbolic links, the modification time of the link is returned, rather
 	 * than that of the link target.
 	 *
-	 * @param f
-	 *            a {@link java.io.File} object.
-	 * @return last modified time of f
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 * @since 3.0
-	 * @deprecated use {@link #lastModifiedInstant(Path)} instead
-	 */
-	@Deprecated
-	public long lastModified(File f) throws IOException {
-		return FileUtils.lastModified(f);
-	}
-
-	/**
-	 * Get the last modified time of a file system object. If the OS/JRE support
-	 * symbolic links, the modification time of the link is returned, rather
-	 * than that of the link target.
-	 *
 	 * @param p
 	 *            a {@link Path} object.
 	 * @return last modified time of p
@@ -1131,25 +1072,6 @@ public Instant lastModifiedInstant(File f) {
 	 * <p>
 	 * For symlinks it sets the modified time of the link target.
 	 *
-	 * @param f
-	 *            a {@link java.io.File} object.
-	 * @param time
-	 *            last modified time
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 * @since 3.0
-	 * @deprecated use {@link #setLastModified(Path, Instant)} instead
-	 */
-	@Deprecated
-	public void setLastModified(File f, long time) throws IOException {
-		FileUtils.setLastModified(f, time);
-	}
-
-	/**
-	 * Set the last modified time of a file system object.
-	 * <p>
-	 * For symlinks it sets the modified time of the link target.
-	 *
 	 * @param p
 	 *            a {@link Path} object.
 	 * @param time
@@ -1443,13 +1365,6 @@ protected static String readPipe(File dir, String[] command,
 			}
 		} catch (IOException e) {
 			LOG.error("Caught exception in FS.readPipe()", e); //$NON-NLS-1$
-		} catch (AccessControlException e) {
-			LOG.warn(MessageFormat.format(
-					JGitText.get().readPipeIsNotAllowedRequiredPermission,
-					command, dir, e.getPermission()));
-		} catch (SecurityException e) {
-			LOG.warn(MessageFormat.format(JGitText.get().readPipeIsNotAllowed,
-					command, dir));
 		}
 		if (debug) {
 			LOG.debug("readpipe returns null"); //$NON-NLS-1$
@@ -1800,25 +1715,6 @@ public void createSymLink(File path, String target) throws IOException {
 	}
 
 	/**
-	 * Create a new file. See {@link java.io.File#createNewFile()}. Subclasses
-	 * of this class may take care to provide a safe implementation for this
-	 * even if {@link #supportsAtomicCreateNewFile()} is <code>false</code>
-	 *
-	 * @param path
-	 *            the file to be created
-	 * @return <code>true</code> if the file was created, <code>false</code> if
-	 *         the file already existed
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 * @deprecated use {@link #createNewFileAtomic(File)} instead
-	 * @since 4.5
-	 */
-	@Deprecated
-	public boolean createNewFile(File path) throws IOException {
-		return path.createNewFile();
-	}
-
-	/**
 	 * A token representing a file created by
 	 * {@link #createNewFileAtomic(File)}. The token must be retained until the
 	 * file has been deleted in order to guarantee that the unique file was
@@ -2042,6 +1938,8 @@ protected ProcessResult internalRunHookIfPresent(Repository repository,
 		environment.put(Constants.GIT_DIR_KEY,
 				repository.getDirectory().getAbsolutePath());
 		if (!repository.isBare()) {
+			environment.put(Constants.GIT_COMMON_DIR_KEY,
+					repository.getCommonDirectory().getAbsolutePath());
 			environment.put(Constants.GIT_WORK_TREE_KEY,
 					repository.getWorkTree().getAbsolutePath());
 		}
@@ -2137,7 +2035,7 @@ private File getRunDirectory(Repository repository,
 		case "post-receive": //$NON-NLS-1$
 		case "post-update": //$NON-NLS-1$
 		case "push-to-checkout": //$NON-NLS-1$
-			return repository.getDirectory();
+			return repository.getCommonDirectory();
 		default:
 			return repository.getWorkTree();
 		}
@@ -2150,7 +2048,7 @@ private File getHooksDirectory(Repository repository) {
 		if (hooksDir != null) {
 			return new File(hooksDir);
 		}
-		File dir = repository.getDirectory();
+		File dir = repository.getCommonDirectory();
 		return dir == null ? null : new File(dir, Constants.HOOKS);
 	}
 
@@ -2424,19 +2322,6 @@ public long getCreationTime() {
 		}
 
 		/**
-		 * Get the time when the file was last modified in milliseconds since
-		 * the epoch
-		 *
-		 * @return the time (milliseconds since 1970-01-01) when this object was
-		 *         last modified
-		 * @deprecated use getLastModifiedInstant instead
-		 */
-		@Deprecated
-		public long getLastModifiedTime() {
-			return lastModifiedInstant.toEpochMilli();
-		}
-
-		/**
 		 * Get the time when this object was last modified
 		 *
 		 * @return the time when this object was last modified
@@ -2578,6 +2463,33 @@ public String normalize(String name) {
 	}
 
 	/**
+	 * Get common dir path.
+	 *
+	 * @param dir
+	 *            the .git folder
+	 * @return common dir path
+	 * @throws IOException
+	 *             if commondir file can't be read
+	 *
+	 * @since 7.0
+	 */
+	public File getCommonDir(File dir) throws IOException {
+		// first the GIT_COMMON_DIR is same as GIT_DIR
+		File commonDir = dir;
+		// now check if commondir file exists (e.g. worktree repository)
+		File commonDirFile = new File(dir, Constants.COMMONDIR_FILE);
+		if (commonDirFile.isFile()) {
+			String commonDirPath = new String(IO.readFully(commonDirFile))
+					.trim();
+			commonDir = new File(commonDirPath);
+			if (!commonDir.isAbsolute()) {
+				commonDir = new File(dir, commonDirPath).getCanonicalFile();
+			}
+		}
+		return commonDir;
+	}
+
+	/**
 	 * This runnable will consume an input stream's content into an output
 	 * stream as soon as it gets available.
 	 * <p>
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 ee907f2..db2b5b4 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, Robin Rosenberg and others
+ * Copyright (C) 2010, 2024, 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
@@ -203,7 +203,16 @@ public boolean supportsExecute() {
 	/** {@inheritDoc} */
 	@Override
 	public boolean canExecute(File f) {
-		return FileUtils.canExecute(f);
+		if (!isFile(f)) {
+			return false;
+		}
+		try {
+			Path path = FileUtils.toPath(f);
+			Set<PosixFilePermission> pset = Files.getPosixFilePermissions(path);
+			return pset.contains(PosixFilePermission.OWNER_EXECUTE);
+		} catch (IOException ex) {
+			return false;
+		}
 	}
 
 	/** {@inheritDoc} */
@@ -332,73 +341,6 @@ public boolean supportsAtomicCreateNewFile() {
 		return supportsAtomicFileCreation == AtomicFileCreation.SUPPORTED;
 	}
 
-	@Override
-	@SuppressWarnings("boxing")
-	/**
-	 * {@inheritDoc}
-	 * <p>
-	 * An implementation of the File#createNewFile() semantics which works also
-	 * on NFS. If the config option
-	 * {@code core.supportsAtomicCreateNewFile = true} (which is the default)
-	 * then simply File#createNewFile() is called.
-	 *
-	 * But if {@code core.supportsAtomicCreateNewFile = false} then after
-	 * successful creation of the lock file a hard link to that lock file is
-	 * created and the attribute nlink of the lock file is checked to be 2. If
-	 * multiple clients manage to create the same lock file nlink would be
-	 * greater than 2 showing the error.
-	 *
-	 * @see "https://www.time-travellers.org/shane/papers/NFS_considered_harmful.html"
-	 *
-	 * @deprecated use {@link FS_POSIX#createNewFileAtomic(File)} instead
-	 * @since 4.5
-	 */
-	@Deprecated
-	public boolean createNewFile(File lock) throws IOException {
-		if (!lock.createNewFile()) {
-			return false;
-		}
-		if (supportsAtomicCreateNewFile()) {
-			return true;
-		}
-		Path lockPath = lock.toPath();
-		Path link = null;
-		FileStore store = null;
-		try {
-			store = Files.getFileStore(lockPath);
-		} catch (SecurityException e) {
-			return true;
-		}
-		try {
-			Boolean canLink = CAN_HARD_LINK.computeIfAbsent(store,
-					s -> Boolean.TRUE);
-			if (Boolean.FALSE.equals(canLink)) {
-				return true;
-			}
-			link = Files.createLink(
-					Paths.get(lock.getAbsolutePath() + ".lnk"), //$NON-NLS-1$
-					lockPath);
-			Integer nlink = (Integer) Files.getAttribute(lockPath,
-					"unix:nlink"); //$NON-NLS-1$
-			if (nlink > 2) {
-				LOG.warn(MessageFormat.format(
-						JGitText.get().failedAtomicFileCreation, lockPath,
-						nlink));
-				return false;
-			} else if (nlink < 2) {
-				CAN_HARD_LINK.put(store, Boolean.FALSE);
-			}
-			return true;
-		} catch (UnsupportedOperationException | IllegalArgumentException e) {
-			CAN_HARD_LINK.put(store, Boolean.FALSE);
-			return true;
-		} finally {
-			if (link != null) {
-				Files.delete(link);
-			}
-		}
-	}
-
 	/**
 	 * {@inheritDoc}
 	 * <p>
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 635351a..2378791 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
@@ -14,8 +14,6 @@
 
 import java.io.File;
 import java.io.OutputStream;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -43,10 +41,7 @@ public class FS_Win32_Cygwin extends FS_Win32 {
 	 * @return true if cygwin is found
 	 */
 	public static boolean isCygwin() {
-		final String path = AccessController
-				.doPrivileged((PrivilegedAction<String>) () -> System
-						.getProperty("java.library.path") //$NON-NLS-1$
-				);
+		final String path = System.getProperty("java.library.path"); //$NON-NLS-1$
 		if (path == null)
 			return false;
 		File found = FS.searchPath(path, "cygpath.exe"); //$NON-NLS-1$
@@ -99,9 +94,7 @@ public File resolve(File dir, String pn) {
 
 	@Override
 	protected File userHomeImpl() {
-		final String home = AccessController.doPrivileged(
-				(PrivilegedAction<String>) () -> System.getenv("HOME") //$NON-NLS-1$
-		);
+		final String home = System.getenv("HOME"); //$NON-NLS-1$
 		if (home == null || home.length() == 0)
 			return super.userHomeImpl();
 		return resolve(new File("."), home); //$NON-NLS-1$
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 cab0e6a..39c67f1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
@@ -771,24 +771,6 @@ static boolean isSymlink(File file) {
 	}
 
 	/**
-	 * Get the lastModified attribute for a given file
-	 *
-	 * @param file
-	 *            the file
-	 * @return lastModified attribute for given file, not following symbolic
-	 *         links
-	 * @throws IOException
-	 *             if an IO error occurred
-	 * @deprecated use {@link #lastModifiedInstant(Path)} instead which returns
-	 *             FileTime
-	 */
-	@Deprecated
-	static long lastModified(File file) throws IOException {
-		return Files.getLastModifiedTime(toPath(file), LinkOption.NOFOLLOW_LINKS)
-				.toMillis();
-	}
-
-	/**
 	 * Get last modified timestamp of a file
 	 *
 	 * @param path
@@ -830,21 +812,6 @@ static BasicFileAttributes fileAttributes(File file) throws IOException {
 	/**
 	 * Set the last modified time of a file system object.
 	 *
-	 * @param file
-	 *            the file
-	 * @param time
-	 *            last modified timestamp
-	 * @throws IOException
-	 *             if an IO error occurred
-	 */
-	@Deprecated
-	static void setLastModified(File file, long time) throws IOException {
-		Files.setLastModifiedTime(toPath(file), FileTime.fromMillis(time));
-	}
-
-	/**
-	 * Set the last modified time of a file system object.
-	 *
 	 * @param path
 	 *            file path
 	 * @param time
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java
index e6bf497..332e659 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java
@@ -10,10 +10,10 @@
 
 package org.eclipse.jgit.util;
 
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
 import java.util.Locale;
-import java.util.TimeZone;
 
 import org.eclipse.jgit.lib.PersonIdent;
 
@@ -26,9 +26,9 @@
  */
 public class GitDateFormatter {
 
-	private DateFormat dateTimeInstance;
+	private DateTimeFormatter dateTimeFormat;
 
-	private DateFormat dateTimeInstance2;
+	private DateTimeFormatter dateTimeFormat2;
 
 	private final Format format;
 
@@ -96,30 +96,34 @@ public GitDateFormatter(Format format) {
 		default:
 			break;
 		case DEFAULT: // Not default:
-			dateTimeInstance = new SimpleDateFormat(
+			dateTimeFormat = DateTimeFormatter.ofPattern(
 					"EEE MMM dd HH:mm:ss yyyy Z", Locale.US); //$NON-NLS-1$
 			break;
 		case ISO:
-			dateTimeInstance = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", //$NON-NLS-1$
+			dateTimeFormat = DateTimeFormatter.ofPattern(
+					"yyyy-MM-dd HH:mm:ss Z", //$NON-NLS-1$
 					Locale.US);
 			break;
 		case LOCAL:
-			dateTimeInstance = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", //$NON-NLS-1$
+			dateTimeFormat = DateTimeFormatter.ofPattern(
+					"EEE MMM dd HH:mm:ss yyyy", //$NON-NLS-1$
 					Locale.US);
 			break;
 		case RFC:
-			dateTimeInstance = new SimpleDateFormat(
+			dateTimeFormat = DateTimeFormatter.ofPattern(
 					"EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); //$NON-NLS-1$
 			break;
 		case SHORT:
-			dateTimeInstance = new SimpleDateFormat("yyyy-MM-dd", Locale.US); //$NON-NLS-1$
+			dateTimeFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd", //$NON-NLS-1$
+					Locale.US);
 			break;
 		case LOCALE:
 		case LOCALELOCAL:
-			SystemReader systemReader = SystemReader.getInstance();
-			dateTimeInstance = systemReader.getDateTimeInstance(
-					DateFormat.DEFAULT, DateFormat.DEFAULT);
-			dateTimeInstance2 = systemReader.getSimpleDateFormat("Z"); //$NON-NLS-1$
+			dateTimeFormat = DateTimeFormatter
+					.ofLocalizedDateTime(FormatStyle.MEDIUM)
+					.withLocale(Locale.US);
+			dateTimeFormat2 = DateTimeFormatter.ofPattern("Z", //$NON-NLS-1$
+					Locale.US);
 			break;
 		}
 	}
@@ -135,39 +139,45 @@ public GitDateFormatter(Format format) {
 	@SuppressWarnings("boxing")
 	public String formatDate(PersonIdent ident) {
 		switch (format) {
-		case RAW:
-			int offset = ident.getTimeZoneOffset();
+		case RAW: {
+			int offset = ident.getZoneOffset().getTotalSeconds();
 			String sign = offset < 0 ? "-" : "+"; //$NON-NLS-1$ //$NON-NLS-2$
 			int offset2;
-			if (offset < 0)
+			if (offset < 0) {
 				offset2 = -offset;
-			else
+			} else {
 				offset2 = offset;
-			int hours = offset2 / 60;
-			int minutes = offset2 % 60;
+			}
+			int minutes = (offset2 / 60) % 60;
+			int hours = offset2 / 60 / 60;
 			return String.format("%d %s%02d%02d", //$NON-NLS-1$
-					ident.getWhen().getTime() / 1000, sign, hours, minutes);
+					ident.getWhenAsInstant().getEpochSecond(), sign, hours,
+					minutes);
+		}
 		case RELATIVE:
-			return RelativeDateFormatter.format(ident.getWhen());
+			return RelativeDateFormatter.format(ident.getWhenAsInstant());
 		case LOCALELOCAL:
 		case LOCAL:
-			dateTimeInstance.setTimeZone(SystemReader.getInstance()
-					.getTimeZone());
-			return dateTimeInstance.format(ident.getWhen());
-		case LOCALE:
-			TimeZone tz = ident.getTimeZone();
-			if (tz == null)
-				tz = SystemReader.getInstance().getTimeZone();
-			dateTimeInstance.setTimeZone(tz);
-			dateTimeInstance2.setTimeZone(tz);
-			return dateTimeInstance.format(ident.getWhen()) + " " //$NON-NLS-1$
-					+ dateTimeInstance2.format(ident.getWhen());
-		default:
-			tz = ident.getTimeZone();
-			if (tz == null)
-				tz = SystemReader.getInstance().getTimeZone();
-			dateTimeInstance.setTimeZone(ident.getTimeZone());
-			return dateTimeInstance.format(ident.getWhen());
+			return dateTimeFormat
+					.withZone(SystemReader.getInstance().getTimeZoneId())
+					.format(ident.getWhenAsInstant());
+		case LOCALE: {
+			ZoneId tz = ident.getZoneId();
+			if (tz == null) {
+				tz = SystemReader.getInstance().getTimeZoneId();
+			}
+			return dateTimeFormat.withZone(tz).format(ident.getWhenAsInstant())
+					+ " " //$NON-NLS-1$
+					+ dateTimeFormat2.withZone(tz)
+							.format(ident.getWhenAsInstant());
+		}
+		default: {
+			ZoneId tz = ident.getZoneId();
+			if (tz == null) {
+				tz = SystemReader.getInstance().getTimeZoneId();
+			}
+			return dateTimeFormat.withZone(tz).format(ident.getWhenAsInstant());
+		}
 		}
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java
index 6a4b396..f080056 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java
@@ -28,7 +28,10 @@
  * used. One example is the parsing of the config parameter gc.pruneexpire. The
  * parser can handle only subset of what native gits approxidate parser
  * understands.
+ *
+ * @deprecated Use {@link GitTimeParser} instead.
  */
+@Deprecated(since = "7.1")
 public class GitDateParser {
 	/**
 	 * The Date representing never. Though this is a concrete value, most
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitTimeParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitTimeParser.java
new file mode 100644
index 0000000..acaa1ce
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitTimeParser.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2024 Christian Halstrick 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.text.ParseException;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.util.EnumMap;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.JGitText;
+
+/**
+ * Parses strings with time and date specifications into
+ * {@link java.time.Instant}.
+ *
+ * When git needs to parse strings specified by the user this parser can be
+ * used. One example is the parsing of the config parameter gc.pruneexpire. The
+ * parser can handle only subset of what native gits approxidate parser
+ * understands.
+ *
+ * @since 7.1
+ */
+public class GitTimeParser {
+
+	private static final Map<ParseableSimpleDateFormat, DateTimeFormatter> formatCache = new EnumMap<>(
+			ParseableSimpleDateFormat.class);
+
+	// An enum of all those formats which this parser can parse with the help of
+	// a DateTimeFormatter. There are other formats (e.g. the relative formats
+	// like "yesterday" or "1 week ago") which this parser can parse but which
+	// are not listed here because they are parsed without the help of a
+	// DateTimeFormatter.
+	enum ParseableSimpleDateFormat {
+		ISO("yyyy-MM-dd HH:mm:ss Z"), // //$NON-NLS-1$
+		RFC("EEE, dd MMM yyyy HH:mm:ss Z"), // //$NON-NLS-1$
+		SHORT("yyyy-MM-dd"), // //$NON-NLS-1$
+		SHORT_WITH_DOTS_REVERSE("dd.MM.yyyy"), // //$NON-NLS-1$
+		SHORT_WITH_DOTS("yyyy.MM.dd"), // //$NON-NLS-1$
+		SHORT_WITH_SLASH("MM/dd/yyyy"), // //$NON-NLS-1$
+		DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), // //$NON-NLS-1$
+		LOCAL("EEE MMM dd HH:mm:ss yyyy"); //$NON-NLS-1$
+
+		private final String formatStr;
+
+		ParseableSimpleDateFormat(String formatStr) {
+			this.formatStr = formatStr;
+		}
+	}
+
+	private GitTimeParser() {
+		// This class is not supposed to be instantiated
+	}
+
+	/**
+	 * Parses a string into a {@link java.time.LocalDateTime} using the default
+	 * locale. Since this parser also supports relative formats (e.g.
+	 * "yesterday") the caller can specify the reference date. These types of
+	 * strings can be parsed:
+	 * <ul>
+	 * <li>"never"</li>
+	 * <li>"now"</li>
+	 * <li>"yesterday"</li>
+	 * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br>
+	 * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of '
+	 * ' one can use '.' to separate the words</li>
+	 * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li>
+	 * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li>
+	 * <li>"yyyy-MM-dd"</li>
+	 * <li>"yyyy.MM.dd"</li>
+	 * <li>"MM/dd/yyyy",</li>
+	 * <li>"dd.MM.yyyy"</li>
+	 * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li>
+	 * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li>
+	 * </ul>
+	 *
+	 * @param dateStr
+	 *            the string to be parsed
+	 * @return the parsed {@link java.time.LocalDateTime}
+	 * @throws java.text.ParseException
+	 *             if the given dateStr was not recognized
+	 */
+	public static LocalDateTime parse(String dateStr) throws ParseException {
+		return parse(dateStr, SystemReader.getInstance().civilNow());
+	}
+
+	/**
+	 * Parses a string into a {@link java.time.Instant} using the default
+	 * locale. Since this parser also supports relative formats (e.g.
+	 * "yesterday") the caller can specify the reference date. These types of
+	 * strings can be parsed:
+	 * <ul>
+	 * <li>"never"</li>
+	 * <li>"now"</li>
+	 * <li>"yesterday"</li>
+	 * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br>
+	 * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of '
+	 * ' one can use '.' to separate the words</li>
+	 * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li>
+	 * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li>
+	 * <li>"yyyy-MM-dd"</li>
+	 * <li>"yyyy.MM.dd"</li>
+	 * <li>"MM/dd/yyyy",</li>
+	 * <li>"dd.MM.yyyy"</li>
+	 * <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li>
+	 * <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li>
+	 * </ul>
+	 *
+	 * @param dateStr
+	 *            the string to be parsed
+	 * @return the parsed {@link java.time.Instant}
+	 * @throws java.text.ParseException
+	 *             if the given dateStr was not recognized
+	 * @since 7.2
+	 */
+	public static Instant parseInstant(String dateStr) throws ParseException {
+		return parse(dateStr).atZone(SystemReader.getInstance().getTimeZoneId())
+				.toInstant();
+	}
+
+	// Only tests seem to use this method
+	static LocalDateTime parse(String dateStr, LocalDateTime now)
+			throws ParseException {
+		dateStr = dateStr.trim();
+
+		if (dateStr.equalsIgnoreCase("never")) { //$NON-NLS-1$
+			return LocalDateTime.MAX;
+		}
+		LocalDateTime ret = parseRelative(dateStr, now);
+		if (ret != null) {
+			return ret;
+		}
+		for (ParseableSimpleDateFormat f : ParseableSimpleDateFormat.values()) {
+			try {
+				return parseSimple(dateStr, f);
+			} catch (DateTimeParseException e) {
+				// simply proceed with the next parser
+			}
+		}
+		ParseableSimpleDateFormat[] values = ParseableSimpleDateFormat.values();
+		StringBuilder allFormats = new StringBuilder("\"") //$NON-NLS-1$
+				.append(values[0].formatStr);
+		for (int i = 1; i < values.length; i++) {
+			allFormats.append("\", \"").append(values[i].formatStr); //$NON-NLS-1$
+		}
+		allFormats.append("\""); //$NON-NLS-1$
+		throw new ParseException(
+				MessageFormat.format(JGitText.get().cannotParseDate, dateStr,
+						allFormats.toString()),
+				0);
+	}
+
+	// tries to parse a string with the formats supported by DateTimeFormatter
+	private static LocalDateTime parseSimple(String dateStr,
+			ParseableSimpleDateFormat f) throws DateTimeParseException {
+		DateTimeFormatter dateFormat = formatCache.computeIfAbsent(f,
+				format -> DateTimeFormatter
+						.ofPattern(f.formatStr)
+						.withLocale(SystemReader.getInstance().getLocale()));
+		TemporalAccessor parsed = dateFormat.parse(dateStr);
+		return parsed.isSupported(ChronoField.HOUR_OF_DAY)
+				? LocalDateTime.from(parsed)
+				: LocalDate.from(parsed).atStartOfDay();
+	}
+
+	// tries to parse a string with a relative time specification
+	@SuppressWarnings("nls")
+	@Nullable
+	private static LocalDateTime parseRelative(String dateStr,
+			LocalDateTime now) {
+		// check for the static words "yesterday" or "now"
+		if (dateStr.equals("now")) {
+			return now;
+		}
+
+		if (dateStr.equals("yesterday")) {
+			return now.minusDays(1);
+		}
+
+		// parse constructs like "3 days ago", "5.week.2.day.ago"
+		String[] parts = dateStr.split("\\.| ", -1);
+		int partsLength = parts.length;
+		// check we have an odd number of parts (at least 3) and that the last
+		// part is "ago"
+		if (partsLength < 3 || (partsLength & 1) == 0
+				|| !parts[parts.length - 1].equals("ago")) {
+			return null;
+		}
+		int number;
+		for (int i = 0; i < parts.length - 2; i += 2) {
+			try {
+				number = Integer.parseInt(parts[i]);
+			} catch (NumberFormatException e) {
+				return null;
+			}
+			if (parts[i + 1] == null) {
+				return null;
+			}
+			switch (parts[i + 1]) {
+			case "year":
+			case "years":
+				now = now.minusYears(number);
+				break;
+			case "month":
+			case "months":
+				now = now.minusMonths(number);
+				break;
+			case "week":
+			case "weeks":
+				now = now.minusWeeks(number);
+				break;
+			case "day":
+			case "days":
+				now = now.minusDays(number);
+				break;
+			case "hour":
+			case "hours":
+				now = now.minusHours(number);
+				break;
+			case "minute":
+			case "minutes":
+				now = now.minusMinutes(number);
+				break;
+			case "second":
+			case "seconds":
+				now = now.minusSeconds(number);
+				break;
+			default:
+				return null;
+			}
+		}
+		return now;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
index 46d0bc8..3ed7251 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
@@ -13,6 +13,8 @@
 
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.time.Instant.EPOCH;
+import static java.time.ZoneOffset.UTC;
 import static org.eclipse.jgit.lib.ObjectChecker.author;
 import static org.eclipse.jgit.lib.ObjectChecker.committer;
 import static org.eclipse.jgit.lib.ObjectChecker.encoding;
@@ -30,6 +32,10 @@
 import java.nio.charset.CodingErrorAction;
 import java.nio.charset.IllegalCharsetNameException;
 import java.nio.charset.UnsupportedCharsetException;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
@@ -44,14 +50,6 @@
  * Handy utility functions to parse raw object contents.
  */
 public final class RawParseUtils {
-	/**
-	 * UTF-8 charset constant.
-	 *
-	 * @since 2.2
-	 * @deprecated use {@link java.nio.charset.StandardCharsets#UTF_8} instead
-	 */
-	@Deprecated
-	public static final Charset UTF8_CHARSET = UTF_8;
 
 	private static final byte[] digits10;
 
@@ -467,6 +465,29 @@ public static final int parseTimeZoneOffset(final byte[] b, int ptr,
 	}
 
 	/**
+	 * Parse a Git style timezone string in [+-]hhmm format
+	 *
+	 * @param b
+	 *            buffer to scan.
+	 * @param ptr
+	 *            position within buffer to start parsing digits at.
+	 * @param ptrResult
+	 *            optional location to return the new ptr value through. If null
+	 *            the ptr value will be discarded.
+	 * @return the ZoneOffset represention of the timezone offset string.
+	 *         Invalid offsets default to UTC.
+	 */
+	private static ZoneId parseZoneOffset(final byte[] b, int ptr,
+			MutableInteger ptrResult) {
+		int hhmm = parseBase10(b, ptr, ptrResult);
+		try {
+			return ZoneOffset.ofHoursMinutes(hhmm / 100, hhmm % 100);
+		} catch (DateTimeException e) {
+			return UTC;
+		}
+	}
+
+	/**
 	 * Locate the first position after a given character.
 	 *
 	 * @param b
@@ -1035,17 +1056,19 @@ public static PersonIdent parsePersonIdent(byte[] raw, int nameB) {
 		// character if there is no trailing LF.
 		final int tzBegin = lastIndexOfTrim(raw, ' ',
 				nextLF(raw, emailE - 1) - 2) + 1;
-		if (tzBegin <= emailE) // No time/zone, still valid
-			return new PersonIdent(name, email, 0, 0);
+		if (tzBegin <= emailE) { // No time/zone, still valid
+			return new PersonIdent(name, email, EPOCH, UTC);
+		}
 
 		final int whenBegin = Math.max(emailE,
 				lastIndexOfTrim(raw, ' ', tzBegin - 1) + 1);
-		if (whenBegin >= tzBegin - 1) // No time/zone, still valid
-			return new PersonIdent(name, email, 0, 0);
+		if (whenBegin >= tzBegin - 1) { // No time/zone, still valid
+			return new PersonIdent(name, email, EPOCH, UTC);
+		}
 
-		final long when = parseLongBase10(raw, whenBegin, null);
-		final int tz = parseTimeZoneOffset(raw, tzBegin);
-		return new PersonIdent(name, email, when * 1000L, tz);
+		long when = parseLongBase10(raw, whenBegin, null);
+		return new PersonIdent(name, email, Instant.ofEpochSecond(when),
+				parseZoneOffset(raw, tzBegin, null));
 	}
 
 	/**
@@ -1083,16 +1106,16 @@ public static PersonIdent parsePersonIdentOnly(final byte[] raw,
 			name = decode(raw, nameB, stop);
 
 		final MutableInteger ptrout = new MutableInteger();
-		long when;
-		int tz;
+		Instant when;
+		ZoneId tz;
 		if (emailE < stop) {
-			when = parseLongBase10(raw, emailE + 1, ptrout);
-			tz = parseTimeZoneOffset(raw, ptrout.value);
+			when = Instant.ofEpochSecond(parseLongBase10(raw, emailE + 1, ptrout));
+			tz = parseZoneOffset(raw, ptrout.value, null);
 		} else {
-			when = 0;
-			tz = 0;
+			when = EPOCH;
+			tz = UTC;
 		}
-		return new PersonIdent(name, email, when * 1000L, tz);
+		return new PersonIdent(name, email, when, tz);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java
index 5611b1e..b6b19e0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java
@@ -10,6 +10,8 @@
 package org.eclipse.jgit.util;
 
 import java.text.MessageFormat;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Date;
 
 import org.eclipse.jgit.internal.JGitText;
@@ -42,12 +44,29 @@ public class RelativeDateFormatter {
 	 * @return age of given {@link java.util.Date} compared to now formatted in
 	 *         the same relative format as returned by
 	 *         {@code git log --relative-date}
+	 * @deprecated Use {@link #format(Instant)} instead.
 	 */
+	@Deprecated(since = "7.2")
 	@SuppressWarnings("boxing")
 	public static String format(Date when) {
+		return format(when.toInstant());
+	}
 
-		long ageMillis = SystemReader.getInstance().getCurrentTime()
-				- when.getTime();
+	/**
+	 * Get age of given {@link java.time.Instant} compared to now formatted in the
+	 * same relative format as returned by {@code git log --relative-date}
+	 *
+	 * @param when
+	 *            an instant to format
+	 * @return age of given instant compared to now formatted in
+	 *         the same relative format as returned by
+	 *         {@code git log --relative-date}
+	 * @since 7.2
+	 */
+	@SuppressWarnings("boxing")
+	public static String format(Instant when) {
+		long ageMillis = Duration
+				.between(when, SystemReader.getInstance().now()).toMillis();
 
 		// shouldn't happen in a perfect world
 		if (ageMillis < 0)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java
index cf06172..e3e3e04 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SignatureUtils.java
@@ -13,8 +13,8 @@
 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.SignatureVerifier.SignatureVerification;
+import org.eclipse.jgit.lib.SignatureVerifier.TrustLevel;
 import org.eclipse.jgit.lib.PersonIdent;
 
 /**
@@ -39,29 +39,34 @@ private SignatureUtils() {
 	 *            to use for dates
 	 * @return a textual representation of the {@link SignatureVerification},
 	 *         using LF as line separator
+	 *
+	 * @since 7.0
 	 */
 	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');
+		if (verification.creationDate() != null) {
+			// Use the creator's timezone for the signature date
+			PersonIdent dateId = new PersonIdent(creator,
+					verification.creationDate().toInstant());
+			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)));
+				verification.keyFingerprint().toUpperCase(Locale.ROOT)));
 		result.append('\n');
-		if (!StringUtils.isEmptyOrNull(verification.getSigner())) {
+		if (!StringUtils.isEmptyOrNull(verification.signer())) {
 			result.append(
 					MessageFormat.format(JGitText.get().verifySignatureIssuer,
-							verification.getSigner()));
+							verification.signer()));
 			result.append('\n');
 		}
 		String msg;
-		if (verification.getVerified()) {
-			if (verification.isExpired()) {
+		if (verification.verified()) {
+			if (verification.expired()) {
 				msg = JGitText.get().verifySignatureExpired;
 			} else {
 				msg = JGitText.get().verifySignatureGood;
@@ -69,14 +74,14 @@ public static String toString(SignatureVerification verification,
 		} else {
 			msg = JGitText.get().verifySignatureBad;
 		}
-		result.append(MessageFormat.format(msg, verification.getKeyUser()));
-		if (!TrustLevel.UNKNOWN.equals(verification.getTrustLevel())) {
+		result.append(MessageFormat.format(msg, verification.keyUser()));
+		if (!TrustLevel.UNKNOWN.equals(verification.trustLevel())) {
 			result.append(' ' + MessageFormat
 					.format(JGitText.get().verifySignatureTrust, verification
-							.getTrustLevel().name().toLowerCase(Locale.ROOT)));
+							.trustLevel().name().toLowerCase(Locale.ROOT)));
 		}
 		result.append('\n');
-		msg = verification.getMessage();
+		msg = verification.message();
 		if (!StringUtils.isEmptyOrNull(msg)) {
 			result.append(msg);
 			result.append('\n');
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
index d957deb..efa6e7dd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Stats.java
@@ -43,14 +43,18 @@ public void add(double x) {
 	}
 
 	/**
-	 * @return number of the added values
+	 * Returns the number of added values
+	 *
+	 * @return  the number of added values
 	 */
 	public int count() {
 		return n;
 	}
 
 	/**
-	 * @return minimum of the added values
+	 * Returns the smallest value added
+	 *
+	 * @return the smallest value added
 	 */
 	public double min() {
 		if (n < 1) {
@@ -60,7 +64,9 @@ public double min() {
 	}
 
 	/**
-	 * @return maximum of the added values
+	 * Returns the biggest value added
+	 *
+	 * @return the biggest value added
 	 */
 	public double max() {
 		if (n < 1) {
@@ -70,9 +76,10 @@ public double max() {
 	}
 
 	/**
-	 * @return average of the added values
+	 * Returns the average of the added values
+	 *
+	 * @return the average of the added values
 	 */
-
 	public double avg() {
 		if (n < 1) {
 			return Double.NaN;
@@ -81,7 +88,9 @@ public double avg() {
 	}
 
 	/**
-	 * @return variance of the added values
+	 * Returns the variance of the added values
+	 *
+	 * @return the variance of the added values
 	 */
 	public double var() {
 		if (n < 2) {
@@ -91,7 +100,9 @@ public double var() {
 	}
 
 	/**
-	 * @return standard deviation of the added values
+	 * Returns the standard deviation of the added values
+	 *
+	 * @return the standard deviation of the added values
 	 */
 	public double stddev() {
 		return Math.sqrt(this.var());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java
index 2fbd12d..e381a3b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java
@@ -278,6 +278,44 @@ public static String join(Collection<String> parts, String separator,
 	}
 
 	/**
+	 * Remove the specified character from beginning and end of a string
+	 * <p>
+	 * If the character repeats, all copies
+	 *
+	 * @param str input string
+	 * @param c character to remove
+	 * @return the input string with c
+	 * @since 7.2
+	 */
+	public static String trim(String str, char c) {
+		if (str == null || str.length() == 0) {
+			return str;
+		}
+
+		int endPos = str.length()-1;
+		while (endPos >= 0 && str.charAt(endPos) == c) {
+			endPos--;
+		}
+
+		// Whole string is c
+		if (endPos == -1) {
+			return EMPTY;
+		}
+
+		int startPos = 0;
+		while (startPos < endPos && str.charAt(startPos) == c) {
+			startPos++;
+		}
+
+		if (startPos == 0 && endPos == str.length()-1) {
+			// No need to copy
+			return str;
+		}
+
+		return str.substring(startPos, endPos+1);
+	}
+
+	/**
 	 * Appends {@link Constants#DOT_GIT_EXT} unless the given name already ends
 	 * with that suffix.
 	 *
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 ed62c71..22b82b3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
@@ -23,10 +23,12 @@
 import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.util.Locale;
 import java.util.TimeZone;
 import java.util.concurrent.atomic.AtomicReference;
@@ -169,6 +171,11 @@ public long getCurrentTime() {
 		}
 
 		@Override
+		public Instant now() {
+			return Instant.now();
+		}
+
+		@Override
 		public int getTimezone(long when) {
 			return getTimeZone().getOffset(when) / (60 * 1000);
 		}
@@ -230,9 +237,19 @@ public long getCurrentTime() {
 		}
 
 		@Override
+		public Instant now() {
+			return delegate.now();
+		}
+
+		@Override
 		public int getTimezone(long when) {
 			return delegate.getTimezone(when);
 		}
+
+		@Override
+		public ZoneOffset getTimeZoneAt(Instant when) {
+			return delegate.getTimeZoneAt(when);
+		}
 	}
 
 	private static volatile SystemReader INSTANCE = DEFAULT;
@@ -503,10 +520,37 @@ private void updateAll(Config config)
 	 * Get the current system time
 	 *
 	 * @return the current system time
+	 *
+	 * @deprecated Use {@link #now()}
 	 */
+	@Deprecated(since = "7.1")
 	public abstract long getCurrentTime();
 
 	/**
+	 * Get the current system time
+	 *
+	 * @return the current system time
+	 *
+	 * @since 7.1
+	 */
+	public Instant now() {
+		// Subclasses overriding getCurrentTime should keep working
+		// TODO(ifrade): Once we remove getCurrentTime, use Instant.now()
+		return Instant.ofEpochMilli(getCurrentTime());
+	}
+
+	/**
+	 * Get "now" as civil time, in the System timezone
+	 *
+	 * @return the current system time
+	 *
+	 * @since 7.1
+	 */
+	public LocalDateTime civilNow() {
+		return LocalDateTime.ofInstant(now(), getTimeZoneId());
+	}
+
+	/**
 	 * Get clock instance preferred by this system.
 	 *
 	 * @return clock instance preferred by this system.
@@ -522,20 +566,48 @@ public MonotonicClock getClock() {
 	 * @param when
 	 *            a system timestamp
 	 * @return the local time zone
+	 *
+	 * @deprecated Use {@link #getTimeZoneAt(Instant)} instead.
 	 */
+	@Deprecated(since = "7.1")
 	public abstract int getTimezone(long when);
 
 	/**
+	 * Get the local time zone offset at "when" time
+	 *
+	 * @param when
+	 *            a system timestamp
+	 * @return the local time zone
+	 * @since 7.1
+	 */
+	public ZoneOffset getTimeZoneAt(Instant when) {
+		return getTimeZoneId().getRules().getOffset(when);
+	}
+
+	/**
 	 * Get system time zone, possibly mocked for testing
 	 *
 	 * @return system time zone, possibly mocked for testing
 	 * @since 1.2
+	 *
+	 * @deprecated Use {@link #getTimeZoneId()}
 	 */
+	@Deprecated(since = "7.1")
 	public TimeZone getTimeZone() {
 		return TimeZone.getDefault();
 	}
 
 	/**
+	 * Get system time zone, possibly mocked for testing
+	 *
+	 * @return system time zone, possibly mocked for testing
+	 * @since 7.1
+	 */
+	public ZoneId getTimeZoneId() {
+		return ZoneId.systemDefault();
+	}
+
+	/**
 	 * Get the locale to use
 	 *
 	 * @return the locale to use
@@ -670,9 +742,7 @@ public boolean isPerformanceTraceEnabled() {
 	}
 
 	private String getOsName() {
-		return AccessController.doPrivileged(
-				(PrivilegedAction<String>) () -> getProperty("os.name") //$NON-NLS-1$
-		);
+		return getProperty("os.name"); //$NON-NLS-1$
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFInputStream.java
index 2385865..4b9706a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFInputStream.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/AutoLFInputStream.java
@@ -147,44 +147,6 @@ public AutoLFInputStream(InputStream in, Set<StreamFlag> flags) {
 				&& flags.contains(StreamFlag.FOR_CHECKOUT);
 	}
 
-	/**
-	 * Creates a new InputStream, wrapping the specified stream.
-	 *
-	 * @param in
-	 *            raw input stream
-	 * @param detectBinary
-	 *            whether binaries should be detected
-	 * @since 2.0
-	 * @deprecated since 5.9, use {@link #create(InputStream, StreamFlag...)}
-	 *             instead
-	 */
-	@Deprecated
-	public AutoLFInputStream(InputStream in, boolean detectBinary) {
-		this(in, detectBinary, false);
-	}
-
-	/**
-	 * Creates a new InputStream, wrapping the specified stream.
-	 *
-	 * @param in
-	 *            raw input stream
-	 * @param detectBinary
-	 *            whether binaries should be detected
-	 * @param abortIfBinary
-	 *            throw an IOException if the file is binary
-	 * @since 3.3
-	 * @deprecated since 5.9, use {@link #create(InputStream, StreamFlag...)}
-	 *             instead
-	 */
-	@Deprecated
-	public AutoLFInputStream(InputStream in, boolean detectBinary,
-			boolean abortIfBinary) {
-		this.in = in;
-		this.detectBinary = detectBinary;
-		this.abortIfBinary = abortIfBinary;
-		this.forCheckout = false;
-	}
-
 	@Override
 	public int read() throws IOException {
 		final int read = read(single, 0, 1);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java
index 4764676..13982b1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/ThrowingPrintWriter.java
@@ -11,8 +11,6 @@
 
 import java.io.IOException;
 import java.io.Writer;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 
 import org.eclipse.jgit.util.SystemReader;
 
@@ -35,10 +33,7 @@ public class ThrowingPrintWriter extends Writer {
 	 */
 	public ThrowingPrintWriter(Writer out) {
 		this.out = out;
-		LF = AccessController
-				.doPrivileged((PrivilegedAction<String>) () -> SystemReader
-						.getInstance().getProperty("line.separator") //$NON-NLS-1$
-				);
+		LF = SystemReader.getInstance().getProperty("line.separator"); //$NON-NLS-1$
 	}
 
 	@Override
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/UnionInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/UnionInputStream.java
index c3a1c4e..7e950f6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/UnionInputStream.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/UnionInputStream.java
@@ -14,7 +14,6 @@
 import java.io.InputStream;
 import java.util.ArrayDeque;
 import java.util.Deque;
-import java.util.Iterator;
 
 /**
  * An InputStream which reads from one or more InputStreams.
@@ -164,14 +163,14 @@ public long skip(long count) throws IOException {
 	public void close() throws IOException {
 		IOException err = null;
 
-		for (Iterator<InputStream> i = streams.iterator(); i.hasNext();) {
+		for (InputStream stream : streams) {
 			try {
-				i.next().close();
+				stream.close();
 			} catch (IOException closeError) {
 				err = closeError;
 			}
-			i.remove();
 		}
+		streams.clear();
 
 		if (err != null)
 			throw err;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java
index a5ee107..a20eaaf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java
@@ -138,7 +138,10 @@ public Instant instant() {
 	 * Get time since epoch, with up to microsecond resolution.
 	 *
 	 * @return time since epoch, with up to microsecond resolution.
+	 *
+	 * @deprecated Use instant() instead
 	 */
+	@Deprecated(since = "7.2")
 	public Timestamp timestamp() {
 		return Timestamp.from(instant());
 	}
@@ -147,7 +150,10 @@ public Timestamp timestamp() {
 	 * Get time since epoch, with up to millisecond resolution.
 	 *
 	 * @return time since epoch, with up to millisecond resolution.
+	 *
+	 * @deprecated Use instant() instead
 	 */
+	@Deprecated(since = "7.2")
 	public Date date() {
 		return new Date(millis());
 	}
diff --git a/pom.xml b/pom.xml
index 590dffa..ecae118 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>7.0.0-SNAPSHOT</version>
+  <version>7.3.0-SNAPSHOT</version>
 
   <name>JGit - Parent</name>
   <url>${jgit-url}</url>
@@ -33,8 +33,8 @@
   </description>
 
   <scm>
-    <url>https://git.eclipse.org/r/plugins/gitiles/jgit/jgit</url>
-    <connection>scm:git:https://git.eclipse.org/r/jgit/jgit</connection>
+    <url>https://eclipse.gerrithub.io/plugins/gitiles/eclipse-jgit/jgit</url>
+    <connection>scm:git:https://eclipse.gerrithub.io/eclipse-jgit/jgit</connection>
   </scm>
 
   <ciManagement>
@@ -95,8 +95,8 @@
   </mailingLists>
 
   <issueManagement>
-    <url>https://bugs.eclipse.org/bugs/buglist.cgi?query_format=advanced;component=JGit;product=JGit;classification=Technology</url>
-    <system>Bugzilla</system>
+    <url>https://github.com/eclipse-jgit/jgit/issues</url>
+    <system>GitHub Issues</system>
   </issueManagement>
 
   <licenses>
@@ -118,37 +118,37 @@
 
     <project.build.outputTimestamp>${commit.time.iso}</project.build.outputTimestamp>
 
-    <jgit-last-release-version>6.9.0.202403050737-r</jgit-last-release-version>
-    <ant-version>1.10.14</ant-version>
-    <apache-sshd-version>2.12.0</apache-sshd-version>
+    <jgit-last-release-version>7.1.0.202411261347-r</jgit-last-release-version>
+    <ant-version>1.10.15</ant-version>
+    <apache-sshd-version>2.15.0</apache-sshd-version>
     <jsch-version>0.1.55</jsch-version>
     <jzlib-version>1.1.3</jzlib-version>
     <javaewah-version>1.2.3</javaewah-version>
     <junit-version>4.13.2</junit-version>
     <test-fork-count>1C</test-fork-count>
-    <args4j-version>2.33</args4j-version>
-    <commons-compress-version>1.26.0</commons-compress-version>
+    <args4j-version>2.37</args4j-version>
+    <commons-compress-version>1.27.1</commons-compress-version>
     <osgi-core-version>6.0.0</osgi-core-version>
-    <servlet-api-version>6.0.0</servlet-api-version>
-    <jetty-version>12.0.9</jetty-version>
-    <japicmp-version>0.18.5</japicmp-version>
+    <servlet-api-version>6.1.0</servlet-api-version>
+    <jetty-version>12.0.16</jetty-version>
+    <japicmp-version>0.23.1</japicmp-version>
     <httpclient-version>4.5.14</httpclient-version>
     <httpcore-version>4.4.16</httpcore-version>
     <slf4j-version>1.7.36</slf4j-version>
-    <maven-javadoc-plugin-version>3.6.3</maven-javadoc-plugin-version>
-    <gson-version>2.10.1</gson-version>
-    <bouncycastle-version>1.77</bouncycastle-version>
-    <spotbugs-maven-plugin-version>4.8.3.1</spotbugs-maven-plugin-version>
-    <maven-project-info-reports-plugin-version>3.5.1</maven-project-info-reports-plugin-version>
-    <maven-jxr-plugin-version>3.3.2</maven-jxr-plugin-version>
-    <maven-surefire-plugin-version>3.2.5</maven-surefire-plugin-version>
+    <maven-javadoc-plugin-version>3.11.2</maven-javadoc-plugin-version>
+    <gson-version>2.12.1</gson-version>
+    <bouncycastle-version>1.80</bouncycastle-version>
+    <spotbugs-maven-plugin-version>4.9.1.0</spotbugs-maven-plugin-version>
+    <maven-project-info-reports-plugin-version>3.8.0</maven-project-info-reports-plugin-version>
+    <maven-jxr-plugin-version>3.6.0</maven-jxr-plugin-version>
+    <maven-surefire-plugin-version>3.5.2</maven-surefire-plugin-version>
     <maven-surefire-report-plugin-version>${maven-surefire-plugin-version}</maven-surefire-report-plugin-version>
-    <maven-compiler-plugin-version>3.12.1</maven-compiler-plugin-version>
+    <maven-compiler-plugin-version>3.14.0</maven-compiler-plugin-version>
     <plexus-compiler-version>2.13.0</plexus-compiler-version>
     <hamcrest-version>2.2</hamcrest-version>
-    <assertj-version>3.25.3</assertj-version>
-    <jna-version>5.14.0</jna-version>
-    <byte-buddy-version>1.14.12</byte-buddy-version>
+    <assertj-version>3.27.3</assertj-version>
+    <jna-version>5.16.0</jna-version>
+    <byte-buddy-version>1.17.1</byte-buddy-version>
 
     <!-- Properties to enable jacoco code coverage analysis -->
     <sonar.core.codeCoveragePlugin>jacoco</sonar.core.codeCoveragePlugin>
@@ -184,7 +184,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-jar-plugin</artifactId>
-          <version>3.3.0</version>
+          <version>3.4.2</version>
           <configuration>
             <archive>
               <manifestEntries>
@@ -208,13 +208,13 @@
 
         <plugin>
           <artifactId>maven-clean-plugin</artifactId>
-          <version>3.3.2</version>
+          <version>3.4.1</version>
         </plugin>
 
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-shade-plugin</artifactId>
-          <version>3.5.1</version>
+          <version>3.6.0</version>
         </plugin>
 
         <plugin>
@@ -226,13 +226,13 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-dependency-plugin</artifactId>
-          <version>3.6.1</version>
+          <version>3.8.1</version>
         </plugin>
 
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-source-plugin</artifactId>
-          <version>3.3.0</version>
+          <version>3.3.1</version>
         </plugin>
 
         <plugin>
@@ -255,7 +255,7 @@
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>build-helper-maven-plugin</artifactId>
-          <version>3.5.0</version>
+          <version>3.6.0</version>
         </plugin>
 
         <plugin>
@@ -277,7 +277,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-pmd-plugin</artifactId>
-          <version>3.21.2</version>
+          <version>3.26.0</version>
           <configuration>
             <inputEncoding>${project.build.sourceEncoding}</inputEncoding>
             <minimumTokens>100</minimumTokens>
@@ -300,17 +300,17 @@
         <plugin>
           <groupId>org.eclipse.cbi.maven.plugins</groupId>
           <artifactId>eclipse-jarsigner-plugin</artifactId>
-          <version>1.4.3</version>
+          <version>1.5.2</version>
         </plugin>
         <plugin>
           <groupId>org.jacoco</groupId>
           <artifactId>jacoco-maven-plugin</artifactId>
-          <version>0.8.11</version>
+          <version>0.8.12</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-site-plugin</artifactId>
-          <version>4.0.0-M13</version>
+          <version>4.0.0-M16</version>
           <dependencies>
             <dependency><!-- add support for ssh/scp -->
               <groupId>org.apache.maven.wagon</groupId>
@@ -337,12 +337,12 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-deploy-plugin</artifactId>
-          <version>3.1.1</version>
+          <version>3.1.3</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-install-plugin</artifactId>
-          <version>3.1.1</version>
+          <version>3.1.3</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
@@ -357,7 +357,7 @@
         <plugin>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-maven-plugin</artifactId>
-          <version>2.7.13</version>
+          <version>3.4.3</version>
         </plugin>
         <plugin>
           <groupId>org.eclipse.dash</groupId>
@@ -367,12 +367,12 @@
         <plugin>
           <groupId>org.cyclonedx</groupId>
           <artifactId>cyclonedx-maven-plugin</artifactId>
-          <version>2.7.10</version>
+          <version>2.9.1</version>
         </plugin>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-artifact-plugin</artifactId>
-          <version>3.5.0</version>
+          <version>3.6.0</version>
           <configuration>
             <ignore>**/*cyclonedx.json</ignore>
             <reproducible>true</reproducible>
@@ -381,7 +381,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-enforcer-plugin</artifactId>
-          <version>3.4.1</version>
+          <version>3.5.0</version>
         </plugin>
       </plugins>
     </pluginManagement>
@@ -623,7 +623,7 @@
       <plugin>
         <groupId>io.github.git-commit-id</groupId>
         <artifactId>git-commit-id-maven-plugin</artifactId>
-        <version>7.0.0</version>
+        <version>9.0.1</version>
         <executions>
           <execution>
             <id>get-the-git-infos</id>
@@ -642,18 +642,18 @@
       <plugin>
         <groupId>org.codehaus.gmavenplus</groupId>
         <artifactId>gmavenplus-plugin</artifactId>
-        <version>3.0.2</version>
+        <version>4.1.1</version>
         <dependencies>
           <dependency>
             <groupId>org.apache.groovy</groupId>
             <artifactId>groovy</artifactId>
-            <version>4.0.15</version>
+            <version>4.0.21</version>
             <scope>runtime</scope>
           </dependency>
           <dependency>
             <groupId>org.apache.groovy</groupId>
             <artifactId>groovy-ant</artifactId>
-            <version>4.0.15</version>
+            <version>4.0.21</version>
             <scope>runtime</scope>
           </dependency>
         </dependencies>
@@ -884,15 +884,33 @@
       </dependency>
 
       <dependency>
+        <groupId>commons-codec</groupId>
+        <artifactId>commons-codec</artifactId>
+        <version>1.18.0</version>
+      </dependency>
+
+      <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-compress</artifactId>
         <version>${commons-compress-version}</version>
       </dependency>
 
       <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-io</artifactId>
+        <version>2.18.0</version>
+      </dependency>
+
+      <dependency>
+        <groupId>commons-logging</groupId>
+        <artifactId>commons-logging</artifactId>
+        <version>1.3.5</version>
+      </dependency>
+
+      <dependency>
         <groupId>org.tukaani</groupId>
         <artifactId>xz</artifactId>
-        <version>1.9</version>
+        <version>1.10</version>
         <optional>true</optional>
       </dependency>
 
@@ -989,7 +1007,7 @@
       <dependency>
         <groupId>org.mockito</groupId>
         <artifactId>mockito-core</artifactId>
-        <version>5.10.0</version>
+        <version>5.15.2</version>
       </dependency>
 
       <dependency>
@@ -1098,7 +1116,7 @@
               <dependency>
                 <groupId>org.eclipse.jdt</groupId>
                 <artifactId>ecj</artifactId>
-                <version>3.36.0</version>
+                <version>3.40.0</version>
               </dependency>
             </dependencies>
           </plugin>
diff --git a/tools/BUILD b/tools/BUILD
index 8c424b3..844f004 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -10,6 +10,7 @@
     java_runtime = "@rules_java//toolchains:remotejdk_17",
     package_configuration = [
         ":error_prone",
+        ":error_prone_tests",
     ],
     source_version = "17",
     target_version = "17",
@@ -22,6 +23,7 @@
     java_runtime = "@rules_java//toolchains:remotejdk_21",
     package_configuration = [
         ":error_prone",
+        ":error_prone_tests",
     ],
     source_version = "21",
     target_version = "21",
@@ -32,9 +34,7 @@
 # enabled. This warnings list is originally based on:
 # https://github.com/bazelbuild/BUILD_file_generator/blob/master/tools/bazel_defs/java.bzl
 # However, feel free to add any additional errors. Thus far they have all been pretty useful.
-java_package_configuration(
-    name = "error_prone",
-    javacopts = [
+errorprone_checks = [
         "-XepDisableWarningsInGeneratedCode",
         # The XepDisableWarningsInGeneratedCode disables only warnings, but
         # not errors. We should manually exclude all files generated by
@@ -422,37 +422,57 @@
         "-Xep:WrongOneof:ERROR",
         "-Xep:XorPower:ERROR",
         "-Xep:ZoneIdOfZ:ERROR",
-    ],
+]
+
+
+exclude_in_tests = ["-Xep:EmptyBlockTag:WARN",
+                    "-Xep:MissingSummary:WARN"]
+
+java_package_configuration(
+    name = "error_prone",
+    javacopts = errorprone_checks,
     packages = ["error_prone_packages"],
 )
 
+java_package_configuration(
+    name = "error_prone_tests",
+    javacopts = [ check for check in errorprone_checks if check not in exclude_in_tests],
+    packages = ["error_prone_packages_test"],
+)
+
 package_group(
     name = "error_prone_packages",
     packages = [
-        "//org.eclipse.jgit.ant.test/...",
         "//org.eclipse.jgit.ant/...",
         "//org.eclipse.jgit.archive/...",
-        "//org.eclipse.jgit.gpg.bc.test/...",
         "//org.eclipse.jgit.gpg.bc/...",
         "//org.eclipse.jgit.http.apache/...",
         "//org.eclipse.jgit.http.server/...",
-        "//org.eclipse.jgit.http.test/...",
         "//org.eclipse.jgit.junit.ssh/...",
         "//org.eclipse.jgit.junit/...",
         "//org.eclipse.jgit.junit/http/...",
-        "//org.eclipse.jgit.lfs.server.test/...",
         "//org.eclipse.jgit.lfs.server/...",
-        "//org.eclipse.jgit.lfs.test/...",
         "//org.eclipse.jgit.lfs/...",
-        "//org.eclipse.jgit.pgm.test/...",
         "//org.eclipse.jgit.pgm/...",
         "//org.eclipse.jgit.ssh.apache.agent/...",
-        "//org.eclipse.jgit.ssh.apache.test/...",
         "//org.eclipse.jgit.ssh.apache/...",
-        "//org.eclipse.jgit.ssh.jsch.test/...",
         "//org.eclipse.jgit.ssh.jsch/...",
-        "//org.eclipse.jgit.test/...",
         "//org.eclipse.jgit.ui/...",
         "//org.eclipse.jgit/...",
     ],
 )
+
+package_group(
+    name = "error_prone_packages_test",
+    packages = [
+        "//org.eclipse.jgit.ant.test/...",
+        "//org.eclipse.jgit.gpg.bc.test/...",
+        "//org.eclipse.jgit.http.test/...",
+        "//org.eclipse.jgit.lfs.server.test/...",
+        "//org.eclipse.jgit.lfs.test/...",
+        "//org.eclipse.jgit.pgm.test/...",
+        "//org.eclipse.jgit.ssh.apache.test/...",
+        "//org.eclipse.jgit.ssh.jsch.test/...",
+        "//org.eclipse.jgit.test/...",
+    ],
+)
diff --git a/tools/workspace_status.py b/tools/workspace_status.py
index ca9e0a9..1186a4a 100644
--- a/tools/workspace_status.py
+++ b/tools/workspace_status.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (C) 2020, David Ostrovsky <david@ostrovsky.org> and others
 #
 # This program and the accompanying materials are made available under the