| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642 |
- # 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(...) <git.util.RemoteProgress.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[assignment]
- """This is a bogus type string 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", 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:
- The :class:`~git.objects.commit.Commit` whose tree is supposed to contain
- the ``.gitmodules`` blob, or ``None`` to always point to the most recent
- commit. See :meth:`set_parent_commit` for details.
- :param url:
- The URL to the remote repository which is the submodule.
- :param branch_path:
- Full repository-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:
- 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
- @classmethod
- 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
- @classmethod
- 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,
- )
- @classmethod
- def _config_parser(
- cls, repo: "Repo", parent_commit: Union["Commit", 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
- @classmethod
- def _sio_modules(cls, parent_commit: "Commit") -> BytesIO:
- """
- :return:
- Configuration file as :class:`~io.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 = 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))
- @classmethod
- 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()
- @classmethod
- 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:
- :class:`~git.repo.base.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 :manpage:`git-clone(1)`.
- """
- 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(repo.working_tree_dir, path) # type: ignore[arg-type]
- if url.startswith("../"):
- remote_name = cast("RemoteReference", repo.active_branch.tracking_branch()).remote_name
- repo_remote_url = repo.remote(remote_name).url
- url = os.path.join(repo_remote_url, url)
- 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
- @classmethod
- 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
- @classmethod
- 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 sys.platform == "win32" 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
- @classmethod
- 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 :manpage:`git-clone(1)` 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 :class:`Submodule` instance.
- :note:
- Works atomically, such that no change will be done if, for example, the
- repository update fails.
- """
- 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 might happen on Windows.
- 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[os.fspath(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 if the ``branch`` option was specified
- for this submodule and the branch existed remotely.
- :param progress:
- :class:`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 :manpage:`git-clone(1)` 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:
- if self.url.startswith("."):
- url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url)
- else:
- url = self.url
- mrepo = self._clone_repo(
- self.repo,
- 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):
- _logger.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:
- _logger.error(
- "%s a tracking branch was not set for local branch '%s'",
- msg_base,
- mrepo.head.reference,
- )
- # END handle remote ref
- else:
- _logger.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"
- _logger.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,
- )
- _logger.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
- _logger.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
- @unbare_repo
- 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
- @unbare_repo
- 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 git.exc.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, str, 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
- @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
|