diff --git a/lib/bb/fetch2/__init__.py b/lib/bb/fetch2/__init__.py
index f7d5dfe9..a697daa5 100644
--- a/lib/bb/fetch2/__init__.py
+++ b/lib/bb/fetch2/__init__.py
@@ -485,6 +485,10 @@ def uri_replace(ud, uri_find, uri_replace, replacements, d, mirrortarball=None):
             # User/password in the replacement is just a straight replacement
             result_decoded[loc] = uri_replace_decoded[loc]
         elif (re.match(regexp, uri_decoded[loc])):
+            # Snapshot the replacement template before placeholder substitution
+            # so we can later detect whether the user explicitly asked for the
+            # source PATH to be preserved (YOCTO #14156).
+            replace_template = uri_replace_decoded[loc] if loc == 2 else None
             if not uri_replace_decoded[loc]:
                 result_decoded[loc] = ""
             else:
@@ -495,7 +499,23 @@ def uri_replace(ud, uri_find, uri_replace, replacements, d, mirrortarball=None):
             if loc == 2:
                 # Handle path manipulations
                 basename = None
-                if uri_decoded[0] != uri_replace_decoded[0] and mirrortarball:
+                # If the replacement template references the PATH placeholder
+                # the user has explicitly asked for the source path to be
+                # preserved in the rewritten URL (e.g. a cross-scheme PREMIRROR
+                # redirect to a checkout location, not a tarball mirror). In
+                # that case, suppress the mirrortarball-basename override and
+                # carry the source URL parameters through (YOCTO #14156).
+                # The substring check is on the pre-substitution template, so a
+                # source URL whose path coincidentally contains "PATH" will not
+                # trigger a false positive. Cross-scheme rewrites that rely on
+                # an absolute literal target path (no PATH placeholder) still
+                # take the mirrortarball-basename path; widening to those is
+                # left for a follow-up if a real-world rule turns up.
+                user_path_explicit = (replace_template is not None
+                                      and "PATH" in replace_template)
+                if (uri_decoded[0] != uri_replace_decoded[0]
+                        and mirrortarball
+                        and not user_path_explicit):
                     # If the source and destination url types differ, must be a mirrortarball mapping
                     basename = os.path.basename(mirrortarball)
                     # Kill parameters, they make no sense for mirror tarballs
diff --git a/lib/bb/tests/fetch.py b/lib/bb/tests/fetch.py
index 86dd9299..66cb2d97 100644
--- a/lib/bb/tests/fetch.py
+++ b/lib/bb/tests/fetch.py
@@ -566,6 +566,27 @@ class MirrorUriTest(FetcherTest):
                                 'https://bbbb/B/B/B/bitbake/bitbake-1.0.tar.gz',
                                 'http://aaaa/A/A/A/B/B/bitbake/bitbake-1.0.tar.gz'])
 
+    def test_gitsm_premirror_path_placeholder(self):
+        # YOCTO #14156: when a PREMIRROR rewrites a gitsm:// URL to a
+        # different scheme but uses the HOST/PATH placeholders to redirect
+        # to a checkout location (not a tarball), the rewritten URL must
+        # land on the user-specified checkout path, not on the canonical
+        # mirror-tarball filename. niqingliang2003 reported (2025-07-18) a
+        # case where bitbake produced
+        # 'git:///mnt/datum/repositories/github.com/libjxl/git2_github.com.libjxl.libjxl.git.tar.gz'
+        # for a PREMIRROR rule of
+        # 'gitsm://.*/.* git:///mnt/datum/repositories/HOST/PATH;protocol=file',
+        # which is unfetchable.
+        src = "gitsm://github.com/libjxl/libjxl.git;protocol=https;nobranch=1;rev=" + ("1" * 40)
+        find = "gitsm://.*/.*"
+        replace = "git:///mnt/datum/repositories/HOST/PATH;protocol=file"
+        expected = "git:///mnt/datum/repositories/github.com/libjxl/libjxl.git;protocol=file;nobranch=1;rev=" + ("1" * 40)
+        ud = bb.fetch.FetchData(src, self.d)
+        ud.setup_localpath(self.d)
+        mirrors = bb.fetch2.mirror_from_string("%s %s" % (find, replace))
+        newuris, _ = bb.fetch2.build_mirroruris(ud, mirrors, self.d)
+        self.assertEqual([expected], newuris)
+
 
 class GitDownloadDirectoryNamingTest(FetcherTest):
     def setUp(self):
