Spaces:
Paused
Paused
| # This module is part of GitPython and is released under the | |
| # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ | |
| import gc | |
| from io import BytesIO | |
| import logging | |
| import os | |
| import os.path as osp | |
| import stat | |
| import uuid | |
| import git | |
| from git.cmd import Git | |
| from git.compat import defenc | |
| from git.config import GitConfigParser, SectionConstraint, cp | |
| from git.exc import ( | |
| BadName, | |
| InvalidGitRepositoryError, | |
| NoSuchPathError, | |
| RepositoryDirtyError, | |
| ) | |
| from git.objects.base import IndexObject, Object | |
| from git.objects.util import TraversableIterableObj | |
| from git.util import ( | |
| IterableList, | |
| RemoteProgress, | |
| join_path_native, | |
| rmtree, | |
| to_native_path_linux, | |
| unbare_repo, | |
| ) | |
| from .util import ( | |
| SubmoduleConfigParser, | |
| find_first_remote_branch, | |
| mkhead, | |
| sm_name, | |
| sm_section, | |
| ) | |
| # typing ---------------------------------------------------------------------- | |
| from typing import Callable, Dict, Mapping, Sequence, TYPE_CHECKING, cast | |
| from typing import Any, Iterator, Union | |
| from git.types import Commit_ish, Literal, PathLike, TBD | |
| if TYPE_CHECKING: | |
| from git.index import IndexFile | |
| from git.repo import Repo | |
| from git.refs import Head | |
| # ----------------------------------------------------------------------------- | |
| __all__ = ["Submodule", "UpdateProgress"] | |
| log = logging.getLogger("git.objects.submodule.base") | |
| log.addHandler(logging.NullHandler()) | |
| class UpdateProgress(RemoteProgress): | |
| """Class providing detailed progress information to the caller who should | |
| derive from it and implement the ``update(...)`` message.""" | |
| CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)] | |
| _num_op_codes: int = RemoteProgress._num_op_codes + 3 | |
| __slots__ = () | |
| BEGIN = UpdateProgress.BEGIN | |
| END = UpdateProgress.END | |
| CLONE = UpdateProgress.CLONE | |
| FETCH = UpdateProgress.FETCH | |
| UPDWKTREE = UpdateProgress.UPDWKTREE | |
| # IndexObject comes via the util module. It's a 'hacky' fix thanks to Python's import | |
| # mechanism, which causes plenty of trouble if the only reason for packages and | |
| # modules is refactoring - subpackages shouldn't depend on parent packages. | |
| class Submodule(IndexObject, TraversableIterableObj): | |
| """Implements access to a git submodule. They are special in that their sha | |
| represents a commit in the submodule's repository which is to be checked out | |
| at the path of this instance. | |
| The submodule type does not have a string type associated with it, as it exists | |
| solely as a marker in the tree and index. | |
| All methods work in bare and non-bare repositories. | |
| """ | |
| _id_attribute_ = "name" | |
| k_modules_file = ".gitmodules" | |
| k_head_option = "branch" | |
| k_head_default = "master" | |
| k_default_mode = stat.S_IFDIR | stat.S_IFLNK | |
| """Submodule flags. Submodules are directories with link-status.""" | |
| type: Literal["submodule"] = "submodule" # type: ignore | |
| """This is a bogus type for base class compatibility.""" | |
| __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__") | |
| _cache_attrs = ("path", "_url", "_branch_path") | |
| def __init__( | |
| self, | |
| repo: "Repo", | |
| binsha: bytes, | |
| mode: Union[int, None] = None, | |
| path: Union[PathLike, None] = None, | |
| name: Union[str, None] = None, | |
| parent_commit: Union[Commit_ish, None] = None, | |
| url: Union[str, None] = None, | |
| branch_path: Union[PathLike, None] = None, | |
| ) -> None: | |
| """Initialize this instance with its attributes. | |
| We only document the parameters that differ from | |
| :class:`~git.objects.base.IndexObject`. | |
| :param repo: Our parent repository. | |
| :param binsha: Binary sha referring to a commit in the remote repository. See | |
| the ``url`` parameter. | |
| :param parent_commit: See :meth:`set_parent_commit`. | |
| :param url: The URL to the remote repository which is the submodule. | |
| :param branch_path: Full (relative) path to ref to checkout when cloning the | |
| remote repository. | |
| """ | |
| super().__init__(repo, binsha, mode, path) | |
| self.size = 0 | |
| self._parent_commit = parent_commit | |
| if url is not None: | |
| self._url = url | |
| if branch_path is not None: | |
| # assert isinstance(branch_path, str) | |
| self._branch_path = branch_path | |
| if name is not None: | |
| self._name = name | |
| def _set_cache_(self, attr: str) -> None: | |
| if attr in ("path", "_url", "_branch_path"): | |
| reader: SectionConstraint = self.config_reader() | |
| # Default submodule values. | |
| try: | |
| self.path = reader.get("path") | |
| except cp.NoSectionError as e: | |
| if self.repo.working_tree_dir is not None: | |
| raise ValueError( | |
| "This submodule instance does not exist anymore in '%s' file" | |
| % osp.join(self.repo.working_tree_dir, ".gitmodules") | |
| ) from e | |
| self._url = reader.get("url") | |
| # GitPython extension values - optional. | |
| self._branch_path = reader.get_value(self.k_head_option, git.Head.to_full_path(self.k_head_default)) | |
| elif attr == "_name": | |
| raise AttributeError("Cannot retrieve the name of a submodule if it was not set initially") | |
| else: | |
| super()._set_cache_(attr) | |
| # END handle attribute name | |
| def _get_intermediate_items(cls, item: "Submodule") -> IterableList["Submodule"]: | |
| """:return: all the submodules of our module repository""" | |
| try: | |
| return cls.list_items(item.module()) | |
| except InvalidGitRepositoryError: | |
| return IterableList("") | |
| # END handle intermediate items | |
| def _need_gitfile_submodules(cls, git: Git) -> bool: | |
| return git.version_info[:3] >= (1, 7, 5) | |
| def __eq__(self, other: Any) -> bool: | |
| """Compare with another submodule.""" | |
| # We may only compare by name as this should be the ID they are hashed with. | |
| # Otherwise this type wouldn't be hashable. | |
| # return self.path == other.path and self.url == other.url and super().__eq__(other) | |
| return self._name == other._name | |
| def __ne__(self, other: object) -> bool: | |
| """Compare with another submodule for inequality.""" | |
| return not (self == other) | |
| def __hash__(self) -> int: | |
| """Hash this instance using its logical id, not the sha.""" | |
| return hash(self._name) | |
| def __str__(self) -> str: | |
| return self._name | |
| def __repr__(self) -> str: | |
| return "git.%s(name=%s, path=%s, url=%s, branch_path=%s)" % ( | |
| type(self).__name__, | |
| self._name, | |
| self.path, | |
| self.url, | |
| self.branch_path, | |
| ) | |
| def _config_parser( | |
| cls, repo: "Repo", parent_commit: Union[Commit_ish, None], read_only: bool | |
| ) -> SubmoduleConfigParser: | |
| """ | |
| :return: Config Parser constrained to our submodule in read or write mode | |
| :raise IOError: If the .gitmodules file cannot be found, either locally or in | |
| the repository at the given parent commit. Otherwise the exception would be | |
| delayed until the first access of the config parser. | |
| """ | |
| parent_matches_head = True | |
| if parent_commit is not None: | |
| try: | |
| parent_matches_head = repo.head.commit == parent_commit | |
| except ValueError: | |
| # We are most likely in an empty repository, so the HEAD doesn't point to a valid ref. | |
| pass | |
| # END handle parent_commit | |
| fp_module: Union[str, BytesIO] | |
| if not repo.bare and parent_matches_head and repo.working_tree_dir: | |
| fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file) | |
| else: | |
| assert parent_commit is not None, "need valid parent_commit in bare repositories" | |
| try: | |
| fp_module = cls._sio_modules(parent_commit) | |
| except KeyError as e: | |
| raise IOError( | |
| "Could not find %s file in the tree of parent commit %s" % (cls.k_modules_file, parent_commit) | |
| ) from e | |
| # END handle exceptions | |
| # END handle non-bare working tree | |
| if not read_only and (repo.bare or not parent_matches_head): | |
| raise ValueError("Cannot write blobs of 'historical' submodule configurations") | |
| # END handle writes of historical submodules | |
| return SubmoduleConfigParser(fp_module, read_only=read_only) | |
| def _clear_cache(self) -> None: | |
| """Clear the possibly changed values.""" | |
| for name in self._cache_attrs: | |
| try: | |
| delattr(self, name) | |
| except AttributeError: | |
| pass | |
| # END try attr deletion | |
| # END for each name to delete | |
| def _sio_modules(cls, parent_commit: Commit_ish) -> BytesIO: | |
| """:return: Configuration file as BytesIO - we only access it through the respective blob's data""" | |
| sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read()) | |
| sio.name = cls.k_modules_file | |
| return sio | |
| def _config_parser_constrained(self, read_only: bool) -> SectionConstraint: | |
| """:return: Config Parser constrained to our submodule in read or write mode""" | |
| try: | |
| pc: Union["Commit_ish", None] = self.parent_commit | |
| except ValueError: | |
| pc = None | |
| # END handle empty parent repository | |
| parser = self._config_parser(self.repo, pc, read_only) | |
| parser.set_submodule(self) | |
| return SectionConstraint(parser, sm_section(self.name)) | |
| def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike: | |
| if cls._need_gitfile_submodules(parent_repo.git): | |
| return osp.join(parent_repo.git_dir, "modules", name) | |
| if parent_repo.working_tree_dir: | |
| return osp.join(parent_repo.working_tree_dir, path) | |
| raise NotADirectoryError() | |
| def _clone_repo( | |
| cls, | |
| repo: "Repo", | |
| url: str, | |
| path: PathLike, | |
| name: str, | |
| allow_unsafe_options: bool = False, | |
| allow_unsafe_protocols: bool = False, | |
| **kwargs: Any, | |
| ) -> "Repo": | |
| """ | |
| :return: Repo instance of newly cloned repository | |
| :param repo: Our parent repository | |
| :param url: URL to clone from | |
| :param path: Repository - relative path to the submodule checkout location | |
| :param name: Canonical name of the submodule | |
| :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext | |
| :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack | |
| :param kwargs: Additional arguments given to git.clone | |
| """ | |
| module_abspath = cls._module_abspath(repo, path, name) | |
| module_checkout_path = module_abspath | |
| if cls._need_gitfile_submodules(repo.git): | |
| kwargs["separate_git_dir"] = module_abspath | |
| module_abspath_dir = osp.dirname(module_abspath) | |
| if not osp.isdir(module_abspath_dir): | |
| os.makedirs(module_abspath_dir) | |
| module_checkout_path = osp.join(str(repo.working_tree_dir), path) | |
| clone = git.Repo.clone_from( | |
| url, | |
| module_checkout_path, | |
| allow_unsafe_options=allow_unsafe_options, | |
| allow_unsafe_protocols=allow_unsafe_protocols, | |
| **kwargs, | |
| ) | |
| if cls._need_gitfile_submodules(repo.git): | |
| cls._write_git_file_and_module_config(module_checkout_path, module_abspath) | |
| return clone | |
| def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike: | |
| """:return: a path guaranteed to be relative to the given parent - repository | |
| :raise ValueError: if path is not contained in the parent repository's working tree""" | |
| path = to_native_path_linux(path) | |
| if path.endswith("/"): | |
| path = path[:-1] | |
| # END handle trailing slash | |
| if osp.isabs(path) and parent_repo.working_tree_dir: | |
| working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir) | |
| if not path.startswith(working_tree_linux): | |
| raise ValueError( | |
| "Submodule checkout path '%s' needs to be within the parents repository at '%s'" | |
| % (working_tree_linux, path) | |
| ) | |
| path = path[len(working_tree_linux.rstrip("/")) + 1 :] | |
| if not path: | |
| raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path) | |
| # END verify converted relative path makes sense | |
| # END convert to a relative path | |
| return path | |
| def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None: | |
| """Write a .git file containing a(preferably) relative path to the actual git module repository. | |
| It is an error if the module_abspath cannot be made into a relative path, relative to the working_tree_dir | |
| :note: This will overwrite existing files! | |
| :note: as we rewrite both the git file as well as the module configuration, we might fail on the configuration | |
| and will not roll back changes done to the git file. This should be a non - issue, but may easily be fixed | |
| if it becomes one. | |
| :param working_tree_dir: Directory to write the .git file into | |
| :param module_abspath: Absolute path to the bare repository | |
| """ | |
| git_file = osp.join(working_tree_dir, ".git") | |
| rela_path = osp.relpath(module_abspath, start=working_tree_dir) | |
| if os.name == "nt" and osp.isfile(git_file): | |
| os.remove(git_file) | |
| with open(git_file, "wb") as fp: | |
| fp.write(("gitdir: %s" % rela_path).encode(defenc)) | |
| with GitConfigParser(osp.join(module_abspath, "config"), read_only=False, merge_includes=False) as writer: | |
| writer.set_value( | |
| "core", | |
| "worktree", | |
| to_native_path_linux(osp.relpath(working_tree_dir, start=module_abspath)), | |
| ) | |
| # { Edit Interface | |
| def add( | |
| cls, | |
| repo: "Repo", | |
| name: str, | |
| path: PathLike, | |
| url: Union[str, None] = None, | |
| branch: Union[str, None] = None, | |
| no_checkout: bool = False, | |
| depth: Union[int, None] = None, | |
| env: Union[Mapping[str, str], None] = None, | |
| clone_multi_options: Union[Sequence[TBD], None] = None, | |
| allow_unsafe_options: bool = False, | |
| allow_unsafe_protocols: bool = False, | |
| ) -> "Submodule": | |
| """Add a new submodule to the given repository. This will alter the index | |
| as well as the .gitmodules file, but will not create a new commit. | |
| If the submodule already exists, no matter if the configuration differs | |
| from the one provided, the existing submodule will be returned. | |
| :param repo: Repository instance which should receive the submodule. | |
| :param name: The name/identifier for the submodule. | |
| :param path: Repository-relative or absolute path at which the submodule | |
| should be located. | |
| It will be created as required during the repository initialization. | |
| :param url: git-clone compatible URL, see git-clone reference for more information. | |
| If None, the repository is assumed to exist, and the url of the first | |
| remote is taken instead. This is useful if you want to make an existing | |
| repository a submodule of another one. | |
| :param branch: name of branch at which the submodule should (later) be checked out. | |
| The given branch must exist in the remote repository, and will be checked | |
| out locally as a tracking branch. | |
| It will only be written into the configuration if it not None, which is | |
| when the checked out branch will be the one the remote HEAD pointed to. | |
| The result you get in these situation is somewhat fuzzy, and it is recommended | |
| to specify at least 'master' here. | |
| Examples are 'master' or 'feature/new'. | |
| :param no_checkout: If True, and if the repository has to be cloned manually, | |
| no checkout will be performed. | |
| :param depth: Create a shallow clone with a history truncated to the | |
| specified number of commits. | |
| :param env: Optional dictionary containing the desired environment variables. | |
| Note: Provided variables will be used to update the execution environment | |
| for ``git``. If some variable is not specified in `env` and is defined in | |
| attr:`os.environ`, the value from attr:`os.environ` will be used. If you | |
| want to unset some variable, consider providing an empty string as its | |
| value. | |
| :param clone_multi_options: A list of Clone options. Please see | |
| :meth:`Repo.clone <git.repo.base.Repo.clone>` for details. | |
| :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext. | |
| :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack | |
| :return: The newly created submodule instance. | |
| :note: Works atomically, such that no change will be done if the repository | |
| update fails for instance. | |
| """ | |
| if repo.bare: | |
| raise InvalidGitRepositoryError("Cannot add submodules to bare repositories") | |
| # END handle bare repos | |
| path = cls._to_relative_path(repo, path) | |
| # Ensure we never put backslashes into the URL, as some operating systems | |
| # like it... | |
| if url is not None: | |
| url = to_native_path_linux(url) | |
| # END ensure URL correctness | |
| # INSTANTIATE INTERMEDIATE SM | |
| sm = cls( | |
| repo, | |
| cls.NULL_BIN_SHA, | |
| cls.k_default_mode, | |
| path, | |
| name, | |
| url="invalid-temporary", | |
| ) | |
| if sm.exists(): | |
| # Reretrieve submodule from tree. | |
| try: | |
| sm = repo.head.commit.tree[str(path)] | |
| sm._name = name | |
| return sm | |
| except KeyError: | |
| # Could only be in index. | |
| index = repo.index | |
| entry = index.entries[index.entry_key(path, 0)] | |
| sm.binsha = entry.binsha | |
| return sm | |
| # END handle exceptions | |
| # END handle existing | |
| # fake-repo - we only need the functionality on the branch instance. | |
| br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default)) | |
| has_module = sm.module_exists() | |
| branch_is_default = branch is None | |
| if has_module and url is not None: | |
| if url not in [r.url for r in sm.module().remotes]: | |
| raise ValueError( | |
| "Specified URL '%s' does not match any remote url of the repository at '%s'" % (url, sm.abspath) | |
| ) | |
| # END check url | |
| # END verify urls match | |
| mrepo: Union[Repo, None] = None | |
| if url is None: | |
| if not has_module: | |
| raise ValueError("A URL was not given and a repository did not exist at %s" % path) | |
| # END check url | |
| mrepo = sm.module() | |
| # assert isinstance(mrepo, git.Repo) | |
| urls = [r.url for r in mrepo.remotes] | |
| if not urls: | |
| raise ValueError("Didn't find any remote url in repository at %s" % sm.abspath) | |
| # END verify we have url | |
| url = urls[0] | |
| else: | |
| # Clone new repo. | |
| kwargs: Dict[str, Union[bool, int, str, Sequence[TBD]]] = {"n": no_checkout} | |
| if not branch_is_default: | |
| kwargs["b"] = br.name | |
| # END setup checkout-branch | |
| if depth: | |
| if isinstance(depth, int): | |
| kwargs["depth"] = depth | |
| else: | |
| raise ValueError("depth should be an integer") | |
| if clone_multi_options: | |
| kwargs["multi_options"] = clone_multi_options | |
| # _clone_repo(cls, repo, url, path, name, **kwargs): | |
| mrepo = cls._clone_repo( | |
| repo, | |
| url, | |
| path, | |
| name, | |
| env=env, | |
| allow_unsafe_options=allow_unsafe_options, | |
| allow_unsafe_protocols=allow_unsafe_protocols, | |
| **kwargs, | |
| ) | |
| # END verify url | |
| ## See #525 for ensuring git URLs in config-files are valid under Windows. | |
| url = Git.polish_url(url) | |
| # It's important to add the URL to the parent config, to let `git submodule` know. | |
| # Otherwise there is a '-' character in front of the submodule listing: | |
| # a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8) | |
| # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one | |
| writer: Union[GitConfigParser, SectionConstraint] | |
| with sm.repo.config_writer() as writer: | |
| writer.set_value(sm_section(name), "url", url) | |
| # Update configuration and index. | |
| index = sm.repo.index | |
| with sm.config_writer(index=index, write=False) as writer: | |
| writer.set_value("url", url) | |
| writer.set_value("path", path) | |
| sm._url = url | |
| if not branch_is_default: | |
| # Store full path. | |
| writer.set_value(cls.k_head_option, br.path) | |
| sm._branch_path = br.path | |
| # We deliberately assume that our head matches our index! | |
| if mrepo: | |
| sm.binsha = mrepo.head.commit.binsha | |
| index.add([sm], write=True) | |
| return sm | |
| def update( | |
| self, | |
| recursive: bool = False, | |
| init: bool = True, | |
| to_latest_revision: bool = False, | |
| progress: Union["UpdateProgress", None] = None, | |
| dry_run: bool = False, | |
| force: bool = False, | |
| keep_going: bool = False, | |
| env: Union[Mapping[str, str], None] = None, | |
| clone_multi_options: Union[Sequence[TBD], None] = None, | |
| allow_unsafe_options: bool = False, | |
| allow_unsafe_protocols: bool = False, | |
| ) -> "Submodule": | |
| """Update the repository of this submodule to point to the checkout | |
| we point at with the binsha of this instance. | |
| :param recursive: | |
| If True, we will operate recursively and update child modules as well. | |
| :param init: | |
| If True, the module repository will be cloned into place if necessary. | |
| :param to_latest_revision: | |
| If True, the submodule's sha will be ignored during checkout. Instead, the | |
| remote will be fetched, and the local tracking branch updated. This only | |
| works if we have a local tracking branch, which is the case if the remote | |
| repository had a master branch, or of the 'branch' option was specified for | |
| this submodule and the branch existed remotely. | |
| :param progress: | |
| UpdateProgress instance or None if no progress should be shown. | |
| :param dry_run: | |
| If True, the operation will only be simulated, but not performed. | |
| All performed operations are read-only. | |
| :param force: | |
| If True, we may reset heads even if the repository in question is dirty. | |
| Additionally we will be allowed to set a tracking branch which is ahead of | |
| its remote branch back into the past or the location of the remote branch. | |
| This will essentially 'forget' commits. | |
| If False, local tracking branches that are in the future of their respective | |
| remote branches will simply not be moved. | |
| :param keep_going: | |
| If True, we will ignore but log all errors, and keep going recursively. | |
| Unless dry_run is set as well, keep_going could cause subsequent / inherited | |
| errors you wouldn't see otherwise. | |
| In conjunction with dry_run, it can be useful to anticipate all errors when | |
| updating submodules. | |
| :param env: Optional dictionary containing the desired environment variables. | |
| Note: Provided variables will be used to update the execution environment | |
| for ``git``. If some variable is not specified in `env` and is defined in | |
| attr:`os.environ`, value from attr:`os.environ` will be used. | |
| If you want to unset some variable, consider providing the empty string as | |
| its value. | |
| :param clone_multi_options: | |
| List of Clone options. | |
| Please see :meth:`Repo.clone <git.repo.base.Repo.clone>` for details. | |
| They only take effect with the `init` option. | |
| :param allow_unsafe_protocols: | |
| Allow unsafe protocols to be used, like ext. | |
| :param allow_unsafe_options: | |
| Allow unsafe options to be used, like --upload-pack. | |
| :note: Does nothing in bare repositories. | |
| :note: This method is definitely not atomic if `recursive` is True. | |
| :return: self | |
| """ | |
| if self.repo.bare: | |
| return self | |
| # END pass in bare mode | |
| if progress is None: | |
| progress = UpdateProgress() | |
| # END handle progress | |
| prefix = "" | |
| if dry_run: | |
| prefix = "DRY-RUN: " | |
| # END handle prefix | |
| # To keep things plausible in dry-run mode. | |
| if dry_run: | |
| mrepo = None | |
| # END init mrepo | |
| try: | |
| # ENSURE REPO IS PRESENT AND UP-TO-DATE | |
| ####################################### | |
| try: | |
| mrepo = self.module() | |
| rmts = mrepo.remotes | |
| len_rmts = len(rmts) | |
| for i, remote in enumerate(rmts): | |
| op = FETCH | |
| if i == 0: | |
| op |= BEGIN | |
| # END handle start | |
| progress.update( | |
| op, | |
| i, | |
| len_rmts, | |
| prefix + "Fetching remote %s of submodule %r" % (remote, self.name), | |
| ) | |
| # =============================== | |
| if not dry_run: | |
| remote.fetch(progress=progress) | |
| # END handle dry-run | |
| # =============================== | |
| if i == len_rmts - 1: | |
| op |= END | |
| # END handle end | |
| progress.update( | |
| op, | |
| i, | |
| len_rmts, | |
| prefix + "Done fetching remote of submodule %r" % self.name, | |
| ) | |
| # END fetch new data | |
| except InvalidGitRepositoryError: | |
| mrepo = None | |
| if not init: | |
| return self | |
| # END early abort if init is not allowed | |
| # There is no git-repository yet - but delete empty paths. | |
| checkout_module_abspath = self.abspath | |
| if not dry_run and osp.isdir(checkout_module_abspath): | |
| try: | |
| os.rmdir(checkout_module_abspath) | |
| except OSError as e: | |
| raise OSError( | |
| "Module directory at %r does already exist and is non-empty" % checkout_module_abspath | |
| ) from e | |
| # END handle OSError | |
| # END handle directory removal | |
| # Don't check it out at first - nonetheless it will create a local | |
| # branch according to the remote-HEAD if possible. | |
| progress.update( | |
| BEGIN | CLONE, | |
| 0, | |
| 1, | |
| prefix | |
| + "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name), | |
| ) | |
| if not dry_run: | |
| mrepo = self._clone_repo( | |
| self.repo, | |
| self.url, | |
| self.path, | |
| self.name, | |
| n=True, | |
| env=env, | |
| multi_options=clone_multi_options, | |
| allow_unsafe_options=allow_unsafe_options, | |
| allow_unsafe_protocols=allow_unsafe_protocols, | |
| ) | |
| # END handle dry-run | |
| progress.update( | |
| END | CLONE, | |
| 0, | |
| 1, | |
| prefix + "Done cloning to %s" % checkout_module_abspath, | |
| ) | |
| if not dry_run: | |
| # See whether we have a valid branch to check out. | |
| try: | |
| mrepo = cast("Repo", mrepo) | |
| # Find a remote which has our branch - we try to be flexible. | |
| remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name) | |
| local_branch = mkhead(mrepo, self.branch_path) | |
| # Have a valid branch, but no checkout - make sure we can figure | |
| # that out by marking the commit with a null_sha. | |
| local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA)) | |
| # END initial checkout + branch creation | |
| # Make sure HEAD is not detached. | |
| mrepo.head.set_reference( | |
| local_branch, | |
| logmsg="submodule: attaching head to %s" % local_branch, | |
| ) | |
| mrepo.head.reference.set_tracking_branch(remote_branch) | |
| except (IndexError, InvalidGitRepositoryError): | |
| log.warning("Failed to checkout tracking branch %s", self.branch_path) | |
| # END handle tracking branch | |
| # NOTE: Have to write the repo config file as well, otherwise the | |
| # default implementation will be offended and not update the repository. | |
| # Maybe this is a good way to ensure it doesn't get into our way, but | |
| # we want to stay backwards compatible too... It's so redundant! | |
| with self.repo.config_writer() as writer: | |
| writer.set_value(sm_section(self.name), "url", self.url) | |
| # END handle dry_run | |
| # END handle initialization | |
| # DETERMINE SHAS TO CHECK OUT | |
| ############################# | |
| binsha = self.binsha | |
| hexsha = self.hexsha | |
| if mrepo is not None: | |
| # mrepo is only set if we are not in dry-run mode or if the module existed. | |
| is_detached = mrepo.head.is_detached | |
| # END handle dry_run | |
| if mrepo is not None and to_latest_revision: | |
| msg_base = "Cannot update to latest revision in repository at %r as " % mrepo.working_dir | |
| if not is_detached: | |
| rref = mrepo.head.reference.tracking_branch() | |
| if rref is not None: | |
| rcommit = rref.commit | |
| binsha = rcommit.binsha | |
| hexsha = rcommit.hexsha | |
| else: | |
| log.error( | |
| "%s a tracking branch was not set for local branch '%s'", | |
| msg_base, | |
| mrepo.head.reference, | |
| ) | |
| # END handle remote ref | |
| else: | |
| log.error("%s there was no local tracking branch", msg_base) | |
| # END handle detached head | |
| # END handle to_latest_revision option | |
| # Update the working tree. | |
| # Handles dry_run. | |
| if mrepo is not None and mrepo.head.commit.binsha != binsha: | |
| # We must ensure that our destination sha (the one to point to) is in the future of our current head. | |
| # Otherwise, we will reset changes that might have been done on the submodule, but were not yet pushed. | |
| # We also handle the case that history has been rewritten, leaving no merge-base. In that case | |
| # we behave conservatively, protecting possible changes the user had done. | |
| may_reset = True | |
| if mrepo.head.commit.binsha != self.NULL_BIN_SHA: | |
| base_commit = mrepo.merge_base(mrepo.head.commit, hexsha) | |
| if len(base_commit) == 0 or (base_commit[0] is not None and base_commit[0].hexsha == hexsha): | |
| if force: | |
| msg = "Will force checkout or reset on local branch that is possibly in the future of" | |
| msg += " the commit it will be checked out to, effectively 'forgetting' new commits" | |
| log.debug(msg) | |
| else: | |
| msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits" | |
| msg %= ( | |
| is_detached and "checkout" or "reset", | |
| mrepo.head, | |
| mrepo, | |
| ) | |
| log.info(msg) | |
| may_reset = False | |
| # END handle force | |
| # END handle if we are in the future | |
| if may_reset and not force and mrepo.is_dirty(index=True, working_tree=True, untracked_files=True): | |
| raise RepositoryDirtyError(mrepo, "Cannot reset a dirty repository") | |
| # END handle force and dirty state | |
| # END handle empty repo | |
| # END verify future/past | |
| progress.update( | |
| BEGIN | UPDWKTREE, | |
| 0, | |
| 1, | |
| prefix | |
| + "Updating working tree at %s for submodule %r to revision %s" % (self.path, self.name, hexsha), | |
| ) | |
| if not dry_run and may_reset: | |
| if is_detached: | |
| # NOTE: For now we force. The user is not supposed to change detached | |
| # submodules anyway. Maybe at some point this becomes an option, to | |
| # properly handle user modifications - see below for future options | |
| # regarding rebase and merge. | |
| mrepo.git.checkout(hexsha, force=force) | |
| else: | |
| mrepo.head.reset(hexsha, index=True, working_tree=True) | |
| # END handle checkout | |
| # If we may reset/checkout. | |
| progress.update( | |
| END | UPDWKTREE, | |
| 0, | |
| 1, | |
| prefix + "Done updating working tree for submodule %r" % self.name, | |
| ) | |
| # END update to new commit only if needed | |
| except Exception as err: | |
| if not keep_going: | |
| raise | |
| log.error(str(err)) | |
| # END handle keep_going | |
| # HANDLE RECURSION | |
| ################## | |
| if recursive: | |
| # In dry_run mode, the module might not exist. | |
| if mrepo is not None: | |
| for submodule in self.iter_items(self.module()): | |
| submodule.update( | |
| recursive, | |
| init, | |
| to_latest_revision, | |
| progress=progress, | |
| dry_run=dry_run, | |
| force=force, | |
| keep_going=keep_going, | |
| ) | |
| # END handle recursive update | |
| # END handle dry run | |
| # END for each submodule | |
| return self | |
| def move(self, module_path: PathLike, configuration: bool = True, module: bool = True) -> "Submodule": | |
| """Move the submodule to a another module path. This involves physically moving | |
| the repository at our current path, changing the configuration, as well as | |
| adjusting our index entry accordingly. | |
| :param module_path: The path to which to move our module in the parent | |
| repository's working tree, given as repository - relative or absolute path. | |
| Intermediate directories will be created accordingly. If the path already | |
| exists, it must be empty. Trailing (back)slashes are removed automatically. | |
| :param configuration: If True, the configuration will be adjusted to let | |
| the submodule point to the given path. | |
| :param module: If True, the repository managed by this submodule | |
| will be moved as well. If False, we don't move the submodule's checkout, | |
| which may leave the parent repository in an inconsistent state. | |
| :return: self | |
| :raise ValueError: If the module path existed and was not empty, or was a file. | |
| :note: Currently the method is not atomic, and it could leave the repository | |
| in an inconsistent state if a sub-step fails for some reason. | |
| """ | |
| if module + configuration < 1: | |
| raise ValueError("You must specify to move at least the module or the configuration of the submodule") | |
| # END handle input | |
| module_checkout_path = self._to_relative_path(self.repo, module_path) | |
| # VERIFY DESTINATION | |
| if module_checkout_path == self.path: | |
| return self | |
| # END handle no change | |
| module_checkout_abspath = join_path_native(str(self.repo.working_tree_dir), module_checkout_path) | |
| if osp.isfile(module_checkout_abspath): | |
| raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath) | |
| # END handle target files | |
| index = self.repo.index | |
| tekey = index.entry_key(module_checkout_path, 0) | |
| # if the target item already exists, fail | |
| if configuration and tekey in index.entries: | |
| raise ValueError("Index entry for target path did already exist") | |
| # END handle index key already there | |
| # Remove existing destination. | |
| if module: | |
| if osp.exists(module_checkout_abspath): | |
| if len(os.listdir(module_checkout_abspath)): | |
| raise ValueError("Destination module directory was not empty") | |
| # END handle non-emptiness | |
| if osp.islink(module_checkout_abspath): | |
| os.remove(module_checkout_abspath) | |
| else: | |
| os.rmdir(module_checkout_abspath) | |
| # END handle link | |
| else: | |
| # Recreate parent directories. | |
| # NOTE: renames() does that now. | |
| pass | |
| # END handle existence | |
| # END handle module | |
| # Move the module into place if possible. | |
| cur_path = self.abspath | |
| renamed_module = False | |
| if module and osp.exists(cur_path): | |
| os.renames(cur_path, module_checkout_abspath) | |
| renamed_module = True | |
| if osp.isfile(osp.join(module_checkout_abspath, ".git")): | |
| module_abspath = self._module_abspath(self.repo, self.path, self.name) | |
| self._write_git_file_and_module_config(module_checkout_abspath, module_abspath) | |
| # END handle git file rewrite | |
| # END move physical module | |
| # Rename the index entry - we have to manipulate the index directly as | |
| # git-mv cannot be used on submodules... yeah. | |
| previous_sm_path = self.path | |
| try: | |
| if configuration: | |
| try: | |
| ekey = index.entry_key(self.path, 0) | |
| entry = index.entries[ekey] | |
| del index.entries[ekey] | |
| nentry = git.IndexEntry(entry[:3] + (module_checkout_path,) + entry[4:]) | |
| index.entries[tekey] = nentry | |
| except KeyError as e: | |
| raise InvalidGitRepositoryError("Submodule's entry at %r did not exist" % (self.path)) from e | |
| # END handle submodule doesn't exist | |
| # Update configuration. | |
| with self.config_writer(index=index) as writer: # Auto-write. | |
| writer.set_value("path", module_checkout_path) | |
| self.path = module_checkout_path | |
| # END handle configuration flag | |
| except Exception: | |
| if renamed_module: | |
| os.renames(module_checkout_abspath, cur_path) | |
| # END undo module renaming | |
| raise | |
| # END handle undo rename | |
| # Auto-rename submodule if its name was 'default', that is, the checkout directory. | |
| if previous_sm_path == self.name: | |
| self.rename(module_checkout_path) | |
| return self | |
| def remove( | |
| self, | |
| module: bool = True, | |
| force: bool = False, | |
| configuration: bool = True, | |
| dry_run: bool = False, | |
| ) -> "Submodule": | |
| """Remove this submodule from the repository. This will remove our entry | |
| from the .gitmodules file and the entry in the .git/config file. | |
| :param module: If True, the checked out module we point to will be deleted as | |
| well. If that module is currently on a commit outside any branch in the | |
| remote, or if it is ahead of its tracking branch, or if there are modified | |
| or untracked files in its working tree, then the removal will fail. In case | |
| the removal of the repository fails for these reasons, the submodule status | |
| will not have been altered. | |
| If this submodule has child modules of its own, these will be deleted prior | |
| to touching the direct submodule. | |
| :param force: Enforces the deletion of the module even though it contains | |
| modifications. This basically enforces a brute-force file system based | |
| deletion. | |
| :param configuration: If True, the submodule is deleted from the configuration, | |
| otherwise it isn't. Although this should be enabled most of the time, this | |
| flag enables you to safely delete the repository of your submodule. | |
| :param dry_run: If True, we will not actually do anything, but throw the errors | |
| we would usually throw. | |
| :return: self | |
| :note: Doesn't work in bare repositories. | |
| :note: Doesn't work atomically, as failure to remove any part of the submodule | |
| will leave an inconsistent state. | |
| :raise InvalidGitRepositoryError: Thrown if the repository cannot be deleted. | |
| :raise OSError: If directories or files could not be removed. | |
| """ | |
| if not (module or configuration): | |
| raise ValueError("Need to specify to delete at least the module, or the configuration") | |
| # END handle parameters | |
| # Recursively remove children of this submodule. | |
| nc = 0 | |
| for csm in self.children(): | |
| nc += 1 | |
| csm.remove(module, force, configuration, dry_run) | |
| del csm | |
| if configuration and not dry_run and nc > 0: | |
| # Ensure we don't leave the parent repository in a dirty state, and commit our changes. | |
| # It's important for recursive, unforced, deletions to work as expected. | |
| self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name) | |
| # END handle recursion | |
| # DELETE REPOSITORY WORKING TREE | |
| ################################ | |
| if module and self.module_exists(): | |
| mod = self.module() | |
| git_dir = mod.git_dir | |
| if force: | |
| # Take the fast lane and just delete everything in our module path. | |
| # TODO: If we run into permission problems, we have a highly inconsistent | |
| # state. Delete the .git folders last, start with the submodules first. | |
| mp = self.abspath | |
| method: Union[None, Callable[[PathLike], None]] = None | |
| if osp.islink(mp): | |
| method = os.remove | |
| elif osp.isdir(mp): | |
| method = rmtree | |
| elif osp.exists(mp): | |
| raise AssertionError("Cannot forcibly delete repository as it was neither a link, nor a directory") | |
| # END handle brutal deletion | |
| if not dry_run: | |
| assert method | |
| method(mp) | |
| # END apply deletion method | |
| else: | |
| # Verify we may delete our module. | |
| if mod.is_dirty(index=True, working_tree=True, untracked_files=True): | |
| raise InvalidGitRepositoryError( | |
| "Cannot delete module at %s with any modifications, unless force is specified" | |
| % mod.working_tree_dir | |
| ) | |
| # END check for dirt | |
| # Figure out whether we have new commits compared to the remotes. | |
| # NOTE: If the user pulled all the time, the remote heads might not have | |
| # been updated, so commits coming from the remote look as if they come | |
| # from us. But we stay strictly read-only and don't fetch beforehand. | |
| for remote in mod.remotes: | |
| num_branches_with_new_commits = 0 | |
| rrefs = remote.refs | |
| for rref in rrefs: | |
| num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0 | |
| # END for each remote ref | |
| # Not a single remote branch contained all our commits. | |
| if len(rrefs) and num_branches_with_new_commits == len(rrefs): | |
| raise InvalidGitRepositoryError( | |
| "Cannot delete module at %s as there are new commits" % mod.working_tree_dir | |
| ) | |
| # END handle new commits | |
| # We have to manually delete some references to allow resources to | |
| # be cleaned up immediately when we are done with them, because | |
| # Python's scoping is no more granular than the whole function (loop | |
| # bodies are not scopes). When the objects stay alive longer, they | |
| # can keep handles open. On Windows, this is a problem. | |
| if len(rrefs): | |
| del rref # skipcq: PYL-W0631 | |
| # END handle remotes | |
| del rrefs | |
| del remote | |
| # END for each remote | |
| # Finally delete our own submodule. | |
| if not dry_run: | |
| self._clear_cache() | |
| wtd = mod.working_tree_dir | |
| del mod # Release file-handles (Windows). | |
| gc.collect() | |
| rmtree(str(wtd)) | |
| # END delete tree if possible | |
| # END handle force | |
| if not dry_run and osp.isdir(git_dir): | |
| self._clear_cache() | |
| rmtree(git_dir) | |
| # END handle separate bare repository | |
| # END handle module deletion | |
| # Void our data so as not to delay invalid access. | |
| if not dry_run: | |
| self._clear_cache() | |
| # DELETE CONFIGURATION | |
| ###################### | |
| if configuration and not dry_run: | |
| # First the index-entry. | |
| parent_index = self.repo.index | |
| try: | |
| del parent_index.entries[parent_index.entry_key(self.path, 0)] | |
| except KeyError: | |
| pass | |
| # END delete entry | |
| parent_index.write() | |
| # Now git config - we need the config intact, otherwise we can't query | |
| # information anymore. | |
| with self.repo.config_writer() as gcp_writer: | |
| gcp_writer.remove_section(sm_section(self.name)) | |
| with self.config_writer() as sc_writer: | |
| sc_writer.remove_section() | |
| # END delete configuration | |
| return self | |
| def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True) -> "Submodule": | |
| """Set this instance to use the given commit whose tree is supposed to | |
| contain the .gitmodules blob. | |
| :param commit: | |
| Commit-ish reference pointing at the root_tree, or None to always point to | |
| the most recent commit | |
| :param check: | |
| If True, relatively expensive checks will be performed to verify | |
| validity of the submodule. | |
| :raise ValueError: If the commit's tree didn't contain the .gitmodules blob. | |
| :raise ValueError: | |
| If the parent commit didn't store this submodule under the current path. | |
| :return: self | |
| """ | |
| if commit is None: | |
| self._parent_commit = None | |
| return self | |
| # END handle None | |
| pcommit = self.repo.commit(commit) | |
| pctree = pcommit.tree | |
| if self.k_modules_file not in pctree: | |
| raise ValueError("Tree of commit %s did not contain the %s file" % (commit, self.k_modules_file)) | |
| # END handle exceptions | |
| prev_pc = self._parent_commit | |
| self._parent_commit = pcommit | |
| if check: | |
| parser = self._config_parser(self.repo, self._parent_commit, read_only=True) | |
| if not parser.has_section(sm_section(self.name)): | |
| self._parent_commit = prev_pc | |
| raise ValueError("Submodule at path %r did not exist in parent commit %s" % (self.path, commit)) | |
| # END handle submodule did not exist | |
| # END handle checking mode | |
| # Update our sha, it could have changed. | |
| # If check is False, we might see a parent-commit that doesn't even contain the | |
| # submodule anymore. in that case, mark our sha as being NULL. | |
| try: | |
| self.binsha = pctree[str(self.path)].binsha | |
| except KeyError: | |
| self.binsha = self.NULL_BIN_SHA | |
| self._clear_cache() | |
| return self | |
| def config_writer( | |
| self, index: Union["IndexFile", None] = None, write: bool = True | |
| ) -> SectionConstraint["SubmoduleConfigParser"]: | |
| """ | |
| :return: A config writer instance allowing you to read and write the data | |
| belonging to this submodule into the .gitmodules file. | |
| :param index: If not None, an IndexFile instance which should be written. | |
| Defaults to the index of the Submodule's parent repository. | |
| :param write: If True, the index will be written each time a configuration | |
| value changes. | |
| :note: The parameters allow for a more efficient writing of the index, | |
| as you can pass in a modified index on your own, prevent automatic writing, | |
| and write yourself once the whole operation is complete. | |
| :raise ValueError: If trying to get a writer on a parent_commit which does not | |
| match the current head commit. | |
| :raise IOError: If the .gitmodules file/blob could not be read | |
| """ | |
| writer = self._config_parser_constrained(read_only=False) | |
| if index is not None: | |
| writer.config._index = index | |
| writer.config._auto_write = write | |
| return writer | |
| def rename(self, new_name: str) -> "Submodule": | |
| """Rename this submodule. | |
| :note: | |
| This method takes care of renaming the submodule in various places, such as: | |
| * $parent_git_dir / config | |
| * $working_tree_dir / .gitmodules | |
| * (git >= v1.8.0: move submodule repository to new name) | |
| As .gitmodules will be changed, you would need to make a commit afterwards. The | |
| changed .gitmodules file will already be added to the index. | |
| :return: This submodule instance | |
| """ | |
| if self.name == new_name: | |
| return self | |
| # .git/config | |
| with self.repo.config_writer() as pw: | |
| # As we ourselves didn't write anything about submodules into the parent .git/config, | |
| # we will not require it to exist, and just ignore missing entries. | |
| if pw.has_section(sm_section(self.name)): | |
| pw.rename_section(sm_section(self.name), sm_section(new_name)) | |
| # .gitmodules | |
| with self.config_writer(write=True).config as cw: | |
| cw.rename_section(sm_section(self.name), sm_section(new_name)) | |
| self._name = new_name | |
| # .git/modules | |
| mod = self.module() | |
| if mod.has_separate_working_tree(): | |
| destination_module_abspath = self._module_abspath(self.repo, self.path, new_name) | |
| source_dir = mod.git_dir | |
| # Let's be sure the submodule name is not so obviously tied to a directory. | |
| if str(destination_module_abspath).startswith(str(mod.git_dir)): | |
| tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4())) | |
| os.renames(source_dir, tmp_dir) | |
| source_dir = tmp_dir | |
| # END handle self-containment | |
| os.renames(source_dir, destination_module_abspath) | |
| if mod.working_tree_dir: | |
| self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath) | |
| # END move separate git repository | |
| return self | |
| # } END edit interface | |
| # { Query Interface | |
| def module(self) -> "Repo": | |
| """ | |
| :return: Repo instance initialized from the repository at our submodule path | |
| :raise InvalidGitRepositoryError: If a repository was not available. This could | |
| also mean that it was not yet initialized. | |
| """ | |
| module_checkout_abspath = self.abspath | |
| try: | |
| repo = git.Repo(module_checkout_abspath) | |
| if repo != self.repo: | |
| return repo | |
| # END handle repo uninitialized | |
| except (InvalidGitRepositoryError, NoSuchPathError) as e: | |
| raise InvalidGitRepositoryError("No valid repository at %s" % module_checkout_abspath) from e | |
| else: | |
| raise InvalidGitRepositoryError("Repository at %r was not yet checked out" % module_checkout_abspath) | |
| # END handle exceptions | |
| def module_exists(self) -> bool: | |
| """:return: True if our module exists and is a valid git repository. See module() method.""" | |
| try: | |
| self.module() | |
| return True | |
| except Exception: | |
| return False | |
| # END handle exception | |
| def exists(self) -> bool: | |
| """ | |
| :return: True if the submodule exists, False otherwise. Please note that | |
| a submodule may exist (in the .gitmodules file) even though its module | |
| doesn't exist on disk. | |
| """ | |
| # Keep attributes for later, and restore them if we have no valid data. | |
| # This way we do not actually alter the state of the object. | |
| loc = locals() | |
| for attr in self._cache_attrs: | |
| try: | |
| if hasattr(self, attr): | |
| loc[attr] = getattr(self, attr) | |
| # END if we have the attribute cache | |
| except (cp.NoSectionError, ValueError): | |
| # On PY3, this can happen apparently... don't know why this doesn't happen on PY2. | |
| pass | |
| # END for each attr | |
| self._clear_cache() | |
| try: | |
| try: | |
| self.path | |
| return True | |
| except Exception: | |
| return False | |
| # END handle exceptions | |
| finally: | |
| for attr in self._cache_attrs: | |
| if attr in loc: | |
| setattr(self, attr, loc[attr]) | |
| # END if we have a cache | |
| # END reapply each attribute | |
| # END handle object state consistency | |
| def branch(self) -> "Head": | |
| """ | |
| :return: The branch instance that we are to checkout | |
| :raise InvalidGitRepositoryError: If our module is not yet checked out | |
| """ | |
| return mkhead(self.module(), self._branch_path) | |
| def branch_path(self) -> PathLike: | |
| """ | |
| :return: Full (relative) path as string to the branch we would checkout | |
| from the remote and track | |
| """ | |
| return self._branch_path | |
| def branch_name(self) -> str: | |
| """:return: The name of the branch, which is the shortest possible branch name""" | |
| # Use an instance method, for this we create a temporary Head instance | |
| # which uses a repository that is available at least (it makes no difference). | |
| return git.Head(self.repo, self._branch_path).name | |
| def url(self) -> str: | |
| """:return: The url to the repository which our module - repository refers to""" | |
| return self._url | |
| def parent_commit(self) -> "Commit_ish": | |
| """ | |
| :return: Commit instance with the tree containing the .gitmodules file | |
| :note: Will always point to the current head's commit if it was not set explicitly. | |
| """ | |
| if self._parent_commit is None: | |
| return self.repo.commit() | |
| return self._parent_commit | |
| def name(self) -> str: | |
| """ | |
| :return: The name of this submodule. It is used to identify it within the | |
| .gitmodules file. | |
| :note: By default, this is the name is the path at which to find the submodule, | |
| but in GitPython it should be a unique identifier similar to the identifiers | |
| used for remotes, which allows to change the path of the submodule easily. | |
| """ | |
| return self._name | |
| def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]: | |
| """ | |
| :return: ConfigReader instance which allows you to query the configuration | |
| values of this submodule, as provided by the .gitmodules file. | |
| :note: The config reader will actually read the data directly from the | |
| repository and thus does not need nor care about your working tree. | |
| :note: Should be cached by the caller and only kept as long as needed. | |
| :raise IOError: If the .gitmodules file/blob could not be read. | |
| """ | |
| return self._config_parser_constrained(read_only=True) | |
| def children(self) -> IterableList["Submodule"]: | |
| """ | |
| :return: IterableList(Submodule, ...) an iterable list of submodules instances | |
| which are children of this submodule or 0 if the submodule is not checked out. | |
| """ | |
| return self._get_intermediate_items(self) | |
| # } END query interface | |
| # { Iterable Interface | |
| def iter_items( | |
| cls, | |
| repo: "Repo", | |
| parent_commit: Union[Commit_ish, str] = "HEAD", | |
| *Args: Any, | |
| **kwargs: Any, | |
| ) -> Iterator["Submodule"]: | |
| """:return: Iterator yielding Submodule instances available in the given repository""" | |
| try: | |
| pc = repo.commit(parent_commit) # Parent commit instance | |
| parser = cls._config_parser(repo, pc, read_only=True) | |
| except (IOError, BadName): | |
| return | |
| # END handle empty iterator | |
| for sms in parser.sections(): | |
| n = sm_name(sms) | |
| p = parser.get(sms, "path") | |
| u = parser.get(sms, "url") | |
| b = cls.k_head_default | |
| if parser.has_option(sms, cls.k_head_option): | |
| b = str(parser.get(sms, cls.k_head_option)) | |
| # END handle optional information | |
| # Get the binsha. | |
| index = repo.index | |
| try: | |
| rt = pc.tree # Root tree | |
| sm = rt[p] | |
| except KeyError: | |
| # Try the index, maybe it was just added. | |
| try: | |
| entry = index.entries[index.entry_key(p, 0)] | |
| sm = Submodule(repo, entry.binsha, entry.mode, entry.path) | |
| except KeyError: | |
| # The submodule doesn't exist, probably it wasn't | |
| # removed from the .gitmodules file. | |
| continue | |
| # END handle keyerror | |
| # END handle critical error | |
| # Make sure we are looking at a submodule object. | |
| if type(sm) is not git.objects.submodule.base.Submodule: | |
| continue | |
| # Fill in remaining info - saves time as it doesn't have to be parsed again. | |
| sm._name = n | |
| if pc != repo.commit(): | |
| sm._parent_commit = pc | |
| # END set only if not most recent! | |
| sm._branch_path = git.Head.to_full_path(b) | |
| sm._url = u | |
| yield sm | |
| # END for each section | |
| # } END iterable interface | |