@@ -433,7 +433,7 @@ def clone_repo(self, repo, dest, branch=None, depth=None):
433
433
:param depth: The depth of the clone. Defaults to 1 (shallow clone).
434
434
:return: None
435
435
"""
436
- cmd = [self .options .git_path , "clone" ]
436
+ cmd = [self .options .git_path , "-c" , "core.symlinks=true" , " clone" ]
437
437
if branch :
438
438
cmd .extend (["--branch" , branch ])
439
439
if depth :
@@ -1460,7 +1460,160 @@ def create_cmake_presets(self):
1460
1460
with open (user_presets_path , "w" ) as f :
1461
1461
json .dump (user_presets , f , indent = 4 )
1462
1462
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
+
1463
1614
def install_mrdocs (self ):
1615
+ self .check_git_symlinks (self .options .mrdocs_src_dir )
1616
+
1464
1617
if not self .options .mrdocs_use_user_presets :
1465
1618
self .prompt_option ("mrdocs_build_dir" )
1466
1619
else :
0 commit comments