Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/objects/submodule/base.py: 40%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

721 statements  

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