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

734 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 uuid 

16from io import BytesIO 

17from pathlib import Path 

18 

19# typing ---------------------------------------------------------------------- 

20from typing import ( 

21 TYPE_CHECKING, 

22 Any, 

23 Callable, 

24 Dict, 

25 Iterator, 

26 List, 

27 Literal, 

28 Mapping, 

29 Sequence, 

30 Union, 

31 cast, 

32) 

33 

34import git 

35from git.cmd import Git 

36from git.compat import defenc 

37from git.config import GitConfigParser, SectionConstraint, cp 

38from git.exc import ( 

39 BadName, 

40 InvalidGitRepositoryError, 

41 NoSuchPathError, 

42 RepositoryDirtyError, 

43) 

44from git.objects.base import IndexObject, Object 

45from git.objects.util import TraversableIterableObj 

46from git.util import ( 

47 IterableList, 

48 RemoteProgress, 

49 _to_relative_path, 

50 _validate_repo_path, 

51 join_path_native, 

52 rmtree, 

53 to_native_path_linux, 

54 unbare_repo, 

55) 

56 

57from .util import ( 

58 SubmoduleConfigParser, 

59 _is_absolute_path, 

60 _url_is_local_not_ssh, 

61 find_first_remote_branch, 

62 is_relative_url, 

63 mkhead, 

64 resolve_relative_url, 

65 sm_name, 

66 sm_section, 

67) 

68 

69from git.types import TBD, Commit_ish, PathLike 

70 

71if TYPE_CHECKING: 

72 from git.index import IndexFile 

73 from git.objects.commit import Commit 

74 from git.refs import Head 

75 from git.repo import Repo 

76 

77# ----------------------------------------------------------------------------- 

78 

79_logger = logging.getLogger(__name__) 

80 

81 

82class UpdateProgress(RemoteProgress): 

83 """Class providing detailed progress information to the caller who should 

84 derive from it and implement the 

85 :meth:`update(...) <git.util.RemoteProgress.update>` message.""" 

86 

87 CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)] 

88 _num_op_codes: int = RemoteProgress._num_op_codes + 3 

89 

90 __slots__ = () 

91 

92 

93BEGIN = UpdateProgress.BEGIN 

94END = UpdateProgress.END 

95CLONE = UpdateProgress.CLONE 

96FETCH = UpdateProgress.FETCH 

97UPDWKTREE = UpdateProgress.UPDWKTREE 

98 

99 

100# IndexObject comes via the util module. It's a 'hacky' fix thanks to Python's import 

101# mechanism, which causes plenty of trouble if the only reason for packages and modules 

102# is refactoring - subpackages shouldn't depend on parent packages. 

103class Submodule(IndexObject, TraversableIterableObj): 

104 """Implements access to a git submodule. They are special in that their sha 

105 represents a commit in the submodule's repository which is to be checked out 

106 at the path of this instance. 

107 

108 The submodule type does not have a string type associated with it, as it exists 

109 solely as a marker in the tree and index. 

110 

111 All methods work in bare and non-bare repositories. 

112 """ 

113 

114 _id_attribute_ = "name" 

115 k_modules_file = ".gitmodules" 

116 k_head_option = "branch" 

117 k_head_default = "master" 

118 k_default_mode = stat.S_IFDIR | stat.S_IFLNK 

119 """Submodule flags. Submodules are directories with link-status.""" 

120 

121 type: Literal["submodule"] = "submodule" # type: ignore[assignment] 

122 """This is a bogus type string for base class compatibility.""" 

123 

124 __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__") 

125 

126 _cache_attrs = ("path", "_url", "_branch_path") 

127 

128 def __init__( 

129 self, 

130 repo: "Repo", 

131 binsha: bytes, 

132 mode: Union[int, None] = None, 

133 path: Union[PathLike, None] = None, 

134 name: Union[str, None] = None, 

135 parent_commit: Union["Commit", None] = None, 

136 url: Union[str, None] = None, 

137 branch_path: Union[PathLike, None] = None, 

138 ) -> None: 

139 """Initialize this instance with its attributes. 

140 

141 We only document the parameters that differ from 

142 :class:`~git.objects.base.IndexObject`. 

143 

144 :param repo: 

145 Our parent repository. 

146 

147 :param binsha: 

148 Binary sha referring to a commit in the remote repository. 

149 See the `url` parameter. 

150 

151 :param parent_commit: 

152 The :class:`~git.objects.commit.Commit` whose tree is supposed to contain 

153 the ``.gitmodules`` blob, or ``None`` to always point to the most recent 

154 commit. See :meth:`set_parent_commit` for details. 

155 

156 :param url: 

157 The URL to the remote repository which is the submodule. 

158 

159 :param branch_path: 

160 Full repository-relative path to ref to checkout when cloning the remote 

161 repository. 

162 """ 

163 super().__init__(repo, binsha, mode, path) 

164 self.size = 0 

165 self._parent_commit = parent_commit 

166 if url is not None: 

167 self._url = url 

168 if branch_path is not None: 

169 self._branch_path = branch_path 

170 if name is not None: 

171 self._name = name 

172 

173 def _set_cache_(self, attr: str) -> None: 

174 if attr in ("path", "_url", "_branch_path"): 

175 reader: SectionConstraint = self.config_reader() 

176 # Default submodule values. 

177 try: 

178 self.path = reader.get("path") 

179 except cp.NoSectionError as e: 

180 if self.repo.working_tree_dir is not None: 

181 raise ValueError( 

182 "This submodule instance does not exist anymore in '%s' file" 

183 % osp.join(self.repo.working_tree_dir, ".gitmodules") 

184 ) from e 

185 

186 self._url = reader.get("url") 

187 # GitPython extension values - optional. 

188 self._branch_path = reader.get_value(self.k_head_option, git.Head.to_full_path(self.k_head_default)) 

189 elif attr == "_name": 

190 raise AttributeError("Cannot retrieve the name of a submodule if it was not set initially") 

191 else: 

192 super()._set_cache_(attr) 

193 # END handle attribute name 

194 

195 @classmethod 

196 def _get_intermediate_items(cls, item: "Submodule") -> IterableList["Submodule"]: 

197 """:return: All the submodules of our module repository""" 

198 try: 

199 return cls.list_items(item.module()) 

200 except InvalidGitRepositoryError: 

201 return IterableList("") 

202 # END handle intermediate items 

203 

204 @classmethod 

205 def _need_gitfile_submodules(cls, git: Git) -> bool: 

206 return git.version_info[:3] >= (1, 7, 5) 

207 

208 def __eq__(self, other: Any) -> bool: 

209 """Compare with another submodule.""" 

210 # We may only compare by name as this should be the ID they are hashed with. 

211 # Otherwise this type wouldn't be hashable. 

212 # return self.path == other.path and self.url == other.url and super().__eq__(other) 

213 return self._name == other._name 

214 

215 def __ne__(self, other: object) -> bool: 

216 """Compare with another submodule for inequality.""" 

217 return not (self == other) 

218 

219 def __hash__(self) -> int: 

220 """Hash this instance using its logical id, not the sha.""" 

221 return hash(self._name) 

222 

223 def __str__(self) -> str: 

224 return self._name 

225 

226 def __repr__(self) -> str: 

227 return "git.%s(name=%s, path=%s, url=%s, branch_path=%s)" % ( 

228 type(self).__name__, 

229 self._name, 

230 self.path, 

231 self.url, 

232 self.branch_path, 

233 ) 

234 

235 @classmethod 

236 def _config_parser( 

237 cls, repo: "Repo", parent_commit: Union["Commit", None], read_only: bool 

238 ) -> SubmoduleConfigParser: 

239 """ 

240 :return: 

241 Config parser constrained to our submodule in read or write mode 

242 

243 :raise IOError: 

244 If the ``.gitmodules`` file cannot be found, either locally or in the 

245 repository at the given parent commit. Otherwise the exception would be 

246 delayed until the first access of the config parser. 

247 """ 

248 parent_matches_head = True 

249 if parent_commit is not None: 

250 try: 

251 parent_matches_head = repo.head.commit == parent_commit 

252 except ValueError: 

253 # We are most likely in an empty repository, so the HEAD doesn't point 

254 # to a valid ref. 

255 pass 

256 # END handle parent_commit 

257 fp_module: Union[str, BytesIO] 

258 if not repo.bare and parent_matches_head and repo.working_tree_dir: 

259 fp_module = cls._checked_abspath(repo.working_tree_dir, cls.k_modules_file) 

260 else: 

261 assert parent_commit is not None, "need valid parent_commit in bare repositories" 

262 try: 

263 fp_module = cls._sio_modules(parent_commit) 

264 except KeyError as e: 

265 raise OSError( 

266 "Could not find %s file in the tree of parent commit %s" % (cls.k_modules_file, parent_commit) 

267 ) from e 

