@@ -433,7 +433,7 @@ def clone_repo(self, repo, dest, branch=None, depth=None):
433433 :param depth: The depth of the clone. Defaults to 1 (shallow clone).
434434 :return: None
435435 """
436- cmd = [self .options .git_path , "clone" ]
436+ cmd = [self .options .git_path , "-c" , "core.symlinks=true" , " clone" ]
437437 if branch :
438438 cmd .extend (["--branch" , branch ])
439439 if depth :
@@ -1460,7 +1460,160 @@ def create_cmake_presets(self):
14601460 with open (user_presets_path , "w" ) as f :
14611461 json .dump (user_presets , f , indent = 4 )
14621462
1463+ def _git_symlink_entries (self , repo_dir ):
1464+ """
1465+ Returns a list of (worktree_path, intended_target_string) for all git-tracked symlinks (mode 120000).
1466+ """
1467+ out = subprocess .check_output (
1468+ [self .options .git_path , "-C" , repo_dir , "ls-files" , "-s" ],
1469+ text = True , encoding = "utf-8" , errors = "replace"
1470+ )
1471+ entries = []
1472+ for line in out .splitlines ():
1473+ # "<mode> <object> <stage>\t<path>"
1474+ # Example for symlink: "120000 e69de29... 0\tpath/to/link"
1475+ try :
1476+ head , path = line .split ("\t " , 1 )
1477+ mode , obj , _stage = head .split ()[:3 ]
1478+ except ValueError :
1479+ continue
1480+ if mode != "120000" :
1481+ continue
1482+ target = subprocess .check_output (
1483+ [self .options .git_path , "-C" , repo_dir , "cat-file" , "-p" , obj ],
1484+ text = True , encoding = "utf-8" , errors = "replace"
1485+ ).rstrip ("\n " )
1486+ entries .append ((path , target ))
1487+ return entries
1488+
1489+ def _same_link_target (self , link_path , intended ):
1490+ """Return True if link_path is a symlink pointing to intended (normalized)."""
1491+ try :
1492+ current = os .readlink (link_path )
1493+ except OSError :
1494+ return False
1495+
1496+ def norm (p ):
1497+ return os .path .normpath (p .replace ("/" , os .sep ))
1498+
1499+ return norm (current ) == norm (intended )
1500+
1501+ def _make_symlink_or_fallback (self , file_path , intended_target , repo_dir ):
1502+ """
1503+ Create a symlink at file_path pointing to intended_target (POSIX path from git).
1504+ Falls back to hardlink/copy on Windows if symlinks aren’t permitted.
1505+ Returns: 'symlink' | 'hardlink' | 'copy'
1506+ """
1507+ parent = os .path .dirname (file_path )
1508+ if parent and not os .path .isdir (parent ):
1509+ os .makedirs (parent , exist_ok = True )
1510+
1511+ # Remove existing non-symlink file
1512+ if os .path .exists (file_path ) and not os .path .islink (file_path ):
1513+ os .remove (file_path )
1514+
1515+ # Git stores POSIX-style link text; translate to native separators for the OS call
1516+ native_target = intended_target .replace ("/" , os .sep )
1517+
1518+ # Detect if the final target (as it would resolve in the WT) is a directory (Windows needs this)
1519+ resolved_target = os .path .normpath (os .path .join (parent , native_target ))
1520+ target_is_dir = os .path .isdir (resolved_target )
1521+
1522+ # Try real symlink first
1523+ try :
1524+ # On Windows, target_is_directory must be correct for directory links
1525+ if os .name == "nt" :
1526+ os .symlink (native_target , file_path , target_is_directory = target_is_dir )
1527+ else :
1528+ os .symlink (native_target , file_path )
1529+ return "symlink"
1530+ except (NotImplementedError , OSError , PermissionError ):
1531+ pass
1532+
1533+ # Fallback: hardlink (files only, same volume)
1534+ try :
1535+ if os .path .isfile (resolved_target ):
1536+ os .link (resolved_target , file_path )
1537+ return "hardlink"
1538+ except OSError :
1539+ pass
1540+
1541+ # Last resort: copy the file contents if it exists
1542+ if os .path .isfile (resolved_target ):
1543+ shutil .copyfile (resolved_target , file_path )
1544+ return "copy"
1545+
1546+ # If the target doesn’t exist in WT, write the intended link text so state is explicit
1547+ with open (file_path , "w" , encoding = "utf-8" ) as f :
1548+ f .write (intended_target )
1549+ return "copy"
1550+
1551+ def _is_git_repo (self , repo_dir ):
1552+ """Return True if repo_dir looks like a Git work tree."""
1553+ if os .path .isdir (os .path .join (repo_dir , ".git" )):
1554+ return True
1555+ try :
1556+ out = subprocess .check_output (
1557+ [self .options .git_path , "-C" , repo_dir , "rev-parse" , "--is-inside-work-tree" ],
1558+ stderr = subprocess .DEVNULL , text = True
1559+ )
1560+ return out .strip () == "true"
1561+ except Exception :
1562+ return False
1563+
1564+ def check_git_symlinks (self , repo_dir ):
1565+ """
1566+ Ensure all Git-tracked symlinks in repo_dir are correct in the working tree.
1567+ Fixes text-file placeholders produced when core.symlinks=false.
1568+ """
1569+ repo_dir = os .path .abspath (repo_dir )
1570+ if not self ._is_git_repo (repo_dir ):
1571+ return
1572+
1573+ symlinks = self ._git_symlink_entries (repo_dir )
1574+ if not symlinks :
1575+ return
1576+
1577+ fixed = {"symlink" : 0 , "hardlink" : 0 , "copy" : 0 , "already_ok" : 0 }
1578+
1579+ for rel_path , intended in symlinks :
1580+ link_path = os .path .join (repo_dir , rel_path )
1581+
1582+ # Already OK?
1583+ if os .path .islink (link_path ) and self ._same_link_target (link_path , intended ):
1584+ fixed ["already_ok" ] += 1
1585+ continue
1586+
1587+ # If it's a regular file that merely contains the target text, replace it anyway
1588+ if os .path .exists (link_path ) and not os .path .islink (link_path ):
1589+ try :
1590+ with open (link_path , "r" , encoding = "utf-8" ) as f :
1591+ content = f .read ().strip ()
1592+ # no-op: we still replace below if content == intended (or even if not)
1593+ except Exception :
1594+ # unreadable is fine; we’ll still replace
1595+ pass
1596+
1597+ kind = self ._make_symlink_or_fallback (link_path , intended , repo_dir )
1598+ fixed [kind ] += 1
1599+
1600+ # Summary + Windows hint
1601+ if (fixed ["symlink" ] + fixed ["hardlink" ] + fixed ["copy" ]) > 0 :
1602+ print (
1603+ f"Repaired Git symlinks in { repo_dir } "
1604+ f"(created: { fixed ['symlink' ]} symlink(s), { fixed ['hardlink' ]} hardlink(s), "
1605+ f"{ fixed ['copy' ]} copy/copies; { fixed ['already_ok' ]} already OK)."
1606+ )
1607+ if fixed ["hardlink" ] or fixed ["copy" ]:
1608+ print (
1609+ "Warning: Some symlinks could not be created. On Windows, enable Developer Mode "
1610+ "or run with privileges that allow creating symlinks. Also ensure "
1611+ "`git config core.symlinks true` before checkout."
1612+ )
1613+
14631614 def install_mrdocs (self ):
1615+ self .check_git_symlinks (self .options .mrdocs_src_dir )
1616+
14641617 if not self .options .mrdocs_use_user_presets :
14651618 self .prompt_option ("mrdocs_build_dir" )
14661619 else :
0 commit comments