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