268 # END handle exceptions 

269 # END handle non-bare working tree 

270 

271 if not read_only and (repo.bare or not parent_matches_head): 

272 raise ValueError("Cannot write blobs of 'historical' submodule configurations") 

273 # END handle writes of historical submodules 

274 

275 return SubmoduleConfigParser(fp_module, read_only=read_only, merge_includes=False) 

276 

277 def _clear_cache(self) -> None: 

278 """Clear the possibly changed values.""" 

279 for name in self._cache_attrs: 

280 try: 

281 delattr(self, name) 

282 except AttributeError: 

283 pass 

284 # END try attr deletion 

285 # END for each name to delete 

286 

287 @classmethod 

288 def _sio_modules(cls, parent_commit: "Commit") -> BytesIO: 

289 """ 

290 :return: 

291 Configuration file as :class:`~io.BytesIO` - we only access it through the 

292 respective blob's data 

293 """ 

294 sio = BytesIO(parent_commit.tree[cls.k_modules_file].data_stream.read()) 

295 sio.name = cls.k_modules_file 

296 return sio 

297 

298 def _config_parser_constrained(self, read_only: bool) -> SectionConstraint: 

299 """:return: Config parser constrained to our submodule in read or write mode""" 

300 try: 

301 pc = self.parent_commit 

302 except ValueError: 

303 pc = None 

304 # END handle empty parent repository 

305 parser = self._config_parser(self.repo, pc, read_only) 

306 parser.set_submodule(self) 

307 return SectionConstraint(parser, sm_section(self.name)) 

308 

309 @classmethod 

310 def _validated_name(cls, name: str) -> str: 

311 if ( 

312 not name 

313 or "\0" in name 

314 or name.startswith(("/", "\\")) 

315 or ntpath.splitdrive(name)[0] 

316 or ".." in name.replace("\\", "/").split("/") 

317 ): 

318 raise ValueError("Invalid submodule name %r" % name) 

319 cls._validate_windows_path(name) 

320 return name 

321 

322 @staticmethod 

323 def _validate_windows_path(path: PathLike) -> None: 

324 """Apply Git for Windows' filename checks before creating directories.""" 

325 if sys.platform == "win32": 

326 for component in ntpath.splitdrive(os.fspath(path))[1].replace("\\", "/").split("/"): 

327 if component in (".", ".."): 

328 continue 

329 stem = component.split(".", 1)[0].rstrip(" ").upper() 

330 if ( 

331 component.endswith((" ", ".")) 

332 or any(ord(char) < 32 or char in '<>:"|?*' for char in component) 

333 or re.fullmatch(r"CON(?:IN\$|OUT\$)?|PRN|AUX|NUL|COM[1-9]|LPT[1-9]", stem) 

334 ): 

335 raise ValueError("Invalid submodule path on Windows: %r" % path) 

336 

337 @classmethod 

338 def _module_abspath( 

339 cls, parent_repo: "Repo", path: PathLike, name: str, *, moving_from: Union[PathLike, None] = None 

340 ) -> PathLike: 

341 """Reject nested Git directories, allowing the source of a pending rename.""" 

342 from git.repo.fun import is_git_dir 

343 

344 name = cls._validated_name(name) 

345 if cls._need_gitfile_submodules(parent_repo.git): 

346 directory = osp.join(parent_repo.git_dir, "modules") 

347 for component in to_native_path_linux(name).split("/")[:-1]: 

348 directory = osp.join(directory, component) 

349 if is_git_dir(directory) and ( 

350 moving_from is None or Path(directory).resolve() != Path(moving_from).resolve() 

351 ): 

352 raise ValueError( 

353 "Submodule metadata for %r is inside another Git directory: %r" % (name, directory) 

354 ) 

355 return osp.join(parent_repo.git_dir, "modules", name) 

356 if parent_repo.working_tree_dir: 

357 return cls._checked_abspath( 

358 parent_repo.working_tree_dir, 

359 cls._to_relative_path(parent_repo, path), 

360 git_dirs=(parent_repo.git_dir, parent_repo.common_dir), 

361 ) 

362 raise NotADirectoryError() 

363 

364 @classmethod 

365 def _clone_repo( 

366 cls, 

367 repo: "Repo", 

368 url: str, 

369 path: PathLike, 

370 name: str, 

371 allow_unsafe_options: bool = False, 

372 allow_unsafe_protocols: bool = False, 

373 **kwargs: Any, 

374 ) -> "Repo": 

375 """ 

376 :return: 

377 :class:`~git.repo.base.Repo` instance of newly cloned repository. 

378 

379 :param repo: 

380 Our parent repository. 

381 

382 :param url: 

383 URL to clone from. 

384 

385 :param path: 

386 Repository-relative path to the submodule checkout location. 

387 

388 :param name: 

389 Canonical name of the submodule. 

390 

391 :param allow_unsafe_protocols: 

392 Allow unsafe protocols to be used, like ``ext``. 

393 

394 :param allow_unsafe_options: 

395 Allow unsafe options to be used, like ``--upload-pack``. 

396 

397 :param kwargs: 

398 Additional arguments given to :manpage:`git-clone(1)`. 

399 """ 

400 path = cls._to_relative_path(repo, path) 

401 if repo.working_tree_dir is None: 

402 raise NotADirectoryError("Submodules require a working tree") 

403 module_checkout_path = cls._checked_abspath( 

404 repo.working_tree_dir, path, git_dirs=(repo.git_dir, repo.common_dir) 

405 ) 

406 module_abspath = cls._module_abspath(repo, path, name) 

407 if cls._need_gitfile_submodules(repo.git): 

408 if not allow_unsafe_options: 

409 Git.check_unsafe_options(Git._option_candidates([], kwargs), repo.unsafe_git_clone_options) 

410 multi_options = kwargs.get("multi_options") 

411 if multi_options: 

412 Git.check_unsafe_options( 

413 shlex.split(" ".join(cast("Sequence[str]", multi_options))), 

414 repo.unsafe_git_clone_options, 

415 ) 

416 allow_unsafe_options = True 

417 if osp.islink(module_abspath): 

418 # Clone into the target while retaining the metadata alias. Git for 

419 # Windows cannot initialize through a dangling directory symlink. 

420 # Read the link explicitly for Python 3.7, and remove the Windows 

421 # namespace prefix returned by newer Python versions for Git. 

422 target = os.readlink(module_abspath) 

423 if sys.platform == "win32": 

424 if target.startswith("\\\\?\\UNC\\"): 

425 target = "\\\\" + target[8:] 

426 elif target.startswith("\\\\?\\"): 

427 target = target[4:] 

428 module_abspath = to_native_path_linux(osp.join(osp.dirname(module_abspath), target)) 

429 kwargs["separate_git_dir"] = module_abspath 

430 module_abspath_dir = osp.dirname(module_abspath) 

431 if not osp.isdir(module_abspath_dir): 

432 os.makedirs(module_abspath_dir) 

433 

434 clone = git.Repo.clone_from( 

435 cls._resolve_url(repo, url), 

436 module_checkout_path, 

437 allow_unsafe_options=allow_unsafe_options, 

438 allow_unsafe_protocols=allow_unsafe_protocols, 

439 **kwargs, 

440 ) 

441 if cls._need_gitfile_submodules(repo.git): 

442 # A concurrent clone may have turned a leading directory into a repository. 

443 try: 

444 cls._module_abspath(repo, path, name) 

445 except ValueError: 

446 clone.close() 

447 try: 

448 os.remove(osp.join(clone.git_dir, "HEAD")) 

449 except FileNotFoundError: 

450 pass 

451 raise 

452 cls._write_git_file_and_module_config(module_checkout_path, module_abspath) 

453 

454 return clone 

455 

456 @classmethod 

457 def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike: 

458 """:return: A path guaranteed to be relative to the given parent repository 

459 

460 :raise ValueError: 

461 If path is outside the working tree or is unsafe as a submodule checkout. 

462 """ 

463 cls._validate_windows_path(path) 

464 if parent_repo.working_tree_dir: 

465 path = _to_relative_path(parent_repo.working_tree_dir, path) 

466 else: 

467 path = to_native_path_linux(path) 

468 if path.endswith("/"): 

469 path = path[:-1] 

470 if not path or path == ".": 

471 raise ValueError("Submodule checkout path must not be the repository root") 

472 

473 _validate_repo_path(path) 

474 return path 

475 

476 @property 

477 def abspath(self) -> PathLike: 

478 root = self.repo.working_tree_dir 

479 if root is None: 

480 return super().abspath 

481 return self._checkout_abspath(self._to_relative_path(self.repo, self.path)) 

482 

483 def _checkout_abspath(self, relative_path: PathLike, allow_final_symlink: bool = False) -> PathLike: 

