commit 0be2b64ee8b30c52856e872b37e79e90484af7f9 from: Jelmer Vernooij date: Thu Oct 20 23:55:45 2022 UTC On Windows, provide a hint about developer mode when creating symlinks fails due to a permission error Fixes #1005 commit - 3a0128c1eb5a32eb952a949f06cd55f120858e20 commit + 0be2b64ee8b30c52856e872b37e79e90484af7f9 blob - 0a3226a3feaf808cf7bcf5044ccfec1fa1330d1f blob + 3d71e24bed4cdbbd1dd1e17fc3703ac4e227d678 --- NEWS +++ NEWS @@ -1,4 +1,8 @@ 0.20.47 UNRELEASED + + * On Windows, provide a hint about developer mode + when creating symlinks fails due to a permission + error. (Jelmer Vernooij, #1005) * Support repository format version 1. (Jelmer Vernooij, #1056) blob - f442e4370923f8142c668ec39667ab0128e9ac59 blob + 3a33d3f4cc7d716df3bc38350fad8a8a3c7bc882 --- dulwich/index.py +++ dulwich/index.py @@ -579,10 +579,38 @@ def index_entry_from_stat( flags, extended_flags ) + + +if sys.platform == 'win32': + # On Windows, creating symlinks either requires administrator privileges + # or developer mode. Raise a more helpful error when we're unable to + # create symlinks + + # https://github.com/jelmer/dulwich/issues/1005 + + class WindowsSymlinkPermissionError(PermissionError): + + def __init__(self, errno, msg, filename): + super(PermissionError, self).__init__( + errno, "Unable to create symlink; " + "do you have developer mode enabled? %s" % msg, + filename) + + def symlink(src, dst, target_is_directory=False, *, dir_fd=None): + try: + return os.symlink( + src, dst, target_is_directory=target_is_directory, + dir_fd=dir_fd) + except PermissionError as e: + raise WindowsSymlinkPermissionError( + e.errno, e.strerror, e.filename) from e +else: + symlink = os.symlink def build_file_from_blob( - blob, mode, target_path, honor_filemode=True, tree_encoding="utf-8" + blob, mode, target_path, *, honor_filemode=True, tree_encoding="utf-8", + symlink_fn=None ): """Build a file or symlink on disk based on a Git object. @@ -592,6 +620,7 @@ def build_file_from_blob( target_path: Path to write to honor_filemode: An optional flag to honor core.filemode setting in config file, default is core.filemode=True, change executable bit + symlink: Function to use for creating symlinks Returns: stat object for the file """ try: @@ -607,7 +636,7 @@ def build_file_from_blob( # os.readlink on Python3 on Windows requires a unicode string. contents = contents.decode(tree_encoding) target_path = target_path.decode(tree_encoding) - os.symlink(contents, target_path) + (symlink_fn or symlink)(contents, target_path) else: if oldstat is not None and oldstat.st_size == len(contents): with open(target_path, "rb") as f: @@ -657,6 +686,7 @@ def build_index_from_tree( tree_id, honor_filemode=True, validate_path_element=validate_path_element_default, + symlink_fn=None ): """Generate and materialize index from a tree @@ -695,7 +725,9 @@ def build_index_from_tree( else: obj = object_store[entry.sha] st = build_file_from_blob( - obj, entry.mode, full_path, honor_filemode=honor_filemode + obj, entry.mode, full_path, + honor_filemode=honor_filemode, + symlink_fn=symlink_fn, ) # Add file to index blob - 850a859b476711d16accda12819b23bfa4bd479f blob + 2a397a75c225d04b76d94183dc20829ddd6d475d --- dulwich/porcelain.py +++ dulwich/porcelain.py @@ -1875,7 +1875,8 @@ def update_head(repo, target, detached=False, new_bran r.refs.set_symbolic_ref(b"HEAD", to_set) -def reset_file(repo, file_path: str, target: bytes = b'HEAD'): +def reset_file(repo, file_path: str, target: bytes = b'HEAD', + symlink_fn=None): """Reset the file to specific commit or branch. Args: @@ -1890,7 +1891,7 @@ def reset_file(repo, file_path: str, target: bytes = b full_path = os.path.join(repo.path.encode(), file_path) blob = repo.object_store[file_entry[1]] mode = file_entry[0] - build_file_from_blob(blob, mode, full_path) + build_file_from_blob(blob, mode, full_path, symlink_fn=symlink_fn) def check_mailmap(repo, contact): blob - 2ee88e6034819d94cc4c9d1f833c6b8f1585ef3d blob + 0fa4b1fd483b2f59fa583f53a414f04a50323967 --- dulwich/repo.py +++ dulwich/repo.py @@ -337,6 +337,9 @@ class ParentsProvider(object): class BaseRepo(object): """Base class for a git repository. + This base class is meant to be used for Repository implementations that e.g. + work on top of a different transport than a standard filesystem path. + Attributes: object_store: Dictionary-like object for accessing the objects @@ -1095,6 +1098,7 @@ class Repo(BaseRepo): object_store: Optional[BaseObjectStore] = None, bare: Optional[bool] = None ) -> None: + self.symlink_fn = None hidden_path = os.path.join(root, CONTROLDIR) if bare is None: if (os.path.isfile(hidden_path) @@ -1568,6 +1572,7 @@ class Repo(BaseRepo): tree, honor_filemode=honor_filemode, validate_path_element=validate_path_element, + symlink_fn=self.symlink_fn, ) def get_config(self) -> "ConfigFile": blob - 7d3a784c21ef83ef16dea99da7ab608c060ca8a7 blob + 920f344f426e676cf6547400a3e9c8e07a612339 --- dulwich/tests/test_index.py +++ dulwich/tests/test_index.py @@ -70,10 +70,6 @@ def can_symlink(): if sys.platform != "win32": # Platforms other than Windows should allow symlinks without issues. return True - - if not hasattr(os, "symlink"): - # Older Python versions do not have `os.symlink` on Windows. - return False test_source = tempfile.mkdtemp() test_target = test_source + "can_symlink"