← 返回首页
# This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ __all__ = ["Submodule", "UpdateProgress"] import gc from io import BytesIO import logging import os import os.path as osp import stat import sys import uuid import urllib 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 ( Any, Callable, Dict, Iterator, Mapping, Sequence, TYPE_CHECKING, Union, cast, ) if sys.version_info >= (3, 8): from typing import Literal else: from typing_extensions import Literal from git.types import Commit_ish, PathLike, TBD if TYPE_CHECKING: from git.index import IndexFile from git.objects.commit import Commit from git.refs import Head, RemoteReference from git.repo import Repo # ----------------------------------------------------------------------------- _logger = logging.getLogger(__name__) class UpdateProgress(RemoteProgress): """Class providing detailed progress information to the caller who should derive from it and implement the :meth:`update(...) ` message.""" CLONE, FETCH, UPDWKTREE = [1 "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 @unbare_repo 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 :class:`~git.index.base.IndexFile` instance which should be written. Defaults to the index of the :class:`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 @unbare_repo 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 :class:`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 @unbare_repo def module(self) -> "Repo": """ :return: :class:`~git.repo.base.Repo` instance initialized from the repository at our submodule path :raise git.exc.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 the :meth:`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 # noqa: B018 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 @property def branch(self) -> "Head": """ :return: The branch instance that we are to checkout :raise git.exc.InvalidGitRepositoryError: If our module is not yet checked out. """ return mkhead(self.module(), self._branch_path) @property def branch_path(self) -> PathLike: """ :return: Full repository-relative path as string to the branch we would checkout from the remote and track """ return self._branch_path @property 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 @property def url(self) -> str: """:return: The url to the repository our submodule's repository refers to""" return self._url @property def parent_commit(self) -> "Commit": """ :return: :class:`~git.objects.commit.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 @property 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 :class:`Submodule` 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 @classmethod def iter_items( cls, repo: "Repo", parent_commit: Union[Commit_ish, str] = "HEAD", *args: Any, **kwargs: Any, ) -> Iterator["Submodule"]: """ :return: Iterator yielding :class:`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