484 """Check a checkout path already normalized by :meth:`_to_relative_path`.""" 

485 return self._checked_abspath( 

486 self.repo.working_tree_dir, 

487 relative_path, 

488 allow_final_symlink, 

489 git_dirs=(self.repo.git_dir, self.repo.common_dir), 

490 ) 

491 

492 @classmethod 

493 def _checked_abspath( 

494 cls, 

495 root: Union[PathLike, None], 

496 relative_path: PathLike, 

497 allow_final_symlink: bool = False, 

498 *, 

499 git_dirs: Sequence[PathLike] = (), 

500 ) -> str: 

501 """Reject symlinks and checkout aliases of Git directories below a trusted root.""" 

502 if root is None: 

503 raise NotADirectoryError("Submodules require a working tree") 

504 path = os.fspath(root) 

505 metadata_dirs = set(git_dirs) 

506 components = to_native_path_linux(relative_path).split("/") 

507 for index, component in enumerate(components): 

508 path = os.fspath(join_path_native(path, component)) 

509 if metadata_dirs and osp.exists(path) and any(osp.samefile(path, directory) for directory in metadata_dirs): 

510 raise ValueError("Submodule checkout path aliases Git metadata: %r" % relative_path) 

511 if osp.islink(path): 

512 if allow_final_symlink and index == len(components) - 1: 

513 break 

514 raise ValueError("Submodule path %r contains a symbolic link" % relative_path) 

515 return path 

516 

517 @staticmethod 

518 def _renames(source: PathLike, destination: PathLike) -> None: 

519 os.makedirs(osp.dirname(destination), exist_ok=True) 

520 os.rename(source, destination) 

521 # Match renames() cleanup, but stop before directory symlinks: Windows 

522 # rmdir() removes the link even when its target is nonempty. 

523 parent = osp.dirname(source) 

524 while parent and not osp.islink(parent): 

525 try: 

526 os.rmdir(parent) 

527 except OSError: 

528 break 

529 parent = osp.dirname(parent) 

530 

531 @classmethod 

532 def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None: 

533 """Write a ``.git`` file containing a (preferably) relative path to the actual 

534 git module repository. 

535 

536 It is an error if the `module_abspath` cannot be made into a relative path, 

537 relative to the `working_tree_dir`. 

538 

539 :note: 

540 This will overwrite existing files! 

541 

542 :note: 

543 As we rewrite both the git file as well as the module configuration, we 

544 might fail on the configuration and will not roll back changes done to the 

545 git file. This should be a non-issue, but may easily be fixed if it becomes 

546 one. 

547 

548 :param working_tree_dir: 

549 Directory to write the ``.git`` file into. 

550 

551 :param module_abspath: 

552 Absolute path to the bare repository. 

553 """ 

554 # Git resolves metadata symlinks before interpreting core.worktree. 

555 # Path.resolve() also handles Windows symlinks on Python 3.7. 

556 module_abspath = str(Path(module_abspath).resolve()) 

557 working_tree_dir = str(Path(working_tree_dir).resolve()) 

558 git_file = osp.join(working_tree_dir, ".git") 

559 module_config = osp.join(module_abspath, "config") 

560 rela_path = osp.relpath(module_abspath, start=working_tree_dir) 

561 if sys.platform == "win32" and osp.isfile(git_file): 

562 os.remove(git_file) 

563 with open(git_file, "wb") as fp: 

564 fp.write(("gitdir: %s" % rela_path).encode(defenc)) 

565 

566 with GitConfigParser(module_config, read_only=False, merge_includes=False) as writer: 

567 writer.set_value( 

568 "core", 

569 "worktree", 

570 to_native_path_linux(osp.relpath(working_tree_dir, start=module_abspath)), 

571 ) 

572 

573 # { Edit Interface 

574 

575 @classmethod 

576 def add( 

577 cls, 

578 repo: "Repo", 

579 name: str, 

580 path: PathLike, 

581 url: Union[str, None] = None, 

582 branch: Union[str, None] = None, 

583 no_checkout: bool = False, 

584 depth: Union[int, None] = None, 

585 env: Union[Mapping[str, str], None] = None, 

586 clone_multi_options: Union[Sequence[TBD], None] = None, 

587 allow_unsafe_options: bool = False, 

588 allow_unsafe_protocols: bool = False, 

589 ) -> "Submodule": 

590 """Add a new submodule to the given repository. This will alter the index as 

591 well as the ``.gitmodules`` file, but will not create a new commit. If the 

592 submodule already exists, no matter if the configuration differs from the one 

593 provided, the existing submodule will be returned. 

594 

595 :param repo: 

596 Repository instance which should receive the submodule. 

597 

598 :param name: 

599 The name/identifier for the submodule. 

600 

601 :param path: 

602 Repository-relative or absolute path at which the submodule should be 

603 located. 

604 It will be created as required during the repository initialization. 

605 

606 :param url: 

607 ``git clone ...``-compatible URL. See :manpage:`git-clone(1)` for more 

608 information. If ``None``, the repository is assumed to exist, and the URL of 

609 the first remote is taken instead. This is useful if you want to make an 

610 existing repository a submodule of another one. 

611 

612 :param branch: 

613 Name of branch at which the submodule should (later) be checked out. The 

614 given branch must exist in the remote repository, and will be checked out 

615 locally as a tracking branch. 

616 It will only be written into the configuration if it not ``None``, which is 

617 when the checked out branch will be the one the remote HEAD pointed to. 

618 The result you get in these situation is somewhat fuzzy, and it is 

619 recommended to specify at least ``master`` here. 

620 Examples are ``master`` or ``feature/new``. 

621 

622 :param no_checkout: 

623 If ``True``, and if the repository has to be cloned manually, no checkout 

624 will be performed. 

625 

626 :param depth: 

627 Create a shallow clone with a history truncated to the specified number of 

628 commits. 

629 

630 :param env: 

631 Optional dictionary containing the desired environment variables. 

632 

633 Note: Provided variables will be used to update the execution environment 

634 for ``git``. If some variable is not specified in `env` and is defined in 

635 attr:`os.environ`, the value from attr:`os.environ` will be used. If you 

636 want to unset some variable, consider providing an empty string as its 

637 value. 

638 

639 :param clone_multi_options: 

640 A list of clone options. Please see 

641 :meth:`Repo.clone <git.repo.base.Repo.clone>` for details. 

642 

643 :param allow_unsafe_protocols: 

644 Allow unsafe protocols to be used, like ``ext``. 

645 

646 :param allow_unsafe_options: 

647 Allow unsafe options to be used, like ``--upload-pack``. 

648 

649 :return: 

650 The newly created :class:`Submodule` instance. 

651 

652 :note: 

653 Works atomically, such that no change will be done if, for example, the 

654 repository update fails. 

655 """ 

656 if repo.bare: 

657 raise InvalidGitRepositoryError("Cannot add submodules to bare repositories") 

658 # END handle bare repos 

659 

660 cls._validated_name(name) 

661 path = cls._to_relative_path(repo, path) 

662 

663 # Ensure we never put backslashes into the URL, as might happen on Windows. 

664 if url is not None: 

665 url = to_native_path_linux(url) 

666 # END ensure URL correctness 

667 

668 # INSTANTIATE INTERMEDIATE SM 

669 sm = cls( 

670 repo, 

671 cls.NULL_BIN_SHA, 

672 cls.k_default_mode, 

673 path, 

674 name, 

675 url="invalid-temporary", 

676 ) 

677 cls._checked_abspath(repo.working_tree_dir, cls.k_modules_file) 

678 sm._checkout_abspath(path) 

679 if sm.exists(): 

680 # Reretrieve submodule from tree. 

681 try: 

682 sm = repo.head.commit.tree[os.fspath(path)] 

683 sm._name = name 

684 return sm 

685 except KeyError: 

686 # Could only be in index. 

687 index = repo.index 

688 entry = index.entries[index.entry_key(path, 0)] 

689 sm.binsha = entry.binsha 

690 return sm 

691 # END handle exceptions 

692 # END handle existing 

693 

694 # fake-repo - we only need the functionality on the branch instance. 

695 br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default)) 

696 has_module = sm.module_exists() 

697 branch_is_default = branch is None 

698 if has_module and url is not None: 

699 if url not in [r.url for r in sm.module().remotes]: 

700 raise ValueError( 

701 "Specified URL '%s' does not match any remote url of the repository at '%s'" % (url, sm.abspath) 

702 ) 

703 # END check url 

704 # END verify urls match 

705 

706 mrepo: Union[Repo, None] = None 

707 

708 if url is None: 

709 if not has_module: 

710 raise ValueError("A URL was not given and a repository did not exist at %s" % path) 

711 # END check url 

712 mrepo = sm.module() 

713 # assert isinstance(mrepo, git.Repo) 

