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