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