714 urls = [r.url for r in mrepo.remotes] 

715 if not urls: 

716 raise ValueError("Didn't find any remote url in repository at %s" % sm.abspath) 

717 # END verify we have url 

718 url = urls[0] 

719 else: 

720 # Clone new repo. 

721 kwargs: Dict[str, Union[bool, int, str, Sequence[TBD]]] = {"n": no_checkout} 

722 if not branch_is_default: 

723 kwargs["b"] = br.name 

724 # END setup checkout-branch 

725 

726 if depth: 

727 if isinstance(depth, int): 

728 kwargs["depth"] = depth 

729 else: 

730 raise ValueError("depth should be an integer") 

731 if clone_multi_options: 

732 kwargs["multi_options"] = clone_multi_options 

733 

734 # _clone_repo(cls, repo, url, path, name, **kwargs): 

735 mrepo = cls._clone_repo( 

736 repo, 

737 url, 

738 path, 

739 name, 

740 env=env, 

741 allow_unsafe_options=allow_unsafe_options, 

742 allow_unsafe_protocols=allow_unsafe_protocols, 

743 **kwargs, 

744 ) 

745 # END verify url 

746 

747 # Relative submodule URLs must keep their prefix to resolve against the parent remote. 

748 if not is_relative_url(url): 

749 url = Git.polish_url(url, expand_vars=False) 

750 

751 # It's important to add the URL to the parent config, to let `git submodule` know. 

752 # Otherwise there is a '-' character in front of the submodule listing: 

753 # a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8) 

754 # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one 

755 writer: Union[GitConfigParser, SectionConstraint] 

756 

757 with sm.repo.config_writer() as writer: 

758 writer.set_value(sm_section(name), "url", url) 

759 

760 # Update configuration and index. 

761 index = sm.repo.index 

762 with sm.config_writer(index=index, write=False) as writer: 

763 writer.set_value("url", url) 

764 writer.set_value("path", path) 

765 

766 sm._url = url 

767 if not branch_is_default: 

768 # Store full path. 

769 writer.set_value(cls.k_head_option, br.path) 

770 sm._branch_path = br.path 

771 

772 # We deliberately assume that our head matches our index! 

773 if mrepo: 

774 # Release cat-file processes before callers move the checkout on Windows. 

775 with mrepo: 

776 sm.binsha = mrepo.head.commit.binsha 

777 index.add([sm], write=True) 

778 

779 return sm 

780 

781 def update( 

782 self, 

783 recursive: bool = False, 

784 init: bool = True, 

785 to_latest_revision: bool = False, 

786 progress: Union["UpdateProgress", None] = None, 

787 dry_run: bool = False, 

788 force: bool = False, 

789 keep_going: bool = False, 

790 env: Union[Mapping[str, str], None] = None, 

791 clone_multi_options: Union[Sequence[TBD], None] = None, 

792 allow_unsafe_options: bool = False, 

793 allow_unsafe_protocols: bool = False, 

794 no_fetch: bool = False, 

795 ) -> "Submodule": 

796 """Update the repository of this submodule to point to the checkout we point at 

797 with the binsha of this instance. 

798 

799 :param recursive: 

800 If ``True``, we will operate recursively and update child modules as well. 

801 

802 :param init: 

803 If ``True``, the module repository will be cloned into place if necessary. 

804 

805 :param to_latest_revision: 

806 If ``True``, the submodule's sha will be ignored during checkout. Instead, 

807 the remote will be fetched, and the local tracking branch updated. This only 

808 works if we have a local tracking branch, which is the case if the remote 

809 repository had a master branch, or if the ``branch`` option was specified 

810 for this submodule and the branch existed remotely. 

811 

812 :param progress: 

813 :class:`UpdateProgress` instance, or ``None`` if no progress should be 

814 shown. 

815 

816 :param dry_run: 

817 If ``True``, the operation will only be simulated, but not performed. 

818 All performed operations are read-only. 

819 

820 :param force: 

821 If ``True``, we may reset heads even if the repository in question is dirty. 

822 Additionally we will be allowed to set a tracking branch which is ahead of 

823 its remote branch back into the past or the location of the remote branch. 

824 This will essentially 'forget' commits. 

825 

826 If ``False``, local tracking branches that are in the future of their 

827 respective remote branches will simply not be moved. 

828 

829 :param keep_going: 

830 If ``True``, we will ignore but log all errors, and keep going recursively. 

831 Unless `dry_run` is set as well, `keep_going` could cause 

832 subsequent/inherited errors you wouldn't see otherwise. 

833 In conjunction with `dry_run`, it can be useful to anticipate all errors 

834 when updating submodules. 

835 

836 :param env: 

837 Optional dictionary containing the desired environment variables. 

838 

839 Note: Provided variables will be used to update the execution environment 

840 for ``git``. If some variable is not specified in `env` and is defined in 

841 attr:`os.environ`, value from attr:`os.environ` will be used. 

842 

843 If you want to unset some variable, consider providing the empty string as 

844 its value. 

845 

846 :param clone_multi_options: 

847 List of :manpage:`git-clone(1)` options. 

848 Please see :meth:`Repo.clone <git.repo.base.Repo.clone>` for details. 

849 They only take effect with the `init` option. 

850 

851 :param allow_unsafe_protocols: 

852 Allow unsafe protocols to be used, like ``ext``. 

853 

854 :param allow_unsafe_options: 

855 Allow unsafe options to be used, like ``--upload-pack``. 

856 

857 :param no_fetch: 

858 If ``True``, update using locally available objects and remote-tracking 

859 refs without fetching or cloning. Repositories retained after 

860 :meth:`deinit` can be restored without fetching. 

861 

862 :note: 

863 Does nothing in bare repositories. 

864 

865 :note: 

866 This method is definitely not atomic if `recursive` is ``True``. 

867 

868 :return: 

869 self 

870 """ 

871 if self.repo.bare: 

872 return self 

873 # END pass in bare mode 

874 

875 if progress is None: 

876 progress = UpdateProgress() 

877 # END handle progress 

878 prefix = "" 

879 if dry_run: 

880 prefix = "DRY-RUN: " 

881 # END handle prefix 

882 

883 mrepo = None 

884 # END init mrepo 

885 

886 def fetch_remotes(module_repo: "Repo") -> None: 

887 rmts = module_repo.remotes 

888 len_rmts = len(rmts) 

889 for i, remote in enumerate(rmts): 

890 op = FETCH 

891 if i == 0: 

892 op |= BEGIN 

893 # END handle start 

894 

895 progress.update( 

896 op, 

897 i, 

898 len_rmts, 

899 prefix + "Fetching remote %s of submodule %r" % (remote, self.name), 

900 ) 

901 # =============================== 

902 if not dry_run: 

903 remote.fetch(progress=progress) 

904 # END handle dry-run 

905 # =============================== 

906 if i == len_rmts - 1: 

907 op |= END 

908 # END handle end 

909 progress.update( 

910 op, 

911 i, 

912 len_rmts, 

913 prefix + "Done fetching remote of submodule %r" % self.name, 

914 ) 

915 # END fetch new data 

916 

917 try: 

918 self._validated_name(self.name) 

919 

920 # ENSURE REPO IS PRESENT AND UP-TO-DATE 

921 ####################################### 

922 try: 

923 mrepo = self.module() 

924 if not no_fetch: 

925 fetch_remotes(mrepo) 

926 except InvalidGitRepositoryError: 

927 mrepo = None 

928 if not init: 

929 return self 

930 # END early abort if init is not allowed 

931 

932 checkout_module_abspath = self.abspath 

933 module_abspath = self._module_abspath(self.repo, self.path, self.name) 

934 

935 # ``git submodule deinit`` leaves the repository in 

936 # ``.git/modules`` and empties the checkout. Reconnect that retained 

937 # repository instead of trying to clone over it. 

938 if not dry_run and osp.isdir(module_abspath): 

939 try: 

940 git.Repo(module_abspath) 

941 except InvalidGitRepositoryError: 

942 pass 

943 else: 

944 if osp.lexists(checkout_module_abspath) and ( 

945 osp.islink(checkout_module_abspath) 

946 or not osp.isdir(checkout_module_abspath) 

947 or os.listdir(checkout_module_abspath) 

948 ): 

949 raise OSError( 

950 "Module directory at %r does already exist and is non-empty" % checkout_module_abspath 

951 ) 

952 os.makedirs(checkout_module_abspath, exist_ok=True) 

953 self._write_git_file_and_module_config(checkout_module_abspath, module_abspath) 

954 mrepo = git.Repo(checkout_module_abspath) 

955 mrepo.head.reset(mrepo.head.commit, index=True, working_tree=True) 

956 if not no_fetch: 

957 fetch_remotes(mrepo) 

