1# This module is part of GitPython and is released under the
2# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
3
4__all__ = ["Submodule", "UpdateProgress"]
5
6import gc
7from io import BytesIO
8import logging
9import os
10import os.path as osp
11import stat
12import sys
13import uuid
14import urllib
15
16import git
17from git.cmd import Git
18from git.compat import defenc
19from git.config import GitConfigParser, SectionConstraint, cp
20from git.exc import (
21 BadName,
22 InvalidGitRepositoryError,
23 NoSuchPathError,
24 RepositoryDirtyError,
25)
26from git.objects.base import IndexObject, Object
27from git.objects.util import TraversableIterableObj
28from git.util import (
29 IterableList,
30 RemoteProgress,
31 join_path_native,
32 rmtree,
33 to_native_path_linux,
34 unbare_repo,
35)
36
37from .util import (
38 SubmoduleConfigParser,
39 find_first_remote_branch,
40 mkhead,
41 sm_name,
42 sm_section,
43)
44
45# typing ----------------------------------------------------------------------
46
47from typing import (
48 Any,
49 Callable,
50 Dict,
51 Iterator,
52 Mapping,
53 Sequence,
54 TYPE_CHECKING,
55 Union,
56 cast,
57)
58
59if sys.version_info >= (3, 8):
60 from typing import Literal
61else:
62 from typing_extensions import Literal
63
64from git.types import Commit_ish, PathLike, TBD
65
66if TYPE_CHECKING:
67 from git.index import IndexFile
68 from git.objects.commit import Commit
69 from git.refs import Head
70 from git.repo import Repo
71
72# -----------------------------------------------------------------------------
73
74_logger = logging.getLogger(__name__)
75
76
77class UpdateProgress(RemoteProgress):
78 """Class providing detailed progress information to the caller who should
79 derive from it and implement the
80 :meth:`update(...) <git.util.RemoteProgress.update>` message."""
81
82 CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)]
83 _num_op_codes: int = RemoteProgress._num_op_codes + 3
84
85 __slots__ = ()
86
87
88BEGIN = UpdateProgress.BEGIN
89END = UpdateProgress.END
90CLONE = UpdateProgress.CLONE
91FETCH = UpdateProgress.FETCH
92UPDWKTREE = UpdateProgress.UPDWKTREE
93
94
95# IndexObject comes via the util module. It's a 'hacky' fix thanks to Python's import
96# mechanism, which causes plenty of trouble if the only reason for packages and modules
97# is refactoring - subpackages shouldn't depend on parent packages.
98class Submodule(IndexObject, TraversableIterableObj):
99 """Implements access to a git submodule. They are special in that their sha
100 represents a commit in the submodule's repository which is to be checked out
101 at the path of this instance.
102
103 The submodule type does not have a string type associated with it, as it exists
104 solely as a marker in the tree and index.
105
106 All methods work in bare and non-bare repositories.
107 """
108
109 _id_attribute_ = "name"
110 k_modules_file = ".gitmodules"
111 k_head_option = "branch"
112 k_head_default = "master"
113 k_default_mode = stat.S_IFDIR | stat.S_IFLNK
114 """Submodule flags. Submodules are directories with link-status."""
115
116 type: Literal["submodule"] = "submodule" # type: ignore[assignment]
117 """This is a bogus type string for base class compatibility."""
118
119 __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__")
120
121 _cache_attrs = ("path", "_url", "_branch_path")
122
123 def __init__(
124 self,
125 repo: "Repo",
126 binsha: bytes,
127 mode: Union[int, None] = None,
128 path: Union[PathLike, None] = None,
129 name: Union[str, None] = None,
130 parent_commit: Union["Commit", None] = None,
131 url: Union[str, None] = None,
132 branch_path: Union[PathLike, None] = None,
133 ) -> None:
134 """Initialize this instance with its attributes.
135
136 We only document the parameters that differ from
137 :class:`~git.objects.base.IndexObject`.
138
139 :param repo:
140 Our parent repository.
141
142 :param binsha:
143 Binary sha referring to a commit in the remote repository.
144 See the `url` parameter.
145
146 :param parent_commit:
147 The :class:`~git.objects.commit.Commit` whose tree is supposed to contain
148 the ``.gitmodules`` blob, or ``None`` to always point to the most recent
149 commit. See :meth:`set_parent_commit` for details.
150
151 :param url:
152 The URL to the remote repository which is the submodule.
153
154 :param branch_path:
155 Full repository-relative path to ref to checkout when cloning the remote
156 repository.
157 """
158 super().__init__(repo, binsha, mode, path)
159 self.size = 0
160 self._parent_commit = parent_commit
161 if url is not None:
162 self._url = url
163 if branch_path is not None:
164 self._branch_path = branch_path
165 if name is not None:
166 self._name = name
167
168 def _set_cache_(self, attr: str) -> None:
169 if attr in ("path", "_url", "_branch_path"):
170 reader: SectionConstraint = self.config_reader()
171 # Default submodule values.
172 try:
173 self.path = reader.get("path")
174 except cp.NoSectionError as e:
175 if self.repo.working_tree_dir is not None:
176 raise ValueError(
177 "This submodule instance does not exist anymore in '%s' file"
178 % osp.join(self.repo.working_tree_dir, ".gitmodules")
179 ) from e
180
181 self._url = reader.get("url")
182 # GitPython extension values - optional.
183 self._branch_path = reader.get_value(self.k_head_option, git.Head.to_full_path(self.k_head_default))
184 elif attr == "_name":
185 raise AttributeError("Cannot retrieve the name of a submodule if it was not set initially")
186 else:
187 super()._set_cache_(attr)
188 # END handle attribute name
189
190 @classmethod
191 def _get_intermediate_items(cls, item: "Submodule") -> IterableList["Submodule"]:
192 """:return: All the submodules of our module repository"""
193 try:
194 return cls.list_items(item.module())
195 except InvalidGitRepositoryError:
196 return IterableList("")
197 # END handle intermediate items
198
199 @classmethod
200 def _need_gitfile_submodules(cls, git: Git) -> bool:
201 return git.version_info[:3] >= (1, 7, 5)
202
203 def __eq__(self, other: Any) -> bool:
204 """Compare with another submodule."""
205 # We may only compare by name as this should be the ID they are hashed with.
206 # Otherwise this type wouldn't be hashable.
207 # return self.path == other.path and self.url == other.url and super().__eq__(other)
208 return self._name == other._name
209
210 def __ne__(self, other: object) -> bool:
211 """Compare with another submodule for inequality."""
212 return not (self == other)
213
214 def __hash__(self) -> int:
215 """Hash this instance using its logical id, not the sha."""
216 return hash(self._name)
217
218 def __str__(self) -> str:
219 return self._name
220
221 def __repr__(self) -> str:
222 return "git.%s(name=%s, path=%s, url=%s, branch_path=%s)" % (
223 type(self).__name__,
224 self._name,
225 self.path,
226 self.url,
227 self.branch_path,
228 )
229
230 @classmethod
231 def _config_parser(
232 cls, repo: "Repo", parent_commit: Union["Commit", None], read_only: bool
233 ) -> SubmoduleConfigParser:
234 """
235 :return:
236 Config parser constrained to our submodule in read or write mode
237
238 :raise IOError:
239 If the ``.gitmodules`` file cannot be found, either locally or in the
240 repository at the given parent commit. Otherwise the exception would be
241 delayed until the first access of the config parser.
242 """
243 parent_matches_head = True
244 if parent_commit is not None:
245 try:
246 parent_matches_head = repo.head.commit == parent_commit
247 except ValueError:
248 # We are most likely in an empty repository, so the HEAD doesn't point
249 # to a valid ref.
250 pass
251 # END handle parent_commit
252 fp_module: Union[str, BytesIO]
253 if not repo.bare and parent_matches_head and repo.working_tree_dir:
254 fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file)
255 else:
256 assert parent_commit is not None, "need valid parent_commit in bare repositories"
257 try:
258 fp_module = cls._sio_modules(parent_commit)
259 except KeyError as e:
260 raise IOError(
261 "Could not find %s file in the tree of parent commit %s" % (cls.k_modules_file, parent_commit)
262 ) from e
263 # END handle exceptions
264 # END handle non-bare working tree
265
266 if not read_only and (repo.bare or not parent_matches_head):
267 raise ValueError("Cannot write blobs of 'historical' submodule configurations")
268 # END handle writes of historical submodules
269
270 return SubmoduleConfigParser(fp_module, read_only=read_only)
271
272 def _clear_cache(self) -> None:
273 """Clear the possibly changed values."""
274 for name in self._cache_attrs:
275 try:
276 delattr(self, name)
277 except AttributeError:
278 pass
279 # END try attr deletion
280 # END for each name to delete
281
282 @classmethod
283 def _sio_modules(cls, parent_commit: "Commit") -> BytesIO:
284 """
285 :return:
286 Configuration file as :class:`~io.BytesIO` - we only access it through the
287 respective blob's data
288 """
289 sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read())
290 sio.name = cls.k_modules_file
291 return sio
292
293 def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
294 """:return: Config parser constrained to our submodule in read or write mode"""
295 try:
296 pc = self.parent_commit
297 except ValueError:
298 pc = None
299 # END handle empty parent repository
300 parser = self._config_parser(self.repo, pc, read_only)
301 parser.set_submodule(self)
302 return SectionConstraint(parser, sm_section(self.name))
303
304 @classmethod
305 def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike:
306 if cls._need_gitfile_submodules(parent_repo.git):
307 return osp.join(parent_repo.git_dir, "modules", name)
308 if parent_repo.working_tree_dir:
309 return osp.join(parent_repo.working_tree_dir, path)
310 raise NotADirectoryError()
311
312 @classmethod
313 def _clone_repo(
314 cls,
315 repo: "Repo",
316 url: str,
317 path: PathLike,
318 name: str,
319 allow_unsafe_options: bool = False,
320 allow_unsafe_protocols: bool = False,
321 **kwargs: Any,
322 ) -> "Repo":
323 """
324 :return:
325 :class:`~git.repo.base.Repo` instance of newly cloned repository.
326
327 :param repo:
328 Our parent repository.
329
330 :param url:
331 URL to clone from.
332
333 :param path:
334 Repository-relative path to the submodule checkout location.
335
336 :param name:
337 Canonical name of the submodule.
338
339 :param allow_unsafe_protocols:
340 Allow unsafe protocols to be used, like ``ext``.
341
342 :param allow_unsafe_options:
343 Allow unsafe options to be used, like ``--upload-pack``.
344
345 :param kwargs:
346 Additional arguments given to :manpage:`git-clone(1)`.
347 """
348 module_abspath = cls._module_abspath(repo, path, name)
349 module_checkout_path = module_abspath
350 if cls._need_gitfile_submodules(repo.git):
351 kwargs["separate_git_dir"] = module_abspath
352 module_abspath_dir = osp.dirname(module_abspath)
353 if not osp.isdir(module_abspath_dir):
354 os.makedirs(module_abspath_dir)
355 module_checkout_path = osp.join(str(repo.working_tree_dir), path)
356
357 if url.startswith("../"):
358 remote_name = repo.active_branch.tracking_branch().remote_name
359 repo_remote_url = repo.remote(remote_name).url
360 url = os.path.join(repo_remote_url, url)
361
362 clone = git.Repo.clone_from(
363 url,
364 module_checkout_path,
365 allow_unsafe_options=allow_unsafe_options,
366 allow_unsafe_protocols=allow_unsafe_protocols,
367 **kwargs,
368 )
369 if cls._need_gitfile_submodules(repo.git):
370 cls._write_git_file_and_module_config(module_checkout_path, module_abspath)
371
372 return clone
373
374 @classmethod
375 def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
376 """:return: A path guaranteed to be relative to the given parent repository
377
378 :raise ValueError:
379 If path is not contained in the parent repository's working tree.
380 """
381 path = to_native_path_linux(path)
382 if path.endswith("/"):
383 path = path[:-1]
384 # END handle trailing slash
385
386 if osp.isabs(path) and parent_repo.working_tree_dir:
387 working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir)
388 if not path.startswith(working_tree_linux):
389 raise ValueError(
390 "Submodule checkout path '%s' needs to be within the parents repository at '%s'"
391 % (working_tree_linux, path)
392 )
393 path = path[len(working_tree_linux.rstrip("/")) + 1 :]
394 if not path:
395 raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path)
396 # END verify converted relative path makes sense
397 # END convert to a relative path
398
399 return path
400
401 @classmethod
402 def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None:
403 """Write a ``.git`` file containing a (preferably) relative path to the actual
404 git module repository.
405
406 It is an error if the `module_abspath` cannot be made into a relative path,
407 relative to the `working_tree_dir`.
408
409 :note:
410 This will overwrite existing files!
411
412 :note:
413 As we rewrite both the git file as well as the module configuration, we
414 might fail on the configuration and will not roll back changes done to the
415 git file. This should be a non-issue, but may easily be fixed if it becomes
416 one.
417
418 :param working_tree_dir:
419 Directory to write the ``.git`` file into.
420
421 :param module_abspath:
422 Absolute path to the bare repository.
423 """
424 git_file = osp.join(working_tree_dir, ".git")
425 rela_path = osp.relpath(module_abspath, start=working_tree_dir)
426 if sys.platform == "win32" and osp.isfile(git_file):
427 os.remove(git_file)
428 with open(git_file, "wb") as fp:
429 fp.write(("gitdir: %s" % rela_path).encode(defenc))
430
431 with GitConfigParser(osp.join(module_abspath, "config"), read_only=False, merge_includes=False) as writer:
432 writer.set_value(
433 "core",
434 "worktree",
435 to_native_path_linux(osp.relpath(working_tree_dir, start=module_abspath)),
436 )
437
438 # { Edit Interface
439
440 @classmethod
441 def add(
442 cls,
443 repo: "Repo",
444 name: str,
445 path: PathLike,
446 url: Union[str, None] = None,
447 branch: Union[str, None] = None,
448 no_checkout: bool = False,
449 depth: Union[int, None] = None,
450 env: Union[Mapping[str, str], None] = None,
451 clone_multi_options: Union[Sequence[TBD], None] = None,
452 allow_unsafe_options: bool = False,
453 allow_unsafe_protocols: bool = False,
454 ) -> "Submodule":
455 """Add a new submodule to the given repository. This will alter the index as
456 well as the ``.gitmodules`` file, but will not create a new commit. If the
457 submodule already exists, no matter if the configuration differs from the one
458 provided, the existing submodule will be returned.
459
460 :param repo:
461 Repository instance which should receive the submodule.
462
463 :param name:
464 The name/identifier for the submodule.
465
466 :param path:
467 Repository-relative or absolute path at which the submodule should be
468 located.
469 It will be created as required during the repository initialization.
470
471 :param url:
472 ``git clone ...``-compatible URL. See :manpage:`git-clone(1)` for more
473 information. If ``None``, the repository is assumed to exist, and the URL of
474 the first remote is taken instead. This is useful if you want to make an
475 existing repository a submodule of another one.
476
477 :param branch:
478 Name of branch at which the submodule should (later) be checked out. The
479 given branch must exist in the remote repository, and will be checked out
480 locally as a tracking branch.
481 It will only be written into the configuration if it not ``None``, which is
482 when the checked out branch will be the one the remote HEAD pointed to.
483 The result you get in these situation is somewhat fuzzy, and it is
484 recommended to specify at least ``master`` here.
485 Examples are ``master`` or ``feature/new``.
486
487 :param no_checkout:
488 If ``True``, and if the repository has to be cloned manually, no checkout
489 will be performed.
490
491 :param depth:
492 Create a shallow clone with a history truncated to the specified number of
493 commits.
494
495 :param env:
496 Optional dictionary containing the desired environment variables.
497
498 Note: Provided variables will be used to update the execution environment
499 for ``git``. If some variable is not specified in `env` and is defined in
500 attr:`os.environ`, the value from attr:`os.environ` will be used. If you
501 want to unset some variable, consider providing an empty string as its
502 value.
503
504 :param clone_multi_options:
505 A list of clone options. Please see
506 :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
507
508 :param allow_unsafe_protocols:
509 Allow unsafe protocols to be used, like ``ext``.
510
511 :param allow_unsafe_options:
512 Allow unsafe options to be used, like ``--upload-pack``.
513
514 :return:
515 The newly created :class:`Submodule` instance.
516
517 :note:
518 Works atomically, such that no change will be done if, for example, the
519 repository update fails.
520 """
521 if repo.bare:
522 raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
523 # END handle bare repos
524
525 path = cls._to_relative_path(repo, path)
526
527 # Ensure we never put backslashes into the URL, as might happen on Windows.
528 if url is not None:
529 url = to_native_path_linux(url)
530 # END ensure URL correctness
531
532 # INSTANTIATE INTERMEDIATE SM
533 sm = cls(
534 repo,
535 cls.NULL_BIN_SHA,
536 cls.k_default_mode,
537 path,
538 name,
539 url="invalid-temporary",
540 )
541 if sm.exists():
542 # Reretrieve submodule from tree.
543 try:
544 sm = repo.head.commit.tree[str(path)]
545 sm._name = name
546 return sm
547 except KeyError:
548 # Could only be in index.
549 index = repo.index
550 entry = index.entries[index.entry_key(path, 0)]
551 sm.binsha = entry.binsha
552 return sm
553 # END handle exceptions
554 # END handle existing
555
556 # fake-repo - we only need the functionality on the branch instance.
557 br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default))
558 has_module = sm.module_exists()
559 branch_is_default = branch is None
560 if has_module and url is not None:
561 if url not in [r.url for r in sm.module().remotes]:
562 raise ValueError(
563 "Specified URL '%s' does not match any remote url of the repository at '%s'" % (url, sm.abspath)
564 )
565 # END check url
566 # END verify urls match
567
568 mrepo: Union[Repo, None] = None
569
570 if url is None:
571 if not has_module:
572 raise ValueError("A URL was not given and a repository did not exist at %s" % path)
573 # END check url
574 mrepo = sm.module()
575 # assert isinstance(mrepo, git.Repo)
576 urls = [r.url for r in mrepo.remotes]
577 if not urls:
578 raise ValueError("Didn't find any remote url in repository at %s" % sm.abspath)
579 # END verify we have url
580 url = urls[0]
581 else:
582 # Clone new repo.
583 kwargs: Dict[str, Union[bool, int, str, Sequence[TBD]]] = {"n": no_checkout}
584 if not branch_is_default:
585 kwargs["b"] = br.name
586 # END setup checkout-branch
587
588 if depth:
589 if isinstance(depth, int):
590 kwargs["depth"] = depth
591 else:
592 raise ValueError("depth should be an integer")
593 if clone_multi_options:
594 kwargs["multi_options"] = clone_multi_options
595
596 # _clone_repo(cls, repo, url, path, name, **kwargs):
597 mrepo = cls._clone_repo(
598 repo,
599 url,
600 path,
601 name,
602 env=env,
603 allow_unsafe_options=allow_unsafe_options,
604 allow_unsafe_protocols=allow_unsafe_protocols,
605 **kwargs,
606 )
607 # END verify url
608
609 ## See #525 for ensuring git URLs in config-files are valid under Windows.
610 url = Git.polish_url(url)
611
612 # It's important to add the URL to the parent config, to let `git submodule` know.
613 # Otherwise there is a '-' character in front of the submodule listing:
614 # a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8)
615 # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one
616 writer: Union[GitConfigParser, SectionConstraint]
617
618 with sm.repo.config_writer() as writer:
619 writer.set_value(sm_section(name), "url", url)
620
621 # Update configuration and index.
622 index = sm.repo.index
623 with sm.config_writer(index=index, write=False) as writer:
624 writer.set_value("url", url)
625 writer.set_value("path", path)
626
627 sm._url = url
628 if not branch_is_default:
629 # Store full path.
630 writer.set_value(cls.k_head_option, br.path)
631 sm._branch_path = br.path
632
633 # We deliberately assume that our head matches our index!
634 if mrepo:
635 sm.binsha = mrepo.head.commit.binsha
636 index.add([sm], write=True)
637
638 return sm
639
640 def update(
641 self,
642 recursive: bool = False,
643 init: bool = True,
644 to_latest_revision: bool = False,
645 progress: Union["UpdateProgress", None] = None,
646 dry_run: bool = False,
647 force: bool = False,
648 keep_going: bool = False,
649 env: Union[Mapping[str, str], None] = None,
650 clone_multi_options: Union[Sequence[TBD], None] = None,
651 allow_unsafe_options: bool = False,
652 allow_unsafe_protocols: bool = False,
653 ) -> "Submodule":
654 """Update the repository of this submodule to point to the checkout we point at
655 with the binsha of this instance.
656
657 :param recursive:
658 If ``True``, we will operate recursively and update child modules as well.
659
660 :param init:
661 If ``True``, the module repository will be cloned into place if necessary.
662
663 :param to_latest_revision:
664 If ``True``, the submodule's sha will be ignored during checkout. Instead,
665 the remote will be fetched, and the local tracking branch updated. This only
666 works if we have a local tracking branch, which is the case if the remote
667 repository had a master branch, or if the ``branch`` option was specified
668 for this submodule and the branch existed remotely.
669
670 :param progress:
671 :class:`UpdateProgress` instance, or ``None`` if no progress should be
672 shown.
673
674 :param dry_run:
675 If ``True``, the operation will only be simulated, but not performed.
676 All performed operations are read-only.
677
678 :param force:
679 If ``True``, we may reset heads even if the repository in question is dirty.
680 Additionally we will be allowed to set a tracking branch which is ahead of
681 its remote branch back into the past or the location of the remote branch.
682 This will essentially 'forget' commits.
683
684 If ``False``, local tracking branches that are in the future of their
685 respective remote branches will simply not be moved.
686
687 :param keep_going:
688 If ``True``, we will ignore but log all errors, and keep going recursively.
689 Unless `dry_run` is set as well, `keep_going` could cause
690 subsequent/inherited errors you wouldn't see otherwise.
691 In conjunction with `dry_run`, it can be useful to anticipate all errors
692 when updating submodules.
693
694 :param env:
695 Optional dictionary containing the desired environment variables.
696
697 Note: Provided variables will be used to update the execution environment
698 for ``git``. If some variable is not specified in `env` and is defined in
699 attr:`os.environ`, value from attr:`os.environ` will be used.
700
701 If you want to unset some variable, consider providing the empty string as
702 its value.
703
704 :param clone_multi_options:
705 List of :manpage:`git-clone(1)` options.
706 Please see :meth:`Repo.clone <git.repo.base.Repo.clone>` for details.
707 They only take effect with the `init` option.
708
709 :param allow_unsafe_protocols:
710 Allow unsafe protocols to be used, like ``ext``.
711
712 :param allow_unsafe_options:
713 Allow unsafe options to be used, like ``--upload-pack``.
714
715 :note:
716 Does nothing in bare repositories.
717
718 :note:
719 This method is definitely not atomic if `recursive` is ``True``.
720
721 :return:
722 self
723 """
724 if self.repo.bare:
725 return self
726 # END pass in bare mode
727
728 if progress is None:
729 progress = UpdateProgress()
730 # END handle progress
731 prefix = ""
732 if dry_run:
733 prefix = "DRY-RUN: "
734 # END handle prefix
735
736 # To keep things plausible in dry-run mode.
737 if dry_run:
738 mrepo = None
739 # END init mrepo
740
741 try:
742 # ENSURE REPO IS PRESENT AND UP-TO-DATE
743 #######################################
744 try:
745 mrepo = self.module()
746 rmts = mrepo.remotes
747 len_rmts = len(rmts)
748 for i, remote in enumerate(rmts):
749 op = FETCH
750 if i == 0:
751 op |= BEGIN
752 # END handle start
753
754 progress.update(
755 op,
756 i,
757 len_rmts,
758 prefix + "Fetching remote %s of submodule %r" % (remote, self.name),
759 )
760 # ===============================
761 if not dry_run:
762 remote.fetch(progress=progress)
763 # END handle dry-run
764 # ===============================
765 if i == len_rmts - 1:
766 op |= END
767 # END handle end
768 progress.update(
769 op,
770 i,
771 len_rmts,
772 prefix + "Done fetching remote of submodule %r" % self.name,
773 )
774 # END fetch new data
775 except InvalidGitRepositoryError:
776 mrepo = None
777 if not init:
778 return self
779 # END early abort if init is not allowed
780
781 # There is no git-repository yet - but delete empty paths.
782 checkout_module_abspath = self.abspath
783 if not dry_run and osp.isdir(checkout_module_abspath):
784 try:
785 os.rmdir(checkout_module_abspath)
786 except OSError as e:
787 raise OSError(
788 "Module directory at %r does already exist and is non-empty" % checkout_module_abspath
789 ) from e
790 # END handle OSError
791 # END handle directory removal
792
793 # Don't check it out at first - nonetheless it will create a local
794 # branch according to the remote-HEAD if possible.
795 progress.update(
796 BEGIN | CLONE,
797 0,
798 1,
799 prefix
800 + "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name),
801 )
802 if not dry_run:
803 if self.url.startswith("."):
804 url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url)
805 else:
806 url = self.url
807 mrepo = self._clone_repo(
808 self.repo,
809 url,
810 self.path,
811 self.name,
812 n=True,
813 env=env,
814 multi_options=clone_multi_options,
815 allow_unsafe_options=allow_unsafe_options,
816 allow_unsafe_protocols=allow_unsafe_protocols,
817 )
818 # END handle dry-run
819 progress.update(
820 END | CLONE,
821 0,
822 1,
823 prefix + "Done cloning to %s" % checkout_module_abspath,
824 )
825
826 if not dry_run:
827 # See whether we have a valid branch to check out.
828 try:
829 mrepo = cast("Repo", mrepo)
830 # Find a remote which has our branch - we try to be flexible.
831 remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name)
832 local_branch = mkhead(mrepo, self.branch_path)
833
834 # Have a valid branch, but no checkout - make sure we can figure
835 # that out by marking the commit with a null_sha.
836 local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
837 # END initial checkout + branch creation
838
839 # Make sure HEAD is not detached.
840 mrepo.head.set_reference(
841 local_branch,
842 logmsg="submodule: attaching head to %s" % local_branch,
843 )
844 mrepo.head.reference.set_tracking_branch(remote_branch)
845 except (IndexError, InvalidGitRepositoryError):
846 _logger.warning("Failed to checkout tracking branch %s", self.branch_path)
847 # END handle tracking branch
848
849 # NOTE: Have to write the repo config file as well, otherwise the
850 # default implementation will be offended and not update the
851 # repository. Maybe this is a good way to ensure it doesn't get into
852 # our way, but we want to stay backwards compatible too... It's so
853 # redundant!
854 with self.repo.config_writer() as writer:
855 writer.set_value(sm_section(self.name), "url", self.url)
856 # END handle dry_run
857 # END handle initialization
858
859 # DETERMINE SHAS TO CHECK OUT
860 #############################
861 binsha = self.binsha
862 hexsha = self.hexsha
863 if mrepo is not None:
864 # mrepo is only set if we are not in dry-run mode or if the module
865 # existed.
866 is_detached = mrepo.head.is_detached
867 # END handle dry_run
868
869 if mrepo is not None and to_latest_revision:
870 msg_base = "Cannot update to latest revision in repository at %r as " % mrepo.working_dir
871 if not is_detached:
872 rref = mrepo.head.reference.tracking_branch()
873 if rref is not None:
874 rcommit = rref.commit
875 binsha = rcommit.binsha
876 hexsha = rcommit.hexsha
877 else:
878 _logger.error(
879 "%s a tracking branch was not set for local branch '%s'",
880 msg_base,
881 mrepo.head.reference,
882 )
883 # END handle remote ref
884 else:
885 _logger.error("%s there was no local tracking branch", msg_base)
886 # END handle detached head
887 # END handle to_latest_revision option
888
889 # Update the working tree.
890 # Handles dry_run.
891 if mrepo is not None and mrepo.head.commit.binsha != binsha:
892 # We must ensure that our destination sha (the one to point to) is in
893 # the future of our current head. Otherwise, we will reset changes that
894 # might have been done on the submodule, but were not yet pushed. We
895 # also handle the case that history has been rewritten, leaving no
896 # merge-base. In that case we behave conservatively, protecting possible
897 # changes the user had done.
898 may_reset = True
899 if mrepo.head.commit.binsha != self.NULL_BIN_SHA:
900 base_commit = mrepo.merge_base(mrepo.head.commit, hexsha)
901 if len(base_commit) == 0 or (base_commit[0] is not None and base_commit[0].hexsha == hexsha):
902 if force:
903 msg = "Will force checkout or reset on local branch that is possibly in the future of"
904 msg += " the commit it will be checked out to, effectively 'forgetting' new commits"
905 _logger.debug(msg)
906 else:
907 msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits"
908 msg %= (
909 is_detached and "checkout" or "reset",
910 mrepo.head,
911 mrepo,
912 )
913 _logger.info(msg)
914 may_reset = False
915 # END handle force
916 # END handle if we are in the future
917
918 if may_reset and not force and mrepo.is_dirty(index=True, working_tree=True, untracked_files=True):
919 raise RepositoryDirtyError(mrepo, "Cannot reset a dirty repository")
920 # END handle force and dirty state
921 # END handle empty repo
922
923 # END verify future/past
924 progress.update(
925 BEGIN | UPDWKTREE,
926 0,
927 1,
928 prefix
929 + "Updating working tree at %s for submodule %r to revision %s" % (self.path, self.name, hexsha),
930 )
931
932 if not dry_run and may_reset:
933 if is_detached:
934 # NOTE: For now we force. The user is not supposed to change
935 # detached submodules anyway. Maybe at some point this becomes
936 # an option, to properly handle user modifications - see below
937 # for future options regarding rebase and merge.
938 mrepo.git.checkout(hexsha, force=force)
939 else:
940 mrepo.head.reset(hexsha, index=True, working_tree=True)
941 # END handle checkout
942 # If we may reset/checkout.
943 progress.update(
944 END | UPDWKTREE,
945 0,
946 1,
947 prefix + "Done updating working tree for submodule %r" % self.name,
948 )
949 # END update to new commit only if needed
950 except Exception as err:
951 if not keep_going:
952 raise
953 _logger.error(str(err))
954 # END handle keep_going
955
956 # HANDLE RECURSION
957 ##################
958 if recursive:
959 # In dry_run mode, the module might not exist.
960 if mrepo is not None:
961 for submodule in self.iter_items(self.module()):
962 submodule.update(
963 recursive,
964 init,
965 to_latest_revision,
966 progress=progress,
967 dry_run=dry_run,
968 force=force,
969 keep_going=keep_going,
970 )
971 # END handle recursive update
972 # END handle dry run
973 # END for each submodule
974
975 return self
976
977 @unbare_repo
978 def move(self, module_path: PathLike, configuration: bool = True, module: bool = True) -> "Submodule":
979 """Move the submodule to a another module path. This involves physically moving
980 the repository at our current path, changing the configuration, as well as
981 adjusting our index entry accordingly.
982
983 :param module_path:
984 The path to which to move our module in the parent repository's working
985 tree, given as repository-relative or absolute path. Intermediate
986 directories will be created accordingly. If the path already exists, it must
987 be empty. Trailing (back)slashes are removed automatically.
988
989 :param configuration:
990 If ``True``, the configuration will be adjusted to let the submodule point
991 to the given path.
992
993 :param module:
994 If ``True``, the repository managed by this submodule will be moved as well.
995 If ``False``, we don't move the submodule's checkout, which may leave the
996 parent repository in an inconsistent state.
997
998 :return:
999 self
1000
1001 :raise ValueError:
1002 If the module path existed and was not empty, or was a file.
1003
1004 :note:
1005 Currently the method is not atomic, and it could leave the repository in an
1006 inconsistent state if a sub-step fails for some reason.
1007 """
1008 if module + configuration < 1:
1009 raise ValueError("You must specify to move at least the module or the configuration of the submodule")
1010 # END handle input
1011
1012 module_checkout_path = self._to_relative_path(self.repo, module_path)
1013
1014 # VERIFY DESTINATION
1015 if module_checkout_path == self.path:
1016 return self
1017 # END handle no change
1018
1019 module_checkout_abspath = join_path_native(str(self.repo.working_tree_dir), module_checkout_path)
1020 if osp.isfile(module_checkout_abspath):
1021 raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath)
1022 # END handle target files
1023
1024 index = self.repo.index
1025 tekey = index.entry_key(module_checkout_path, 0)
1026 # if the target item already exists, fail
1027 if configuration and tekey in index.entries:
1028 raise ValueError("Index entry for target path did already exist")
1029 # END handle index key already there
1030
1031 # Remove existing destination.
1032 if module:
1033 if osp.exists(module_checkout_abspath):
1034 if len(os.listdir(module_checkout_abspath)):
1035 raise ValueError("Destination module directory was not empty")
1036 # END handle non-emptiness
1037
1038 if osp.islink(module_checkout_abspath):
1039 os.remove(module_checkout_abspath)
1040 else:
1041 os.rmdir(module_checkout_abspath)
1042 # END handle link
1043 else:
1044 # Recreate parent directories.
1045 # NOTE: renames() does that now.
1046 pass
1047 # END handle existence
1048 # END handle module
1049
1050 # Move the module into place if possible.
1051 cur_path = self.abspath
1052 renamed_module = False
1053 if module and osp.exists(cur_path):
1054 os.renames(cur_path, module_checkout_abspath)
1055 renamed_module = True
1056
1057 if osp.isfile(osp.join(module_checkout_abspath, ".git")):
1058 module_abspath = self._module_abspath(self.repo, self.path, self.name)
1059 self._write_git_file_and_module_config(module_checkout_abspath, module_abspath)
1060 # END handle git file rewrite
1061 # END move physical module
1062
1063 # Rename the index entry - we have to manipulate the index directly as git-mv
1064 # cannot be used on submodules... yeah.
1065 previous_sm_path = self.path
1066 try:
1067 if configuration:
1068 try:
1069 ekey = index.entry_key(self.path, 0)
1070 entry = index.entries[ekey]
1071 del index.entries[ekey]
1072 nentry = git.IndexEntry(entry[:3] + (module_checkout_path,) + entry[4:])
1073 index.entries[tekey] = nentry
1074 except KeyError as e:
1075 raise InvalidGitRepositoryError("Submodule's entry at %r did not exist" % (self.path)) from e
1076 # END handle submodule doesn't exist
1077
1078 # Update configuration.
1079 with self.config_writer(index=index) as writer: # Auto-write.
1080 writer.set_value("path", module_checkout_path)
1081 self.path = module_checkout_path
1082 # END handle configuration flag
1083 except Exception:
1084 if renamed_module:
1085 os.renames(module_checkout_abspath, cur_path)
1086 # END undo module renaming
1087 raise
1088 # END handle undo rename
1089
1090 # Auto-rename submodule if its name was 'default', that is, the checkout
1091 # directory.
1092 if previous_sm_path == self.name:
1093 self.rename(module_checkout_path)
1094
1095 return self
1096
1097 @unbare_repo
1098 def remove(
1099 self,
1100 module: bool = True,
1101 force: bool = False,
1102 configuration: bool = True,
1103 dry_run: bool = False,
1104 ) -> "Submodule":
1105 """Remove this submodule from the repository. This will remove our entry
1106 from the ``.gitmodules`` file and the entry in the ``.git/config`` file.
1107
1108 :param module:
1109 If ``True``, the checked out module we point to will be deleted as well. If
1110 that module is currently on a commit outside any branch in the remote, or if
1111 it is ahead of its tracking branch, or if there are modified or untracked
1112 files in its working tree, then the removal will fail. In case the removal
1113 of the repository fails for these reasons, the submodule status will not
1114 have been altered.
1115
1116 If this submodule has child modules of its own, these will be deleted prior
1117 to touching the direct submodule.
1118
1119 :param force:
1120 Enforces the deletion of the module even though it contains modifications.
1121 This basically enforces a brute-force file system based deletion.
1122
1123 :param configuration:
1124 If ``True``, the submodule is deleted from the configuration, otherwise it
1125 isn't. Although this should be enabled most of the time, this flag enables
1126 you to safely delete the repository of your submodule.
1127
1128 :param dry_run:
1129 If ``True``, we will not actually do anything, but throw the errors we would
1130 usually throw.
1131
1132 :return:
1133 self
1134
1135 :note:
1136 Doesn't work in bare repositories.
1137
1138 :note:
1139 Doesn't work atomically, as failure to remove any part of the submodule will
1140 leave an inconsistent state.
1141
1142 :raise git.exc.InvalidGitRepositoryError:
1143 Thrown if the repository cannot be deleted.
1144
1145 :raise OSError:
1146 If directories or files could not be removed.
1147 """
1148 if not (module or configuration):
1149 raise ValueError("Need to specify to delete at least the module, or the configuration")
1150 # END handle parameters
1151
1152 # Recursively remove children of this submodule.
1153 nc = 0
1154 for csm in self.children():
1155 nc += 1
1156 csm.remove(module, force, configuration, dry_run)
1157 del csm
1158
1159 if configuration and not dry_run and nc > 0:
1160 # Ensure we don't leave the parent repository in a dirty state, and commit
1161 # our changes. It's important for recursive, unforced, deletions to work as
1162 # expected.
1163 self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name)
1164 # END handle recursion
1165
1166 # DELETE REPOSITORY WORKING TREE
1167 ################################
1168 if module and self.module_exists():
1169 mod = self.module()
1170 git_dir = mod.git_dir
1171 if force:
1172 # Take the fast lane and just delete everything in our module path.
1173 # TODO: If we run into permission problems, we have a highly
1174 # inconsistent state. Delete the .git folders last, start with the
1175 # submodules first.
1176 mp = self.abspath
1177 method: Union[None, Callable[[PathLike], None]] = None
1178 if osp.islink(mp):
1179 method = os.remove
1180 elif osp.isdir(mp):
1181 method = rmtree
1182 elif osp.exists(mp):
1183 raise AssertionError("Cannot forcibly delete repository as it was neither a link, nor a directory")
1184 # END handle brutal deletion
1185 if not dry_run:
1186 assert method
1187 method(mp)
1188 # END apply deletion method
1189 else:
1190 # Verify we may delete our module.
1191 if mod.is_dirty(index=True, working_tree=True, untracked_files=True):
1192 raise InvalidGitRepositoryError(
1193 "Cannot delete module at %s with any modifications, unless force is specified"
1194 % mod.working_tree_dir
1195 )
1196 # END check for dirt
1197
1198 # Figure out whether we have new commits compared to the remotes.
1199 # NOTE: If the user pulled all the time, the remote heads might not have
1200 # been updated, so commits coming from the remote look as if they come
1201 # from us. But we stay strictly read-only and don't fetch beforehand.
1202 for remote in mod.remotes:
1203 num_branches_with_new_commits = 0
1204 rrefs = remote.refs
1205 for rref in rrefs:
1206 num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
1207 # END for each remote ref
1208 # Not a single remote branch contained all our commits.
1209 if len(rrefs) and num_branches_with_new_commits == len(rrefs):
1210 raise InvalidGitRepositoryError(
1211 "Cannot delete module at %s as there are new commits" % mod.working_tree_dir
1212 )
1213 # END handle new commits
1214 # We have to manually delete some references to allow resources to
1215 # be cleaned up immediately when we are done with them, because
1216 # Python's scoping is no more granular than the whole function (loop
1217 # bodies are not scopes). When the objects stay alive longer, they
1218 # can keep handles open. On Windows, this is a problem.
1219 if len(rrefs):
1220 del rref # skipcq: PYL-W0631
1221 # END handle remotes
1222 del rrefs
1223 del remote
1224 # END for each remote
1225
1226 # Finally delete our own submodule.
1227 if not dry_run:
1228 self._clear_cache()
1229 wtd = mod.working_tree_dir
1230 del mod # Release file-handles (Windows).
1231 gc.collect()
1232 rmtree(str(wtd))
1233 # END delete tree if possible
1234 # END handle force
1235
1236 if not dry_run and osp.isdir(git_dir):
1237 self._clear_cache()
1238 rmtree(git_dir)
1239 # END handle separate bare repository
1240 # END handle module deletion
1241
1242 # Void our data so as not to delay invalid access.
1243 if not dry_run:
1244 self._clear_cache()
1245
1246 # DELETE CONFIGURATION
1247 ######################
1248 if configuration and not dry_run:
1249 # First the index-entry.
1250 parent_index = self.repo.index
1251 try:
1252 del parent_index.entries[parent_index.entry_key(self.path, 0)]
1253 except KeyError:
1254 pass
1255 # END delete entry
1256 parent_index.write()
1257
1258 # Now git config - we need the config intact, otherwise we can't query
1259 # information anymore.
1260
1261 with self.repo.config_writer() as gcp_writer:
1262 gcp_writer.remove_section(sm_section(self.name))
1263
1264 with self.config_writer() as sc_writer:
1265 sc_writer.remove_section()
1266 # END delete configuration
1267
1268 return self
1269
1270 def set_parent_commit(self, commit: Union[Commit_ish, str, None], check: bool = True) -> "Submodule":
1271 """Set this instance to use the given commit whose tree is supposed to
1272 contain the ``.gitmodules`` blob.
1273
1274 :param commit:
1275 Commit-ish reference pointing at the root tree, or ``None`` to always point
1276 to the most recent commit.
1277
1278 :param check:
1279 If ``True``, relatively expensive checks will be performed to verify
1280 validity of the submodule.
1281
1282 :raise ValueError:
1283 If the commit's tree didn't contain the ``.gitmodules`` blob.
1284
1285 :raise ValueError:
1286 If the parent commit didn't store this submodule under the current path.
1287
1288 :return:
1289 self
1290 """
1291 if commit is None:
1292 self._parent_commit = None
1293 return self
1294 # END handle None
1295 pcommit = self.repo.commit(commit)
1296 pctree = pcommit.tree
1297 if self.k_modules_file not in pctree:
1298 raise ValueError("Tree of commit %s did not contain the %s file" % (commit, self.k_modules_file))
1299 # END handle exceptions
1300
1301 prev_pc = self._parent_commit
1302 self._parent_commit = pcommit
1303
1304 if check:
1305 parser = self._config_parser(self.repo, self._parent_commit, read_only=True)
1306 if not parser.has_section(sm_section(self.name)):
1307 self._parent_commit = prev_pc
1308 raise ValueError("Submodule at path %r did not exist in parent commit %s" % (self.path, commit))
1309 # END handle submodule did not exist
1310 # END handle checking mode
1311
1312 # Update our sha, it could have changed.
1313 # If check is False, we might see a parent-commit that doesn't even contain the
1314 # submodule anymore. in that case, mark our sha as being NULL.
1315 try:
1316 self.binsha = pctree[str(self.path)].binsha
1317 except KeyError:
1318 self.binsha = self.NULL_BIN_SHA
1319
1320 self._clear_cache()
1321 return self
1322
1323 @unbare_repo
1324 def config_writer(
1325 self, index: Union["IndexFile", None] = None, write: bool = True
1326 ) -> SectionConstraint["SubmoduleConfigParser"]:
1327 """
1328 :return:
1329 A config writer instance allowing you to read and write the data belonging
1330 to this submodule into the ``.gitmodules`` file.
1331
1332 :param index:
1333 If not ``None``, an :class:`~git.index.base.IndexFile` instance which should
1334 be written. Defaults to the index of the :class:`Submodule`'s parent
1335 repository.
1336
1337 :param write:
1338 If ``True``, the index will be written each time a configuration value changes.
1339
1340 :note:
1341 The parameters allow for a more efficient writing of the index, as you can
1342 pass in a modified index on your own, prevent automatic writing, and write
1343 yourself once the whole operation is complete.
1344
1345 :raise ValueError:
1346 If trying to get a writer on a parent_commit which does not match the
1347 current head commit.
1348
1349 :raise IOError:
1350 If the ``.gitmodules`` file/blob could not be read.
1351 """
1352 writer = self._config_parser_constrained(read_only=False)
1353 if index is not None:
1354 writer.config._index = index
1355 writer.config._auto_write = write
1356 return writer
1357
1358 @unbare_repo
1359 def rename(self, new_name: str) -> "Submodule":
1360 """Rename this submodule.
1361
1362 :note:
1363 This method takes care of renaming the submodule in various places, such as:
1364
1365 * ``$parent_git_dir / config``
1366 * ``$working_tree_dir / .gitmodules``
1367 * (git >= v1.8.0: move submodule repository to new name)
1368
1369 As ``.gitmodules`` will be changed, you would need to make a commit afterwards.
1370 The changed ``.gitmodules`` file will already be added to the index.
1371
1372 :return:
1373 This :class:`Submodule` instance
1374 """
1375 if self.name == new_name:
1376 return self
1377
1378 # .git/config
1379 with self.repo.config_writer() as pw:
1380 # As we ourselves didn't write anything about submodules into the parent
1381 # .git/config, we will not require it to exist, and just ignore missing
1382 # entries.
1383 if pw.has_section(sm_section(self.name)):
1384 pw.rename_section(sm_section(self.name), sm_section(new_name))
1385
1386 # .gitmodules
1387 with self.config_writer(write=True).config as cw:
1388 cw.rename_section(sm_section(self.name), sm_section(new_name))
1389
1390 self._name = new_name
1391
1392 # .git/modules
1393 mod = self.module()
1394 if mod.has_separate_working_tree():
1395 destination_module_abspath = self._module_abspath(self.repo, self.path, new_name)
1396 source_dir = mod.git_dir
1397 # Let's be sure the submodule name is not so obviously tied to a directory.
1398 if str(destination_module_abspath).startswith(str(mod.git_dir)):
1399 tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4()))
1400 os.renames(source_dir, tmp_dir)
1401 source_dir = tmp_dir
1402 # END handle self-containment
1403 os.renames(source_dir, destination_module_abspath)
1404 if mod.working_tree_dir:
1405 self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath)
1406 # END move separate git repository
1407
1408 return self
1409
1410 # } END edit interface
1411
1412 # { Query Interface
1413
1414 @unbare_repo
1415 def module(self) -> "Repo":
1416 """
1417 :return:
1418 :class:`~git.repo.base.Repo` instance initialized from the repository at our
1419 submodule path
1420
1421 :raise git.exc.InvalidGitRepositoryError:
1422 If a repository was not available.
1423 This could also mean that it was not yet initialized.
1424 """
1425 module_checkout_abspath = self.abspath
1426 try:
1427 repo = git.Repo(module_checkout_abspath)
1428 if repo != self.repo:
1429 return repo
1430 # END handle repo uninitialized
1431 except (InvalidGitRepositoryError, NoSuchPathError) as e:
1432 raise InvalidGitRepositoryError("No valid repository at %s" % module_checkout_abspath) from e
1433 else:
1434 raise InvalidGitRepositoryError("Repository at %r was not yet checked out" % module_checkout_abspath)
1435 # END handle exceptions
1436
1437 def module_exists(self) -> bool:
1438 """
1439 :return:
1440 ``True`` if our module exists and is a valid git repository.
1441 See the :meth:`module` method.
1442 """
1443 try:
1444 self.module()
1445 return True
1446 except Exception:
1447 return False
1448 # END handle exception
1449
1450 def exists(self) -> bool:
1451 """
1452 :return:
1453 ``True`` if the submodule exists, ``False`` otherwise.
1454 Please note that a submodule may exist (in the ``.gitmodules`` file) even
1455 though its module doesn't exist on disk.
1456 """
1457 # Keep attributes for later, and restore them if we have no valid data.
1458 # This way we do not actually alter the state of the object.
1459 loc = locals()
1460 for attr in self._cache_attrs:
1461 try:
1462 if hasattr(self, attr):
1463 loc[attr] = getattr(self, attr)
1464 # END if we have the attribute cache
1465 except (cp.NoSectionError, ValueError):
1466 # On PY3, this can happen apparently... don't know why this doesn't
1467 # happen on PY2.
1468 pass
1469 # END for each attr
1470 self._clear_cache()
1471
1472 try:
1473 try:
1474 self.path # noqa: B018
1475 return True
1476 except Exception:
1477 return False
1478 # END handle exceptions
1479 finally:
1480 for attr in self._cache_attrs:
1481 if attr in loc:
1482 setattr(self, attr, loc[attr])
1483 # END if we have a cache
1484 # END reapply each attribute
1485 # END handle object state consistency
1486
1487 @property
1488 def branch(self) -> "Head":
1489 """
1490 :return:
1491 The branch instance that we are to checkout
1492
1493 :raise git.exc.InvalidGitRepositoryError:
1494 If our module is not yet checked out.
1495 """
1496 return mkhead(self.module(), self._branch_path)
1497
1498 @property
1499 def branch_path(self) -> PathLike:
1500 """
1501 :return:
1502 Full repository-relative path as string to the branch we would checkout from
1503 the remote and track
1504 """
1505 return self._branch_path
1506
1507 @property
1508 def branch_name(self) -> str:
1509 """
1510 :return:
1511 The name of the branch, which is the shortest possible branch name
1512 """
1513 # Use an instance method, for this we create a temporary Head instance which
1514 # uses a repository that is available at least (it makes no difference).
1515 return git.Head(self.repo, self._branch_path).name
1516
1517 @property
1518 def url(self) -> str:
1519 """:return: The url to the repository our submodule's repository refers to"""
1520 return self._url
1521
1522 @property
1523 def parent_commit(self) -> "Commit":
1524 """
1525 :return:
1526 :class:`~git.objects.commit.Commit` instance with the tree containing the
1527 ``.gitmodules`` file
1528
1529 :note:
1530 Will always point to the current head's commit if it was not set explicitly.
1531 """
1532 if self._parent_commit is None:
1533 return self.repo.commit()
1534 return self._parent_commit
1535
1536 @property
1537 def name(self) -> str:
1538 """
1539 :return:
1540 The name of this submodule. It is used to identify it within the
1541 ``.gitmodules`` file.
1542
1543 :note:
1544 By default, this is the name is the path at which to find the submodule, but
1545 in GitPython it should be a unique identifier similar to the identifiers
1546 used for remotes, which allows to change the path of the submodule easily.
1547 """
1548 return self._name
1549
1550 def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]:
1551 """
1552 :return:
1553 ConfigReader instance which allows you to query the configuration values of
1554 this submodule, as provided by the ``.gitmodules`` file.
1555
1556 :note:
1557 The config reader will actually read the data directly from the repository
1558 and thus does not need nor care about your working tree.
1559
1560 :note:
1561 Should be cached by the caller and only kept as long as needed.
1562
1563 :raise IOError:
1564 If the ``.gitmodules`` file/blob could not be read.
1565 """
1566 return self._config_parser_constrained(read_only=True)
1567
1568 def children(self) -> IterableList["Submodule"]:
1569 """
1570 :return:
1571 IterableList(Submodule, ...) An iterable list of :class:`Submodule`
1572 instances which are children of this submodule or 0 if the submodule is not
1573 checked out.
1574 """
1575 return self._get_intermediate_items(self)
1576
1577 # } END query interface
1578
1579 # { Iterable Interface
1580
1581 @classmethod
1582 def iter_items(
1583 cls,
1584 repo: "Repo",
1585 parent_commit: Union[Commit_ish, str] = "HEAD",
1586 *args: Any,
1587 **kwargs: Any,
1588 ) -> Iterator["Submodule"]:
1589 """
1590 :return:
1591 Iterator yielding :class:`Submodule` instances available in the given
1592 repository
1593 """
1594 try:
1595 pc = repo.commit(parent_commit) # Parent commit instance
1596 parser = cls._config_parser(repo, pc, read_only=True)
1597 except (IOError, BadName):
1598 return
1599 # END handle empty iterator
1600
1601 for sms in parser.sections():
1602 n = sm_name(sms)
1603 p = parser.get(sms, "path")
1604 u = parser.get(sms, "url")
1605 b = cls.k_head_default
1606 if parser.has_option(sms, cls.k_head_option):
1607 b = str(parser.get(sms, cls.k_head_option))
1608 # END handle optional information
1609
1610 # Get the binsha.
1611 index = repo.index
1612 try:
1613 rt = pc.tree # Root tree
1614 sm = rt[p]
1615 except KeyError:
1616 # Try the index, maybe it was just added.
1617 try:
1618 entry = index.entries[index.entry_key(p, 0)]
1619 sm = Submodule(repo, entry.binsha, entry.mode, entry.path)
1620 except KeyError:
1621 # The submodule doesn't exist, probably it wasn't removed from the
1622 # .gitmodules file.
1623 continue
1624 # END handle keyerror
1625 # END handle critical error
1626
1627 # Make sure we are looking at a submodule object.
1628 if type(sm) is not git.objects.submodule.base.Submodule:
1629 continue
1630
1631 # Fill in remaining info - saves time as it doesn't have to be parsed again.
1632 sm._name = n
1633 if pc != repo.commit():
1634 sm._parent_commit = pc
1635 # END set only if not most recent!
1636 sm._branch_path = git.Head.to_full_path(b)
1637 sm._url = u
1638
1639 yield sm
1640 # END for each section
1641
1642 # } END iterable interface