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