958 with self.repo.config_writer() as writer: 

959 writer.set_value(sm_section(self.name), "url", self.url) 

960 

961 if mrepo is None: 

962 # There is no git-repository yet - but delete empty paths. 

963 if not dry_run and osp.isdir(checkout_module_abspath): 

964 try: 

965 os.rmdir(checkout_module_abspath) 

966 except OSError as e: 

967 raise OSError( 

968 "Module directory at %r does already exist and is non-empty" % checkout_module_abspath 

969 ) from e 

970 # END handle directory removal 

971 

972 # Don't check it out at first - nonetheless it will create a local 

973 # branch according to the remote-HEAD if possible. 

974 progress.update( 

975 BEGIN | CLONE, 

976 0, 

977 1, 

978 prefix 

979 + "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name), 

980 ) 

981 if not dry_run: 

982 if no_fetch: 

983 raise ValueError("Missing module at %r but fetching is disabled" % self.path) from None 

984 mrepo = self._clone_repo( 

985 self.repo, 

986 self.url, 

987 self.path, 

988 self.name, 

989 n=True, 

990 env=env, 

991 multi_options=clone_multi_options, 

992 allow_unsafe_options=allow_unsafe_options, 

993 allow_unsafe_protocols=allow_unsafe_protocols, 

994 ) 

995 progress.update(END | CLONE, 0, 1, prefix + "Done cloning to %s" % checkout_module_abspath) 

996 

997 if not dry_run: 

998 # See whether we have a valid branch to check out. 

999 try: 

1000 mrepo = cast("Repo", mrepo) 

1001 remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name) 

1002 local_branch = mkhead(mrepo, self.branch_path) 

1003 local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA)) 

1004 mrepo.head.set_reference( 

1005 local_branch, 

1006 logmsg="submodule: attaching head to %s" % local_branch, 

1007 ) 

1008 mrepo.head.reference.set_tracking_branch(remote_branch) 

1009 except (IndexError, InvalidGitRepositoryError): 

1010 _logger.warning("Failed to checkout tracking branch %s", self.branch_path) 

1011 

1012 with self.repo.config_writer() as writer: 

1013 writer.set_value(sm_section(self.name), "url", self.url) 

1014 # END handle initialization 

1015 

1016 # DETERMINE SHAS TO CHECK OUT 

1017 ############################# 

1018 binsha = self.binsha 

1019 hexsha = self.hexsha 

1020 is_detached = False 

1021 if mrepo is not None: 

1022 # mrepo is only set if we are not in dry-run mode or if the module 

1023 # existed. 

1024 is_detached = mrepo.head.is_detached 

1025 # END handle dry_run 

1026 

1027 if mrepo is not None and to_latest_revision: 

1028 msg_base = "Cannot update to latest revision in repository at %r as " % mrepo.working_dir 

1029 if not is_detached: 

1030 rref = mrepo.head.reference.tracking_branch() 

1031 if rref is not None: 

1032 rcommit = rref.commit 

1033 binsha = rcommit.binsha 

1034 hexsha = rcommit.hexsha 

1035 else: 

1036 _logger.error( 

1037 "%s a tracking branch was not set for local branch '%s'", 

1038 msg_base, 

1039 mrepo.head.reference, 

1040 ) 

1041 # END handle remote ref 

1042 else: 

1043 _logger.error("%s there was no local tracking branch", msg_base) 

1044 # END handle detached head 

1045 # END handle to_latest_revision option 

1046 

1047 # Update the working tree. 

1048 # Handles dry_run. 

1049 if mrepo is not None and mrepo.head.commit.binsha != binsha: 

1050 # We must ensure that our destination sha (the one to point to) is in 

1051 # the future of our current head. Otherwise, we will reset changes that 

1052 # might have been done on the submodule, but were not yet pushed. We 

1053 # also handle the case that history has been rewritten, leaving no 

1054 # merge-base. In that case we behave conservatively, protecting possible 

1055 # changes the user had done. 

1056 may_reset = True 

1057 if mrepo.head.commit.binsha != self.NULL_BIN_SHA: 

1058 base_commit = mrepo.merge_base(mrepo.head.commit, hexsha) 

1059 if len(base_commit) == 0 or (base_commit[0] is not None and base_commit[0].hexsha == hexsha): 

1060 if force: 

1061 msg = "Will force checkout or reset on local branch that is possibly in the future of" 

1062 msg += " the commit it will be checked out to, effectively 'forgetting' new commits" 

1063 _logger.debug(msg) 

1064 else: 

1065 msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits" 

1066 msg %= ( 

1067 is_detached and "checkout" or "reset", 

1068 mrepo.head, 

1069 mrepo, 

1070 ) 

1071 _logger.info(msg) 

1072 may_reset = False 

1073 # END handle force 

1074 # END handle if we are in the future 

1075 

1076 if may_reset and not force and mrepo.is_dirty(index=True, working_tree=True, untracked_files=True): 

1077 raise RepositoryDirtyError(mrepo, "Cannot reset a dirty repository") 

1078 # END handle force and dirty state 

1079 # END handle empty repo 

1080 

1081 # END verify future/past 

1082 progress.update( 

1083 BEGIN | UPDWKTREE, 

1084 0, 

1085 1, 

1086 prefix 

1087 + "Updating working tree at %s for submodule %r to revision %s" % (self.path, self.name, hexsha), 

1088 ) 

1089 

1090 if not dry_run and may_reset: 

1091 if is_detached: 

1092 # NOTE: For now we force. The user is not supposed to change 

1093 # detached submodules anyway. Maybe at some point this becomes 

1094 # an option, to properly handle user modifications - see below 

1095 # for future options regarding rebase and merge. 

1096 mrepo.git.checkout(hexsha, force=force) 

1097 else: 

1098 mrepo.head.reset(hexsha, index=True, working_tree=True) 

1099 # END handle checkout 

1100 # If we may reset/checkout. 

1101 progress.update( 

1102 END | UPDWKTREE, 

1103 0, 

1104 1, 

1105 prefix + "Done updating working tree for submodule %r" % self.name, 

1106 ) 

1107 # END update to new commit only if needed 

1108 except Exception as err: 

1109 if not keep_going: 

1110 raise 

1111 _logger.error(str(err)) 

1112 # END handle keep_going 

1113 

1114 # HANDLE RECURSION 

1115 ################## 

1116 if recursive: 

1117 # In dry_run mode, the module might not exist. 

1118 if mrepo is not None: 

1119 for submodule in self.iter_items(self.module()): 

1120 submodule.update( 

1121 recursive, 

1122 init, 

1123 to_latest_revision, 

1124 progress=progress, 

1125 dry_run=dry_run, 

1126 force=force, 

1127 keep_going=keep_going, 

1128 no_fetch=no_fetch, 

1129 ) 

1130 # END handle recursive update 

1131 # END handle dry run 

1132 # END for each submodule 

1133 

1134 return self 

1135 

1136 @unbare_repo 

1137 def move(self, module_path: PathLike, configuration: bool = True, module: bool = True) -> "Submodule": 

1138 """Move the submodule to a another module path. This involves physically moving 

1139 the repository at our current path, changing the configuration, as well as 

1140 adjusting our index entry accordingly. 

1141 

1142 :param module_path: 

1143 The path to which to move our module in the parent repository's working 

1144 tree, given as repository-relative or absolute path. Intermediate 

1145 directories will be created accordingly. If the path already exists, it must 

1146 be empty. Trailing (back)slashes are removed automatically. 

1147 

1148 :param configuration: 

1149 If ``True``, the configuration will be adjusted to let the submodule point 

1150 to the given path. 

1151 

1152 :param module: 

1153 If ``True``, the repository managed by this submodule will be moved as well. 

1154 If ``False``, we don't move the submodule's checkout, which may leave the 

1155 parent repository in an inconsistent state. 

1156 

1157 :return: 

1158 self 

1159 

1160 :raise ValueError: 

1161 If the module path existed and was not empty, was a file, or had a 

1162 symbolic link in an intermediate component. 

1163 

1164 :note: 

1165 Currently the method is not atomic, and it could leave the repository in an 

1166 inconsistent state if a sub-step fails for some reason. 

1167 """ 

1168 if module + configuration < 1: 

1169 raise ValueError("You must specify to move at least the module or the configuration of the submodule") 

1170 # END handle input 

1171 

1172 self._validated_name(self.name) 

1173 module_checkout_path = self._to_relative_path(self.repo, module_path) 

1174 

1175 # VERIFY DESTINATION 

1176 if module_checkout_path == self.path: 

1177 return self 

1178 # END handle no change 

1179 

1180 if configuration: 

1181 self._checked_abspath(self.repo.working_tree_dir, self.k_modules_file) 

1182 # Validate the source before removing the destination. 

1183 cur_path = self.abspath 

