commit f14106a58b7a37a74a99c09372d0a9f941cd21ef from: Jelmer Vernooij via: GitHub date: Thu Apr 27 16:03:31 2023 UTC Merge pull request #1171 from jelmer/core-symlinks Support core.symlink=false commit - d40c5cfe2c07824595c5d582d3268a1a322027dd commit + f14106a58b7a37a74a99c09372d0a9f941cd21ef blob - 343b6117ebe64d95a55a143ce494976a471fdf46 blob + 3c0638f7ffed3e6fef90dfefef03003ccfb5c9e2 --- NEWS +++ NEWS @@ -1,4 +1,6 @@ 0.21.4 UNRELEASED + + * Support ``core.symlinks=false``. (Jelmer Vernooij, #1169) * Deprecate ``dulwich.objects.parse_commit``. blob - 8d15cc8f255770cde66c029122e1e80b37e2bd3f blob + d63523f094eb699f1907e5def4aedb4e6b5c6482 --- dulwich/porcelain.py +++ dulwich/porcelain.py @@ -435,12 +435,13 @@ def commit_tree(repo, tree, message=None, author=None, ) -def init(path=".", bare=False): +def init(path=".", *, bare=False, symlinks: Optional[bool] = None): """Create a new git repository. Args: path: Path to repository. bare: Whether to create a bare repository. + symlinks: Whether to create actual symlinks (defaults to autodetect) Returns: A Repo instance """ if not os.path.exists(path): @@ -449,7 +450,7 @@ def init(path=".", bare=False): if bare: return Repo.init_bare(path) else: - return Repo.init(path) + return Repo.init(path, symlinks=symlinks) def clone( blob - 4450dce43695532c0763f0c266909ca0a185f966 blob + d24f733c1d138e2d160301093c4e1c7931d4ae97 --- dulwich/repo.py +++ dulwich/repo.py @@ -328,7 +328,14 @@ class BaseRepo: """ raise NotImplementedError(self._determine_file_mode) - def _init_files(self, bare: bool) -> None: + def _determine_symlinks(self) -> bool: + """Probe the filesystem to determine whether symlinks can be created. + + Returns: True if symlinks can be created, False otherwise. + """ + raise NotImplementedError(self._determine_symlinks) + + def _init_files(self, *, bare: bool, symlinks: Optional[bool] = None) -> None: """Initialize a default set of named files.""" from .config import ConfigFile @@ -341,6 +348,12 @@ class BaseRepo: else: cf.set("core", "filemode", False) + if symlinks is None and not bare: + symlinks = self._determine_symlinks() + + if symlinks is False: + cf.set("core", "symlinks", symlinks) + cf.set("core", "bare", bare) cf.set("core", "logallrefupdates", True) cf.write_to_file(f) @@ -1069,7 +1082,6 @@ class Repo(BaseRepo): object_store: Optional[PackBasedObjectStore] = 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) @@ -1234,6 +1246,14 @@ class Repo(BaseRepo): return mode_differs and st2_has_exec + def _determine_symlinks(self): + """Probe the filesystem to determine whether symlinks can be created. + + Returns: True if symlinks can be created, False otherwise. + """ + # TODO(jelmer): Actually probe disk / look at filesystem + return sys.platform != "win32" + def _put_named_file(self, path, contents): """Write a file to the control dir with the given name and contents. @@ -1418,6 +1438,7 @@ class Repo(BaseRepo): def clone( self, target_path, + *, mkdir=True, bare=False, origin=b"origin", @@ -1425,7 +1446,8 @@ class Repo(BaseRepo): branch=None, progress=None, depth=None, - ): + symlinks=None, + ) -> "Repo": """Clone this repository. Args: @@ -1439,6 +1461,7 @@ class Repo(BaseRepo): instead of this repository's HEAD. progress: Optional progress function depth: Depth at which to fetch + symlinks: Symlinks setting (default to autodetect) Returns: Created repository as `Repo` """ @@ -1448,9 +1471,8 @@ class Repo(BaseRepo): os.mkdir(target_path) try: - target = None if not bare: - target = Repo.init(target_path) + target = Repo.init(target_path, symlinks=symlinks) if checkout is None: checkout = True else: @@ -1458,48 +1480,50 @@ class Repo(BaseRepo): raise ValueError("checkout and bare are incompatible") target = Repo.init_bare(target_path) - target_config = target.get_config() - target_config.set((b"remote", origin), b"url", encoded_path) - target_config.set( - (b"remote", origin), - b"fetch", - b"+refs/heads/*:refs/remotes/" + origin + b"/*", - ) - target_config.write_to_path() + try: + target_config = target.get_config() + target_config.set((b"remote", origin), b"url", encoded_path) + target_config.set( + (b"remote", origin), + b"fetch", + b"+refs/heads/*:refs/remotes/" + origin + b"/*", + ) + target_config.write_to_path() - ref_message = b"clone: from " + encoded_path - self.fetch(target, depth=depth) - target.refs.import_refs( - b"refs/remotes/" + origin, - self.refs.as_dict(b"refs/heads"), - message=ref_message, - ) - target.refs.import_refs( - b"refs/tags", self.refs.as_dict(b"refs/tags"), message=ref_message - ) - - head_chain, origin_sha = self.refs.follow(b"HEAD") - origin_head = head_chain[-1] if head_chain else None - if origin_sha and not origin_head: - # set detached HEAD - target.refs[b"HEAD"] = origin_sha - else: - _set_origin_head(target.refs, origin, origin_head) - head_ref = _set_default_branch( - target.refs, origin, origin_head, branch, ref_message + ref_message = b"clone: from " + encoded_path + self.fetch(target, depth=depth) + target.refs.import_refs( + b"refs/remotes/" + origin, + self.refs.as_dict(b"refs/heads"), + message=ref_message, ) + target.refs.import_refs( + b"refs/tags", self.refs.as_dict(b"refs/tags"), message=ref_message + ) - # Update target head - if head_ref: - head = _set_head(target.refs, head_ref, ref_message) + head_chain, origin_sha = self.refs.follow(b"HEAD") + origin_head = head_chain[-1] if head_chain else None + if origin_sha and not origin_head: + # set detached HEAD + target.refs[b"HEAD"] = origin_sha else: - head = None + _set_origin_head(target.refs, origin, origin_head) + head_ref = _set_default_branch( + target.refs, origin, origin_head, branch, ref_message + ) - if checkout and head is not None: - target.reset_index() - except BaseException: - if target is not None: + # Update target head + if head_ref: + head = _set_head(target.refs, head_ref, ref_message) + else: + head = None + + if checkout and head is not None: + target.reset_index() + except BaseException: target.close() + raise + except BaseException: if mkdir: import shutil shutil.rmtree(target_path) @@ -1513,6 +1537,7 @@ class Repo(BaseRepo): tree: Tree SHA to reset to, None for current HEAD tree. """ from .index import (build_index_from_tree, + symlink, validate_path_element_default, validate_path_element_ntfs) @@ -1528,6 +1553,12 @@ class Repo(BaseRepo): validate_path_element = validate_path_element_ntfs else: validate_path_element = validate_path_element_default + if config.get_boolean(b"core", b"symlinks", True): + symlink_fn = symlink + else: + def symlink_fn(source, target): # type: ignore + with open(target, 'w' + ('b' if isinstance(source, bytes) else '')) as f: + f.write(source) return build_index_from_tree( self.path, self.index_path(), @@ -1535,7 +1566,7 @@ class Repo(BaseRepo): tree, honor_filemode=honor_filemode, validate_path_element=validate_path_element, - symlink_fn=self.symlink_fn, + symlink_fn=symlink_fn ) def get_worktree_config(self) -> "ConfigFile": @@ -1590,7 +1621,7 @@ class Repo(BaseRepo): @classmethod def _init_maybe_bare( cls, path, controldir, bare, object_store=None, config=None, - default_branch=None): + default_branch=None, symlinks: Optional[bool] = None): for d in BASE_DIRECTORIES: os.mkdir(os.path.join(controldir, *d)) if object_store is None: @@ -1605,11 +1636,11 @@ class Repo(BaseRepo): except KeyError: default_branch = DEFAULT_BRANCH ret.refs.set_symbolic_ref(b"HEAD", LOCAL_BRANCH_PREFIX + default_branch) - ret._init_files(bare) + ret._init_files(bare=bare, symlinks=symlinks) return ret @classmethod - def init(cls, path: str, *, mkdir: bool = False, config=None, default_branch=None) -> "Repo": + def init(cls, path: str, *, mkdir: bool = False, config=None, default_branch=None, symlinks: Optional[bool] = None) -> "Repo": """Create a new repository. Args: @@ -1624,7 +1655,8 @@ class Repo(BaseRepo): _set_filesystem_hidden(controldir) return cls._init_maybe_bare( path, controldir, False, config=config, - default_branch=default_branch) + default_branch=default_branch, + symlinks=symlinks) @classmethod def _init_new_working_directory(cls, path, main_repo, identifier=None, mkdir=False): @@ -1741,6 +1773,13 @@ class MemoryRepo(BaseRepo): """ return sys.platform != "win32" + def _determine_symlinks(self): + """Probe the file-system to determine whether permissions can be trusted. + + Returns: True if permissions can be trusted, False otherwise. + """ + return sys.platform != "win32" + def _put_named_file(self, path, contents): """Write a file to the control dir with the given name and contents. blob - f48fa7642a02e46b16cf61ede8cdce25ffce0b3f blob + 8d84b605ba169fd9f684cdeaf4c106fdf5e583e6 --- dulwich/tests/test_repository.py +++ dulwich/tests/test_repository.py @@ -429,7 +429,44 @@ class RepositoryRootTests(TestCase): tmp_dir = self.mkdtemp() self.addCleanup(shutil.rmtree, tmp_dir) r.clone(tmp_dir, mkdir=False, bare=True) + + def test_reset_index_symlink_enabled(self): + if sys.platform == 'win32': + self.skipTest("symlinks are not supported on Windows") + tmp_dir = self.mkdtemp() + self.addCleanup(shutil.rmtree, tmp_dir) + + o = Repo.init(os.path.join(tmp_dir, "s"), mkdir=True) + os.symlink("foo", os.path.join(tmp_dir, "s", "bar")) + o.stage("bar") + o.do_commit(b"add symlink") + t = o.clone(os.path.join(tmp_dir, "t"), symlinks=True) + o.close() + bar_path = os.path.join(tmp_dir, 't', 'bar') + if sys.platform == 'win32': + with open(bar_path, 'r') as f: + self.assertEqual('foo', f.read()) + else: + self.assertEqual('foo', os.readlink(bar_path)) + t.close() + + def test_reset_index_symlink_disabled(self): + tmp_dir = self.mkdtemp() + self.addCleanup(shutil.rmtree, tmp_dir) + + o = Repo.init(os.path.join(tmp_dir, "s"), mkdir=True) + o.close() + os.symlink("foo", os.path.join(tmp_dir, "s", "bar")) + o.stage("bar") + o.do_commit(b"add symlink") + + t = o.clone(os.path.join(tmp_dir, "t"), symlinks=False) + with open(os.path.join(tmp_dir, "t", 'bar'), 'r') as f: + self.assertEqual('foo', f.read()) + + t.close() + def test_clone_bare(self): r = self.open_repo("a.git") tmp_dir = self.mkdtemp()