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