1184 module_abspath = self._module_abspath(self.repo, self.path, self.name) 

1185 if self.path == self.name: 

1186 self._module_abspath( 

1187 self.repo, module_checkout_path, os.fspath(module_checkout_path), moving_from=module_abspath 

1188 ) 

1189 module_checkout_abspath = self._checkout_abspath(module_checkout_path, allow_final_symlink=True) 

1190 if osp.isfile(module_checkout_abspath): 

1191 raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath) 

1192 # END handle target files 

1193 

1194 index = self.repo.index 

1195 tekey = index.entry_key(module_checkout_path, 0) 

1196 # if the target item already exists, fail 

1197 if configuration and tekey in index.entries: 

1198 raise ValueError("Index entry for target path did already exist") 

1199 # END handle index key already there 

1200 

1201 # Remove existing destination. 

1202 if module: 

1203 if osp.exists(module_checkout_abspath): 

1204 if len(os.listdir(module_checkout_abspath)): 

1205 raise ValueError("Destination module directory was not empty") 

1206 # END handle non-emptiness 

1207 

1208 if osp.islink(module_checkout_abspath): 

1209 os.remove(module_checkout_abspath) 

1210 else: 

1211 os.rmdir(module_checkout_abspath) 

1212 # END handle link 

1213 else: 

1214 # Recreate parent directories. 

1215 # NOTE: renames() does that now. 

1216 pass 

1217 # END handle existence 

1218 # END handle module 

1219 

1220 # Move the module into place if possible. 

1221 renamed_module = False 

1222 if module and osp.exists(cur_path): 

1223 self._renames(cur_path, module_checkout_abspath) 

1224 renamed_module = True 

1225 

1226 if osp.isfile(osp.join(module_checkout_abspath, ".git")): 

1227 self._write_git_file_and_module_config(module_checkout_abspath, module_abspath) 

1228 # END handle git file rewrite 

1229 # END move physical module 

1230 

1231 # Rename the index entry - we have to manipulate the index directly as git-mv 

1232 # cannot be used on submodules... yeah. 

1233 previous_sm_path = self.path 

1234 try: 

1235 if configuration: 

1236 try: 

1237 ekey = index.entry_key(self.path, 0) 

1238 entry = index.entries[ekey] 

1239 del index.entries[ekey] 

1240 nentry = git.IndexEntry(entry[:3] + (module_checkout_path,) + entry[4:]) 

1241 index.entries[tekey] = nentry 

1242 except KeyError as e: 

1243 raise InvalidGitRepositoryError("Submodule's entry at %r did not exist" % (self.path)) from e 

1244 # END handle submodule doesn't exist 

1245 

1246 # Update configuration. 

1247 with self.config_writer(index=index) as writer: # Auto-write. 

1248 writer.set_value("path", module_checkout_path) 

1249 self.path = module_checkout_path 

1250 # END handle configuration flag 

1251 except Exception: 

1252 if renamed_module: 

1253 self._renames(module_checkout_abspath, cur_path) 

1254 # END undo module renaming 

1255 raise 

1256 # END handle undo rename 

1257 

1258 # Auto-rename submodule if its name was 'default', that is, the checkout 

1259 # directory. 

1260 if previous_sm_path == self.name: 

1261 self.rename(module_checkout_path) 

1262 

1263 return self 

1264 

1265 @unbare_repo 

1266 def remove( 

1267 self, 

1268 module: bool = True, 

1269 force: bool = False, 

1270 configuration: bool = True, 

1271 dry_run: bool = False, 

1272 ) -> "Submodule": 

1273 """Remove this submodule from the repository. This will remove our entry 

1274 from the ``.gitmodules`` file and the entry in the ``.git/config`` file. 

1275 

1276 :param module: 

1277 If ``True``, the checked out module we point to will be deleted as well. If 

1278 that module is currently on a commit outside any branch in the remote, or if 

1279 it is ahead of its tracking branch, or if there are modified or untracked 

1280 files in its working tree, then the removal will fail. In case the removal 

1281 of the repository fails for these reasons, the submodule status will not 

1282 have been altered. 

1283 

1284 If this submodule has child modules of its own, these will be deleted prior 

1285 to touching the direct submodule. 

1286 

1287 :param force: 

1288 Enforces the deletion of the module even though it contains modifications. 

1289 This basically enforces a brute-force file system based deletion. 

1290 

1291 :param configuration: 

1292 If ``True``, the submodule is deleted from the configuration, otherwise it 

1293 isn't. Although this should be enabled most of the time, this flag enables 

1294 you to safely delete the repository of your submodule. 

1295 

1296 :param dry_run: 

1297 If ``True``, we will not actually do anything, but throw the errors we would 

1298 usually throw. 

1299 

1300 :return: 

1301 self 

1302 

1303 :note: 

1304 Doesn't work in bare repositories. 

1305 

1306 :note: 

1307 Doesn't work atomically, as failure to remove any part of the submodule will 

1308 leave an inconsistent state. 

1309 

1310 :note: 

1311 Metadata-directory aliases under ``.git/modules`` are retained. A link 

1312 directly to the deleted repository becomes dangling; adding or initializing 

1313 the submodule again recreates its target. Linked parent directories remain 

1314 available to sibling submodules. 

1315 

1316 :raise git.exc.InvalidGitRepositoryError: 

1317 Thrown if the repository cannot be deleted. 

1318 

1319 :raise OSError: 

1320 If directories or files could not be removed. 

1321 """ 

1322 if not (module or configuration): 

1323 raise ValueError("Need to specify to delete at least the module, or the configuration") 

1324 # END handle parameters 

1325 

1326 self._validated_name(self.name) 

1327 if configuration: 

1328 self._checked_abspath(self.repo.working_tree_dir, self.k_modules_file) 

1329 # Recursively remove children of this submodule. 

1330 nc = 0 

1331 for csm in self.children(): 

1332 nc += 1 

1333 csm.remove(module, force, configuration, dry_run) 

1334 del csm 

1335 

1336 if configuration and not dry_run and nc > 0: 

1337 # Ensure we don't leave the parent repository in a dirty state, and commit 

1338 # our changes. It's important for recursive, unforced, deletions to work as 

1339 # expected. 

1340 self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name) 

1341 # END handle recursion 

1342 

1343 # DELETE REPOSITORY WORKING TREE 

1344 ################################ 

1345 if module and self.module_exists(): 

1346 mod = self.module() 

1347 git_dir = str(Path(mod.git_dir).resolve()) 

1348 if force: 

1349 # Take the fast lane and just delete everything in our module path. 

1350 # TODO: If we run into permission problems, we have a highly 

1351 # inconsistent state. Delete the .git folders last, start with the 

1352 # submodules first. 

1353 mp = self.abspath 

1354 method: Union[None, Callable[[PathLike], None]] = None 

1355 if osp.islink(mp): 

1356 method = os.remove 

1357 elif osp.isdir(mp): 

1358 method = rmtree 

1359 elif osp.exists(mp): 

1360 raise AssertionError("Cannot forcibly delete repository as it was neither a link, nor a directory") 

1361 # END handle brutal deletion 

1362 if not dry_run: 

1363 assert method 

1364 method(mp) 

1365 # END apply deletion method 

1366 else: 

1367 # Verify we may delete our module. 

1368 if mod.is_dirty(index=True, working_tree=True, untracked_files=True): 

1369 raise InvalidGitRepositoryError( 

1370 "Cannot delete module at %s with any modifications, unless force is specified" 

1371 % mod.working_tree_dir 

1372 ) 

1373 # END check for dirt 

1374 

1375 # Figure out whether we have new commits compared to the remotes. 

1376 # NOTE: If the user pulled all the time, the remote heads might not have 

1377 # been updated, so commits coming from the remote look as if they come 

1378 # from us. But we stay strictly read-only and don't fetch beforehand. 

1379 for remote in mod.remotes: 

1380 num_branches_with_new_commits = 0 

1381 rrefs = remote.refs 

1382 rref = None 

1383 for rref in rrefs: 

1384 num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0 

1385 # END for each remote ref 

1386 # Not a single remote branch contained all our commits. 

1387 if len(rrefs) and num_branches_with_new_commits == len(rrefs): 

1388 raise InvalidGitRepositoryError( 

1389 "Cannot delete module at %s as there are new commits" % mod.working_tree_dir 

1390 ) 

1391 # END handle new commits 

1392 # We have to manually delete some references to allow resources to 

1393 # be cleaned up immediately when we are done with them, because 

1394 # Python's scoping is no more granular than the whole function (loop 

1395 # bodies are not scopes). When the objects stay alive longer, they 

1396 # can keep handles open. On Windows, this is a problem. 

1397 if len(rrefs): 

1398 del rref # skipcq: PYL-W0631 

1399 # END handle remotes 

