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