← 返回首页
# 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