1400 del rrefs 

1401 del remote 

1402 # END for each remote 

1403 

1404 # Finally delete our own submodule. 

1405 if not dry_run: 

1406 self._clear_cache() 

1407 wtd = mod.working_tree_dir 

1408 del mod # Release file-handles (Windows). 

1409 gc.collect() 

1410 rmtree(str(wtd)) 

1411 # END delete tree if possible 

1412 # END handle force 

1413 

1414 if not dry_run and osp.isdir(git_dir): 

1415 self._clear_cache() 

1416 rmtree(git_dir) 

1417 # END handle separate bare repository 

1418 # END handle module deletion 

1419 

1420 # Void our data so as not to delay invalid access. 

1421 if not dry_run: 

1422 self._clear_cache() 

1423 

1424 # DELETE CONFIGURATION 

1425 ###################### 

1426 if configuration and not dry_run: 

1427 # First the index-entry. 

1428 parent_index = self.repo.index 

1429 try: 

1430 del parent_index.entries[parent_index.entry_key(self.path, 0)] 

1431 except KeyError: 

1432 pass 

1433 # END delete entry 

1434 parent_index.write() 

1435 

1436 # Now git config - we need the config intact, otherwise we can't query 

1437 # information anymore. 

1438 

1439 with self.repo.config_writer() as gcp_writer: 

1440 gcp_writer.remove_section(sm_section(self.name)) 

1441 

1442 with self.config_writer() as sc_writer: 

1443 sc_writer.remove_section() 

1444 # END delete configuration 

1445 

1446 return self 

1447 

1448 @unbare_repo 

1449 def deinit(self, force: bool = False) -> "Submodule": 

1450 """Run ``git submodule deinit`` on this submodule. 

1451 

1452 This is a thin wrapper around ``git submodule deinit <path>``, 

1453 which unregisters the submodule (removes its entry from 

1454 ``.git/config`` and empties the working-tree directory) 

1455 without deleting the submodule from ``.gitmodules`` 

1456 or its checked-out repository under ``.git/modules/``. 

1457 A subsequent :meth:`update` will re-initialize the 

1458 submodule from the retained contents. 

1459 

1460 :param force: 

1461 If ``True``, pass ``--force`` to ``git submodule deinit``. This 

1462 allows deinitialization even when the submodule's working tree has 

1463 local modifications that would otherwise block the command. 

1464 

1465 :return: 

1466 self 

1467 

1468 :note: 

1469 Doesn't work in bare repositories. 

1470 """ 

1471 args: List[str] = [] 

1472 if force: 

1473 args.append("--force") 

1474 args.extend(["--", str(self.path)]) 

1475 self.repo.git.submodule("deinit", *args) 

1476 return self 

1477 

1478 def set_parent_commit(self, commit: Union[Commit_ish, str, None], check: bool = True) -> "Submodule": 

1479 """Set this instance to use the given commit whose tree is supposed to 

1480 contain the ``.gitmodules`` blob. 

1481 

1482 :param commit: 

1483 Commit-ish reference pointing at the root tree, or ``None`` to always point 

1484 to the most recent commit. 

1485 

1486 :param check: 

1487 If ``True``, relatively expensive checks will be performed to verify 

1488 validity of the submodule. 

1489 

1490 :raise ValueError: 

1491 If the commit's tree didn't contain the ``.gitmodules`` blob. 

1492 

1493 :raise ValueError: 

1494 If the parent commit didn't store this submodule under the current path. 

1495 

1496 :return: 

1497 self 

1498 """ 

1499 if commit is None: 

1500 self._parent_commit = None 

1501 return self 

1502 # END handle None 

1503 pcommit = self.repo.commit(commit) 

1504 pctree = pcommit.tree 

1505 if self.k_modules_file not in pctree: 

1506 raise ValueError("Tree of commit %s did not contain the %s file" % (commit, self.k_modules_file)) 

1507 # END handle exceptions 

1508 

1509 prev_pc = self._parent_commit 

1510 self._parent_commit = pcommit 

1511 

1512 if check: 

1513 parser = self._config_parser(self.repo, self._parent_commit, read_only=True) 

1514 if not parser.has_section(sm_section(self.name)): 

1515 self._parent_commit = prev_pc 

1516 raise ValueError("Submodule at path %r did not exist in parent commit %s" % (self.path, commit)) 

1517 # END handle submodule did not exist 

1518 # END handle checking mode 

1519 

1520 # Update our sha, it could have changed. 

1521 # If check is False, we might see a parent-commit that doesn't even contain the 

1522 # submodule anymore. in that case, mark our sha as being NULL. 

1523 try: 

1524 self.binsha = pctree[str(self.path)].binsha 

1525 except KeyError: 

1526 self.binsha = self.NULL_BIN_SHA 

1527 

1528 self._clear_cache() 

1529 return self 

1530 

1531 @unbare_repo 

1532 def config_writer( 

1533 self, index: Union["IndexFile", None] = None, write: bool = True 

1534 ) -> SectionConstraint["SubmoduleConfigParser"]: 

1535 """ 

1536 :return: 

1537 A config writer instance allowing you to read and write the data belonging 

1538 to this submodule into the ``.gitmodules`` file. 

1539 

1540 :param index: 

1541 If not ``None``, an :class:`~git.index.base.IndexFile` instance which should 

1542 be written. Defaults to the index of the :class:`Submodule`'s parent 

1543 repository. 

1544 

1545 :param write: 

1546 If ``True``, the index will be written each time a configuration value changes. 

1547 

1548 :note: 

1549 The parameters allow for a more efficient writing of the index, as you can 

1550 pass in a modified index on your own, prevent automatic writing, and write 

1551 yourself once the whole operation is complete. 

1552 

1553 :raise ValueError: 

1554 If trying to get a writer on a parent_commit which does not match the 

1555 current head commit. 

1556 

1557 :raise IOError: 

1558 If the ``.gitmodules`` file/blob could not be read. 

1559 """ 

1560 writer = self._config_parser_constrained(read_only=False) 

1561 if index is not None: 

1562 writer.config._index = index 

1563 writer.config._auto_write = write 

1564 return writer 

1565 

1566 @unbare_repo 

1567 def rename(self, new_name: str) -> "Submodule": 

1568 """Rename this submodule. 

1569 

1570 :note: 

1571 This method takes care of renaming the submodule in various places, such as: 

1572 

1573 * ``$parent_git_dir / config`` 

1574 * ``$working_tree_dir / .gitmodules`` 

1575 * (git >= v1.8.0: move submodule repository to new name) 

1576 

1577 As ``.gitmodules`` will be changed, you would need to make a commit afterwards. 

1578 The changed ``.gitmodules`` file will already be added to the index. 

1579 

1580 :return: 

1581 This :class:`Submodule` instance 

1582 """ 

1583 if self.name == new_name: 

1584 return self 

1585 

1586 self._validated_name(self.name) 

1587 self._validated_name(new_name) 

1588 mod = self.module() 

1589 destination_module_abspath = self._module_abspath(self.repo, self.path, new_name, moving_from=mod.git_dir) 

1590 self._checked_abspath(self.repo.working_tree_dir, self.k_modules_file) 

1591 

1592 # .git/config 

1593 with self.repo.config_writer() as pw: 

1594 # As we ourselves didn't write anything about submodules into the parent 

1595 # .git/config, we will not require it to exist, and just ignore missing 

1596 # entries. 

1597 if pw.has_section(sm_section(self.name)): 

1598 pw.rename_section(sm_section(self.name), sm_section(new_name)) 

1599 

1600 # .gitmodules 

1601 with self.config_writer(write=True).config as cw: 

1602 cw.rename_section(sm_section(self.name), sm_section(new_name)) 

1603 

1604 self._name = new_name 

1605 

1606 # .git/modules 

1607 if mod.has_separate_working_tree(): 

1608 source_dir = mod.git_dir 

1609 # Let's be sure the submodule name is not so obviously tied to a directory. 

1610 if str(destination_module_abspath).startswith(str(mod.git_dir)): 

1611 tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4())) 

1612 self._renames(source_dir, tmp_dir) 

1613 source_dir = tmp_dir 

1614 # END handle self-containment 

1615 self._renames(source_dir, destination_module_abspath) 

1616 if mod.working_tree_dir: 

1617 self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath) 

1618 # END move separate git repository 

1619 

1620 return self 

1621 

1622 # } END edit interface 

1623 

1624 # { Query Interface 

1625 

1626 @unbare_repo 

1627 def module(self) -> "Repo": 

1628 """ 

1629 :return: 

1630 :class:`~git.repo.base.Repo` instance initialized from the repository at our 

1631 submodule path 

1632 

1633 :raise git.exc.InvalidGitRepositoryError: 

1634 If a repository was not available. 

1635 This could also mean that it was not yet initialized. 

1636 """ 

1637 self._validated_name(self.name) 

1638 module_checkout_abspath = self.abspath 

1639 self._module_abspath(self.repo, self.path, self.name) 

1640 try: 

1641 repo = git.Repo(module_checkout_abspath) 

1642 if repo != self.repo: 

1643 return repo 

1644 # END handle repo uninitialized 

1645 except (InvalidGitRepositoryError, NoSuchPathError) as e: 

1646 raise InvalidGitRepositoryError("No valid repository at %s" % module_checkout_abspath) from e 

1647 else: 

1648 raise InvalidGitRepositoryError("Repository at %r was not yet checked out" % module_checkout_abspath) 

1649 # END handle exceptions 

1650 

1651 def module_exists(self) -> bool: 

1652 """ 

1653 :return: 

1654 ``True`` if our module exists and is a valid git repository. 

1655 See the :meth:`module` method. 

1656 """ 

1657 try: 

1658 self.module() 

1659 return True 

1660 except Exception: 

1661 return False 

1662 # END handle exception 

1663 

1664 def exists(self) -> bool: 

1665 """ 

1666 :return: 

1667 ``True`` if the submodule exists, ``False`` otherwise. 

1668 Please note that a submodule may exist (in the ``.gitmodules`` file) even 

1669 though its module doesn't exist on disk. 

1670 """ 

1671 # Keep attributes for later, and restore them if we have no valid data. 

1672 # This way we do not actually alter the state of the object. 

1673 loc = locals() 

1674 for attr in self._cache_attrs: 

1675 try: 

1676 if hasattr(self, attr): 

1677 loc[attr] = getattr(self, attr) 

1678 # END if we have the attribute cache 

1679 except (cp.NoSectionError, ValueError): 

1680 # On PY3, this can happen apparently... don't know why this doesn't 

1681 # happen on PY2. 

1682 pass 

1683 # END for each attr 

1684 self._clear_cache() 

1685 

1686 try: 

1687 try: 

1688 self.path # noqa: B018 

1689 return True 

1690 except Exception: 

1691 return False 

1692 # END handle exceptions 

1693 finally: 

1694 for attr in self._cache_attrs: 

1695 if attr in loc: 

1696 setattr(self, attr, loc[attr]) 

1697 # END if we have a cache 

1698 # END reapply each attribute 

1699 # END handle object state consistency 

1700 

1701 @property 

1702 def branch(self) -> "Head": 

1703 """ 

1704 :return: 

1705 The branch instance that we are to checkout 

1706 

1707 :raise git.exc.InvalidGitRepositoryError: 

1708 If our module is not yet checked out. 

1709 """ 

1710 return mkhead(self.module(), self._branch_path) 

1711 

1712 @property 

1713 def branch_path(self) -> PathLike: 

1714 """ 

1715 :return: 

1716 Full repository-relative path as string to the branch we would checkout from 

1717 the remote and track 

1718 """ 

1719 return self._branch_path 

1720 

1721 @property 

1722 def branch_name(self) -> str: 

1723 """ 

1724 :return: 

1725 The name of the branch, which is the shortest possible branch name 

1726 """ 

1727 # Use an instance method, for this we create a temporary Head instance which 

1728 # uses a repository that is available at least (it makes no difference). 

1729 return git.Head(self.repo, self._branch_path).name 

1730 

1731 @property 

1732 def url(self) -> str: 

1733 """:return: The url to the repository our submodule's repository refers to""" 

1734 return self._url 

1735 

1736 @staticmethod 

1737 def _resolve_url(repo: "Repo", url: str) -> str: 

1738 """Resolve relative URLs against the tracking remote, then ``origin``, 

1739 falling back to the superproject's directory the way git does.""" 

1740 if not is_relative_url(url): 

1741 return url 

1742 remote_name = "origin" 

1743 if not repo.head.is_detached: 

1744 with repo.active_branch.config_reader() as reader: 

1745 remote_name = reader.get("remote", fallback="origin") 

1746 try: 

1747 remote_url = repo.remote(remote_name).url 

1748 except ValueError: 

1749 remote_url = os.fspath(repo.working_dir) 

1750 if remote_url == ".": 

1751 remote_url = os.fspath(repo.working_dir) 

1752 resolved = resolve_relative_url(remote_url, url) 

1753 if resolved and _url_is_local_not_ssh(resolved) and not _is_absolute_path(resolved): 

1754 resolved = os.path.join(repo.working_dir, resolved) 

1755 return resolved 

1756 

1757 def _resolved_url(self) -> str: 

1758 """:return: :attr:`url` resolved against the superproject's default remote""" 

1759 return self._resolve_url(self.repo, self.url) 

1760 

1761 @property 

1762 def parent_commit(self) -> "Commit": 

1763 """ 

1764 :return: 

1765 :class:`~git.objects.commit.Commit` instance with the tree containing the 

1766 ``.gitmodules`` file 

1767 

1768 :note: 

1769 Will always point to the current head's commit if it was not set explicitly. 

1770 """ 

1771 if self._parent_commit is None: 

1772 return self.repo.commit() 

1773 return self._parent_commit 

1774 

1775 @property 

1776 def name(self) -> str: 

1777 """ 

1778 :return: 

1779 The name of this submodule. It is used to identify it within the 

1780 ``.gitmodules`` file. 

1781 

1782 :note: 

1783 By default, this is the name is the path at which to find the submodule, but 

1784 in GitPython it should be a unique identifier similar to the identifiers 

1785 used for remotes, which allows to change the path of the submodule easily. 

1786 """ 

1787 return self._name 

1788 

1789 def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]: 

1790 """ 

1791 :return: 

1792 ConfigReader instance which allows you to query the configuration values of 

1793 this submodule, as provided by the ``.gitmodules`` file. 

1794 

1795 :note: 

1796 The config reader will actually read the data directly from the repository 

1797 and thus does not need nor care about your working tree. 

1798 

1799 :note: 

1800 Should be cached by the caller and only kept as long as needed. 

1801 

1802 :raise IOError: 

1803 If the ``.gitmodules`` file/blob could not be read. 

1804 """ 

1805 return self._config_parser_constrained(read_only=True) 

1806 

1807 def children(self) -> IterableList["Submodule"]: 

1808 """ 

1809 :return: 

1810 IterableList(Submodule, ...) An iterable list of :class:`Submodule` 

1811 instances which are children of this submodule or 0 if the submodule is not 

1812 checked out. 

1813 """ 

1814 return self._get_intermediate_items(self) 

1815 

1816 # } END query interface 

1817 

1818 # { Iterable Interface 

1819 

1820 @classmethod 

1821 def iter_items( 

1822 cls, 

1823 repo: "Repo", 

1824 parent_commit: Union[Commit_ish, str] = "HEAD", 

1825 *args: Any, 

1826 **kwargs: Any, 

1827 ) -> Iterator["Submodule"]: 

1828 """ 

1829 :return: 

1830 Iterator yielding :class:`Submodule` instances available in the given 

1831 repository 

1832 """ 

1833 try: 

1834 pc = repo.commit(parent_commit) # Parent commit instance 

1835 parser = cls._config_parser(repo, pc, read_only=True) 

1836 except (OSError, BadName): 

1837 return 

1838 # END handle empty iterator 

1839 

1840 for sms in parser.sections(): 

1841 n = sm_name(sms) 

1842 p = parser.get(sms, "path") 

1843 u = parser.get(sms, "url") 

1844 b = cls.k_head_default 

1845 if parser.has_option(sms, cls.k_head_option): 

1846 b = str(parser.get(sms, cls.k_head_option)) 

1847 # END handle optional information 

1848 

1849 # Get the binsha. 

1850 index = repo.index 

1851 try: 

1852 rt = pc.tree # Root tree 

1853 sm = rt[p] 

1854 except KeyError: 

1855 # Try the index, maybe it was just added. 

1856 try: 

1857 entry = index.entries[index.entry_key(p, 0)] 

1858 sm = Submodule(repo, entry.binsha, entry.mode, entry.path) 

1859 except KeyError: 

1860 # The submodule doesn't exist, probably it wasn't removed from the 

1861 # .gitmodules file. 

1862 continue 

1863 # END handle keyerror 

1864 # END handle critical error 

1865 

1866 # Make sure we are looking at a submodule object. 

1867 if type(sm) is not Submodule: 

1868 continue 

1869 

1870 # Fill in remaining info - saves time as it doesn't have to be parsed again. 

1871 sm._name = n 

1872 if pc != repo.commit(): 

1873 sm._parent_commit = pc 

1874 # END set only if not most recent! 

1875 sm._branch_path = git.Head.to_full_path(b) 

1876 sm._url = u 

1877 

1878 yield sm 

1879 # END for each section 

1880 

1881 # } END iterable interface