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

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

587 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 

7from io import BytesIO 

8import logging 

9import os 

10import os.path as osp 

11import stat 

12import sys 

13import uuid 

14import urllib 

15 

16import git 

17from git.cmd import Git 

18from git.compat import defenc 

19from git.config import GitConfigParser, SectionConstraint, cp 

20from git.exc import ( 

21 BadName, 

22 InvalidGitRepositoryError, 

23 NoSuchPathError, 

24 RepositoryDirtyError, 

25) 

26from git.objects.base import IndexObject, Object 

27from git.objects.util import TraversableIterableObj 

28from git.util import ( 

29 IterableList, 

30 RemoteProgress, 

31 join_path_native, 

32 rmtree, 

33 to_native_path_linux, 

34 unbare_repo, 

35) 

36 

37from .util import ( 

38 SubmoduleConfigParser, 

39 find_first_remote_branch, 

40 mkhead, 

41 sm_name, 

42 sm_section, 

43) 

44 

45# typing ---------------------------------------------------------------------- 

46 

47from typing import ( 

48 Any, 

49 Callable, 

50 Dict, 

51 Iterator, 

52 Mapping, 

53 Sequence, 

54 TYPE_CHECKING, 

55 Union, 

56 cast, 

57) 

58 

59if sys.version_info >= (3, 8): 

60 from typing import Literal 

61else: 

62 from typing_extensions import Literal 

63 

64from git.types import Commit_ish, PathLike, TBD 

65 

66if TYPE_CHECKING: 

67 from git.index import IndexFile 

68 from git.objects.commit import Commit 

69 from git.refs import Head 

70 from git.repo import Repo 

71 

72# ----------------------------------------------------------------------------- 

73 

74_logger = logging.getLogger(__name__) 

75 

76 

77class UpdateProgress(RemoteProgress): 

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

79 derive from it and implement the 

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

81 

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

83 _num_op_codes: int = RemoteProgress._num_op_codes + 3 

84 

85 __slots__ = () 

86 

87 

88BEGIN = UpdateProgress.BEGIN 

89END = UpdateProgress.END 

90CLONE = UpdateProgress.CLONE 

91FETCH = UpdateProgress.FETCH 

92UPDWKTREE = UpdateProgress.UPDWKTREE 

93 

94 

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

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

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

98class Submodule(IndexObject, TraversableIterableObj): 

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

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

101 at the path of this instance. 

102 

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

104 solely as a marker in the tree and index. 

105 

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

107 """ 

108 

109 _id_attribute_ = "name" 

110 k_modules_file = ".gitmodules" 

111 k_head_option = "branch" 

112 k_head_default = "master" 

113 k_default_mode = stat.S_IFDIR | stat.S_IFLNK 

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

115 

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

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

118 

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

120 

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

122 

123 def __init__( 

124 self, 

125 repo: "Repo", 

126 binsha: bytes, 

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

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

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

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

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

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

133 ) -> None: 

134 """Initialize this instance with its attributes. 

135 

136 We only document the parameters that differ from 

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

138 

139 :param repo: 

140 Our parent repository. 

141 

142 :param binsha: 

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

144 See the `url` parameter. 

145 

146 :param parent_commit: 

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

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

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

150 

151 :param url: 

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

153 

154 :param branch_path: 

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

156 repository. 

157 """ 

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

159 self.size = 0 

160 self._parent_commit = parent_commit 

161 if url is not None: 

162 self._url = url 

163 if branch_path is not None: 

164 self._branch_path = branch_path 

165 if name is not None: 

166 self._name = name 

167 

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

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

170 reader: SectionConstraint = self.config_reader() 

171 # Default submodule values. 

172 try: 

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

174 except cp.NoSectionError as e: 

175 if self.repo.working_tree_dir is not None: 

176 raise ValueError( 

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

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

179 ) from e 

180 

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

182 # GitPython extension values - optional. 

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

184 elif attr == "_name": 

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

186 else: 

187 super()._set_cache_(attr) 

188 # END handle attribute name 

189 

190 @classmethod 

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

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

193 try: 

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

195 except InvalidGitRepositoryError: 

196 return IterableList("") 

197 # END handle intermediate items 

198 

199 @classmethod 

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

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

202 

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

204 """Compare with another submodule.""" 

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

206 # Otherwise this type wouldn't be hashable. 

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

208 return self._name == other._name 

209 

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

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

212 return not (self == other) 

213 

214 def __hash__(self) -> int: 

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

216 return hash(self._name) 

217 

218 def __str__(self) -> str: 

219 return self._name 

220 

221 def __repr__(self) -> str: 

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

223 type(self).__name__, 

224 self._name, 

225 self.path, 

226 self.url, 

227 self.branch_path, 

228 ) 

229 

230 @classmethod 

231 def _config_parser( 

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

233 ) -> SubmoduleConfigParser: 

234 """ 

235 :return: 

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

237 

238 :raise IOError: 

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

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

241 delayed until the first access of the config parser. 

242 """ 

243 parent_matches_head = True 

244 if parent_commit is not None: 

245 try: 

246 parent_matches_head = repo.head.commit == parent_commit 

247 except ValueError: 

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

249 # to a valid ref. 

250 pass 

251 # END handle parent_commit 

252 fp_module: Union[str, BytesIO] 

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

254 fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file) 

255 else: 

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

257 try: 

258 fp_module = cls._sio_modules(parent_commit) 

259 except KeyError as e: 

260 raise IOError( 

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

262 ) from e 

263 # END handle exceptions 

264 # END handle non-bare working tree 

265 

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

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

268 # END handle writes of historical submodules 

269 

270 return SubmoduleConfigParser(fp_module, read_only=read_only) 

271 

272 def _clear_cache(self) -> None: 

273 """Clear the possibly changed values.""" 

274 for name in self._cache_attrs: 

275 try: 

276 delattr(self, name) 

277 except AttributeError: 

278 pass 

279 # END try attr deletion 

280 # END for each name to delete 

281 

282 @classmethod 

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

284 """ 

285 :return: 

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

287 respective blob's data 

288 """ 

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

290 sio.name = cls.k_modules_file 

291 return sio 

292 

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

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

295 try: 

296 pc = self.parent_commit 

297 except ValueError: 

298 pc = None 

299 # END handle empty parent repository 

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

301 parser.set_submodule(self) 

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

303 

304 @classmethod 

305 def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike: 

306 if cls._need_gitfile_submodules(parent_repo.git): 

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

308 if parent_repo.working_tree_dir: 

309 return osp.join(parent_repo.working_tree_dir, path) 

310 raise NotADirectoryError() 

311 

312 @classmethod 

313 def _clone_repo( 

314 cls, 

315 repo: "Repo", 

316 url: str, 

317 path: PathLike, 

318 name: str, 

319 allow_unsafe_options: bool = False, 

320 allow_unsafe_protocols: bool = False, 

321 **kwargs: Any, 

322 ) -> "Repo": 

323 """ 

324 :return: 

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

326 

327 :param repo: 

328 Our parent repository. 

329 

330 :param url: 

331 URL to clone from. 

332 

333 :param path: 

334 Repository-relative path to the submodule checkout location. 

335 

336 :param name: 

337 Canonical name of the submodule. 

338 

339 :param allow_unsafe_protocols: 

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

341 

342 :param allow_unsafe_options: 

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

344 

345 :param kwargs: 

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

347 """ 

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

349 module_checkout_path = module_abspath 

350 if cls._need_gitfile_submodules(repo.git): 

351 kwargs["separate_git_dir"] = module_abspath 

352 module_abspath_dir = osp.dirname(module_abspath) 

353 if not osp.isdir(module_abspath_dir): 

354 os.makedirs(module_abspath_dir) 

355 module_checkout_path = osp.join(str(repo.working_tree_dir), path) 

356 

357 if url.startswith("../"): 

358 remote_name = repo.active_branch.tracking_branch().remote_name 

359 repo_remote_url = repo.remote(remote_name).url 

360 url = os.path.join(repo_remote_url, url) 

361 

362 clone = git.Repo.clone_from( 

363 url, 

364 module_checkout_path, 

365 allow_unsafe_options=allow_unsafe_options, 

366 allow_unsafe_protocols=allow_unsafe_protocols, 

367 **kwargs, 

368 ) 

369 if cls._need_gitfile_submodules(repo.git): 

370 cls._write_git_file_and_module_config(module_checkout_path, module_abspath) 

371 

372 return clone 

373 

374 @classmethod 

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

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

377 

378 :raise ValueError: 

379 If path is not contained in the parent repository's working tree. 

380 """ 

381 path = to_native_path_linux(path) 

382 if path.endswith("/"): 

383 path = path[:-1] 

384 # END handle trailing slash 

385 

386 if osp.isabs(path) and parent_repo.working_tree_dir: 

387 working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir) 

388 if not path.startswith(working_tree_linux): 

389 raise ValueError( 

390 "Submodule checkout path '%s' needs to be within the parents repository at '%s'" 

391 % (working_tree_linux, path) 

392 ) 

393 path = path[len(working_tree_linux.rstrip("/")) + 1 :] 

394 if not path: 

395 raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path) 

396 # END verify converted relative path makes sense 

397 # END convert to a relative path 

398 

399 return path 

400 

401 @classmethod 

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

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

404 git module repository. 

405 

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

407 relative to the `working_tree_dir`. 

408 

409 :note: 

410 This will overwrite existing files! 

411 

412 :note: 

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

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

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

416 one. 

417 

418 :param working_tree_dir: 

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

420 

421 :param module_abspath: 

422 Absolute path to the bare repository. 

423 """ 

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

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

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

427 os.remove(git_file) 

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

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

430 

431 with GitConfigParser(osp.join(module_abspath, "config"), read_only=False, merge_includes=False) as writer: 

432 writer.set_value( 

433 "core", 

434 "worktree", 

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

436 ) 

437 

438 # { Edit Interface 

439 

440 @classmethod 

441 def add( 

442 cls, 

443 repo: "Repo", 

444 name: str, 

445 path: PathLike, 

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

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

448 no_checkout: bool = False, 

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

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

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

452 allow_unsafe_options: bool = False, 

453 allow_unsafe_protocols: bool = False, 

454 ) -> "Submodule": 

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

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

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

458 provided, the existing submodule will be returned. 

459 

460 :param repo: 

461 Repository instance which should receive the submodule. 

462 

463 :param name: 

464 The name/identifier for the submodule. 

465 

466 :param path: 

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

468 located. 

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

470 

471 :param url: 

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

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

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

475 existing repository a submodule of another one. 

476 

477 :param branch: 

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

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

480 locally as a tracking branch. 

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

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

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

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

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

486 

487 :param no_checkout: 

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

489 will be performed. 

490 

491 :param depth: 

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

493 commits. 

494 

495 :param env: 

496 Optional dictionary containing the desired environment variables. 

497 

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

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

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

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

502 value. 

503 

504 :param clone_multi_options: 

505 A list of clone options. Please see 

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

507 

508 :param allow_unsafe_protocols: 

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

510 

511 :param allow_unsafe_options: 

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

513 

514 :return: 

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

516 

517 :note: 

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

519 repository update fails. 

520 """ 

521 if repo.bare: 

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

523 # END handle bare repos 

524 

525 path = cls._to_relative_path(repo, path) 

526 

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

528 if url is not None: 

529 url = to_native_path_linux(url) 

530 # END ensure URL correctness 

531 

532 # INSTANTIATE INTERMEDIATE SM 

533 sm = cls( 

534 repo, 

535 cls.NULL_BIN_SHA, 

536 cls.k_default_mode, 

537 path, 

538 name, 

539 url="invalid-temporary", 

540 ) 

541 if sm.exists(): 

542 # Reretrieve submodule from tree. 

543 try: 

544 sm = repo.head.commit.tree[str(path)] 

545 sm._name = name 

546 return sm 

547 except KeyError: 

548 # Could only be in index. 

549 index = repo.index 

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

551 sm.binsha = entry.binsha 

552 return sm 

553 # END handle exceptions 

554 # END handle existing 

555 

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

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

558 has_module = sm.module_exists() 

559 branch_is_default = branch is None 

560 if has_module and url is not None: 

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

562 raise ValueError( 

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

564 ) 

565 # END check url 

566 # END verify urls match 

567 

568 mrepo: Union[Repo, None] = None 

569 

570 if url is None: 

571 if not has_module: 

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

573 # END check url 

574 mrepo = sm.module() 

575 # assert isinstance(mrepo, git.Repo) 

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

577 if not urls: 

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

579 # END verify we have url 

580 url = urls[0] 

581 else: 

582 # Clone new repo. 

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

584 if not branch_is_default: 

585 kwargs["b"] = br.name 

586 # END setup checkout-branch 

587 

588 if depth: 

589 if isinstance(depth, int): 

590 kwargs["depth"] = depth 

591 else: 

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

593 if clone_multi_options: 

594 kwargs["multi_options"] = clone_multi_options 

595 

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

597 mrepo = cls._clone_repo( 

598 repo, 

599 url, 

600 path, 

601 name, 

602 env=env, 

603 allow_unsafe_options=allow_unsafe_options, 

604 allow_unsafe_protocols=allow_unsafe_protocols, 

605 **kwargs, 

606 ) 

607 # END verify url 

608 

609 ## See #525 for ensuring git URLs in config-files are valid under Windows. 

610 url = Git.polish_url(url) 

611 

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

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

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

615 # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one 

616 writer: Union[GitConfigParser, SectionConstraint] 

617 

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

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

620 

621 # Update configuration and index. 

622 index = sm.repo.index 

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

624 writer.set_value("url", url) 

625 writer.set_value("path", path) 

626 

627 sm._url = url 

628 if not branch_is_default: 

629 # Store full path. 

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

631 sm._branch_path = br.path 

632 

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

634 if mrepo: 

635 sm.binsha = mrepo.head.commit.binsha 

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

637 

638 return sm 

639 

640 def update( 

641 self, 

642 recursive: bool = False, 

643 init: bool = True, 

644 to_latest_revision: bool = False, 

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

646 dry_run: bool = False, 

647 force: bool = False, 

648 keep_going: bool = False, 

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

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

651 allow_unsafe_options: bool = False, 

652 allow_unsafe_protocols: bool = False, 

653 ) -> "Submodule": 

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

655 with the binsha of this instance. 

656 

657 :param recursive: 

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

659 

660 :param init: 

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

662 

663 :param to_latest_revision: 

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

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

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

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

668 for this submodule and the branch existed remotely. 

669 

670 :param progress: 

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

672 shown. 

673 

674 :param dry_run: 

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

676 All performed operations are read-only. 

677 

678 :param force: 

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

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

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

682 This will essentially 'forget' commits. 

683 

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

685 respective remote branches will simply not be moved. 

686 

687 :param keep_going: 

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

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

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

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

692 when updating submodules. 

693 

694 :param env: 

695 Optional dictionary containing the desired environment variables. 

696 

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

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

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

700 

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

702 its value. 

703 

704 :param clone_multi_options: 

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

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

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

708 

709 :param allow_unsafe_protocols: 

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

711 

712 :param allow_unsafe_options: 

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

714 

715 :note: 

716 Does nothing in bare repositories. 

717 

718 :note: 

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

720 

721 :return: 

722 self 

723 """ 

724 if self.repo.bare: 

725 return self 

726 # END pass in bare mode 

727 

728 if progress is None: 

729 progress = UpdateProgress() 

730 # END handle progress 

731 prefix = "" 

732 if dry_run: 

733 prefix = "DRY-RUN: " 

734 # END handle prefix 

735 

736 # To keep things plausible in dry-run mode. 

737 if dry_run: 

738 mrepo = None 

739 # END init mrepo 

740 

741 try: 

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

743 ####################################### 

744 try: 

745 mrepo = self.module() 

746 rmts = mrepo.remotes 

747 len_rmts = len(rmts) 

748 for i, remote in enumerate(rmts): 

749 op = FETCH 

750 if i == 0: 

751 op |= BEGIN 

752 # END handle start 

753 

754 progress.update( 

755 op, 

756 i, 

757 len_rmts, 

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

759 ) 

760 # =============================== 

761 if not dry_run: 

762 remote.fetch(progress=progress) 

763 # END handle dry-run 

764 # =============================== 

765 if i == len_rmts - 1: 

766 op |= END 

767 # END handle end 

768 progress.update( 

769 op, 

770 i, 

771 len_rmts, 

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

773 ) 

774 # END fetch new data 

775 except InvalidGitRepositoryError: 

776 mrepo = None 

777 if not init: 

778 return self 

779 # END early abort if init is not allowed 

780 

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

782 checkout_module_abspath = self.abspath 

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

784 try: 

785 os.rmdir(checkout_module_abspath) 

786 except OSError as e: 

787 raise OSError( 

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

789 ) from e 

790 # END handle OSError 

791 # END handle directory removal 

792 

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

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

795 progress.update( 

796 BEGIN | CLONE, 

797 0, 

798 1, 

799 prefix 

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

801 ) 

802 if not dry_run: 

803 if self.url.startswith("."): 

804 url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url) 

805 else: 

806 url = self.url 

807 mrepo = self._clone_repo( 

808 self.repo, 

809 url, 

810 self.path, 

811 self.name, 

812 n=True, 

813 env=env, 

814 multi_options=clone_multi_options, 

815 allow_unsafe_options=allow_unsafe_options, 

816 allow_unsafe_protocols=allow_unsafe_protocols, 

817 ) 

818 # END handle dry-run 

819 progress.update( 

820 END | CLONE, 

821 0, 

822 1, 

823 prefix + "Done cloning to %s" % checkout_module_abspath, 

824 ) 

825 

826 if not dry_run: 

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

828 try: 

829 mrepo = cast("Repo", mrepo) 

830 # Find a remote which has our branch - we try to be flexible. 

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

832 local_branch = mkhead(mrepo, self.branch_path) 

833 

834 # Have a valid branch, but no checkout - make sure we can figure 

835 # that out by marking the commit with a null_sha. 

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

837 # END initial checkout + branch creation 

838 

839 # Make sure HEAD is not detached. 

840 mrepo.head.set_reference( 

841 local_branch, 

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

843 ) 

844 mrepo.head.reference.set_tracking_branch(remote_branch) 

845 except (IndexError, InvalidGitRepositoryError): 

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

847 # END handle tracking branch 

848 

849 # NOTE: Have to write the repo config file as well, otherwise the 

850 # default implementation will be offended and not update the 

851 # repository. Maybe this is a good way to ensure it doesn't get into 

852 # our way, but we want to stay backwards compatible too... It's so 

853 # redundant! 

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

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

856 # END handle dry_run 

857 # END handle initialization 

858 

859 # DETERMINE SHAS TO CHECK OUT 

860 ############################# 

861 binsha = self.binsha 

862 hexsha = self.hexsha 

863 if mrepo is not None: 

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

865 # existed. 

866 is_detached = mrepo.head.is_detached 

867 # END handle dry_run 

868 

869 if mrepo is not None and to_latest_revision: 

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

871 if not is_detached: 

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

873 if rref is not None: 

874 rcommit = rref.commit 

875 binsha = rcommit.binsha 

876 hexsha = rcommit.hexsha 

877 else: 

878 _logger.error( 

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

880 msg_base, 

881 mrepo.head.reference, 

882 ) 

883 # END handle remote ref 

884 else: 

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

886 # END handle detached head 

887 # END handle to_latest_revision option 

888 

889 # Update the working tree. 

890 # Handles dry_run. 

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

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

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

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

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

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

897 # changes the user had done. 

898 may_reset = True 

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

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

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

902 if force: 

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

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

905 _logger.debug(msg) 

906 else: 

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

908 msg %= ( 

909 is_detached and "checkout" or "reset", 

910 mrepo.head, 

911 mrepo, 

912 ) 

913 _logger.info(msg) 

914 may_reset = False 

915 # END handle force 

916 # END handle if we are in the future 

917 

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

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

920 # END handle force and dirty state 

921 # END handle empty repo 

922 

923 # END verify future/past 

924 progress.update( 

925 BEGIN | UPDWKTREE, 

926 0, 

927 1, 

928 prefix 

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

930 ) 

931 

932 if not dry_run and may_reset: 

933 if is_detached: 

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

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

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

937 # for future options regarding rebase and merge. 

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

939 else: 

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

941 # END handle checkout 

942 # If we may reset/checkout. 

943 progress.update( 

944 END | UPDWKTREE, 

945 0, 

946 1, 

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

948 ) 

949 # END update to new commit only if needed 

950 except Exception as err: 

951 if not keep_going: 

952 raise 

953 _logger.error(str(err)) 

954 # END handle keep_going 

955 

956 # HANDLE RECURSION 

957 ################## 

958 if recursive: 

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

960 if mrepo is not None: 

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

962 submodule.update( 

963 recursive, 

964 init, 

965 to_latest_revision, 

966 progress=progress, 

967 dry_run=dry_run, 

968 force=force, 

969 keep_going=keep_going, 

970 ) 

971 # END handle recursive update 

972 # END handle dry run 

973 # END for each submodule 

974 

975 return self 

976 

977 @unbare_repo 

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

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

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

981 adjusting our index entry accordingly. 

982 

983 :param module_path: 

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

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

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

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

988 

989 :param configuration: 

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

991 to the given path. 

992 

993 :param module: 

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

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

996 parent repository in an inconsistent state. 

997 

998 :return: 

999 self 

1000 

1001 :raise ValueError: 

1002 If the module path existed and was not empty, or was a file. 

1003 

1004 :note: 

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

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

1007 """ 

1008 if module + configuration < 1: 

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

1010 # END handle input 

1011 

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

1013 

1014 # VERIFY DESTINATION 

1015 if module_checkout_path == self.path: 

1016 return self 

1017 # END handle no change 

1018 

1019 module_checkout_abspath = join_path_native(str(self.repo.working_tree_dir), module_checkout_path) 

1020 if osp.isfile(module_checkout_abspath): 

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

1022 # END handle target files 

1023 

1024 index = self.repo.index 

1025 tekey = index.entry_key(module_checkout_path, 0) 

1026 # if the target item already exists, fail 

1027 if configuration and tekey in index.entries: 

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

1029 # END handle index key already there 

1030 

1031 # Remove existing destination. 

1032 if module: 

1033 if osp.exists(module_checkout_abspath): 

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

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

1036 # END handle non-emptiness 

1037 

1038 if osp.islink(module_checkout_abspath): 

1039 os.remove(module_checkout_abspath) 

1040 else: 

1041 os.rmdir(module_checkout_abspath) 

1042 # END handle link 

1043 else: 

1044 # Recreate parent directories. 

1045 # NOTE: renames() does that now. 

1046 pass 

1047 # END handle existence 

1048 # END handle module 

1049 

1050 # Move the module into place if possible. 

1051 cur_path = self.abspath 

1052 renamed_module = False 

1053 if module and osp.exists(cur_path): 

1054 os.renames(cur_path, module_checkout_abspath) 

1055 renamed_module = True 

1056 

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

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

1059 self._write_git_file_and_module_config(module_checkout_abspath, module_abspath) 

1060 # END handle git file rewrite 

1061 # END move physical module 

1062 

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

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

1065 previous_sm_path = self.path 

1066 try: 

1067 if configuration: 

1068 try: 

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

1070 entry = index.entries[ekey] 

1071 del index.entries[ekey] 

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

1073 index.entries[tekey] = nentry 

1074 except KeyError as e: 

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

1076 # END handle submodule doesn't exist 

1077 

1078 # Update configuration. 

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

1080 writer.set_value("path", module_checkout_path) 

1081 self.path = module_checkout_path 

1082 # END handle configuration flag 

1083 except Exception: 

1084 if renamed_module: 

1085 os.renames(module_checkout_abspath, cur_path) 

1086 # END undo module renaming 

1087 raise 

1088 # END handle undo rename 

1089 

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

1091 # directory. 

1092 if previous_sm_path == self.name: 

1093 self.rename(module_checkout_path) 

1094 

1095 return self 

1096 

1097 @unbare_repo 

1098 def remove( 

1099 self, 

1100 module: bool = True, 

1101 force: bool = False, 

1102 configuration: bool = True, 

1103 dry_run: bool = False, 

1104 ) -> "Submodule": 

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

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

1107 

1108 :param module: 

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

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

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

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

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

1114 have been altered. 

1115 

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

1117 to touching the direct submodule. 

1118 

1119 :param force: 

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

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

1122 

1123 :param configuration: 

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

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

1126 you to safely delete the repository of your submodule. 

1127 

1128 :param dry_run: 

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

1130 usually throw. 

1131 

1132 :return: 

1133 self 

1134 

1135 :note: 

1136 Doesn't work in bare repositories. 

1137 

1138 :note: 

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

1140 leave an inconsistent state. 

1141 

1142 :raise git.exc.InvalidGitRepositoryError: 

1143 Thrown if the repository cannot be deleted. 

1144 

1145 :raise OSError: 

1146 If directories or files could not be removed. 

1147 """ 

1148 if not (module or configuration): 

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

1150 # END handle parameters 

1151 

1152 # Recursively remove children of this submodule. 

1153 nc = 0 

1154 for csm in self.children(): 

1155 nc += 1 

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

1157 del csm 

1158 

1159 if configuration and not dry_run and nc > 0: 

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

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

1162 # expected. 

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

1164 # END handle recursion 

1165 

1166 # DELETE REPOSITORY WORKING TREE 

1167 ################################ 

1168 if module and self.module_exists(): 

1169 mod = self.module() 

1170 git_dir = mod.git_dir 

1171 if force: 

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

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

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

1175 # submodules first. 

1176 mp = self.abspath 

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

1178 if osp.islink(mp): 

1179 method = os.remove 

1180 elif osp.isdir(mp): 

1181 method = rmtree 

1182 elif osp.exists(mp): 

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

1184 # END handle brutal deletion 

1185 if not dry_run: 

1186 assert method 

1187 method(mp) 

1188 # END apply deletion method 

1189 else: 

1190 # Verify we may delete our module. 

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

1192 raise InvalidGitRepositoryError( 

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

1194 % mod.working_tree_dir 

1195 ) 

1196 # END check for dirt 

1197 

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

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

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

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

1202 for remote in mod.remotes: 

1203 num_branches_with_new_commits = 0 

1204 rrefs = remote.refs 

1205 for rref in rrefs: 

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

1207 # END for each remote ref 

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

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

1210 raise InvalidGitRepositoryError( 

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

1212 ) 

1213 # END handle new commits 

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

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

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

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

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

1219 if len(rrefs): 

1220 del rref # skipcq: PYL-W0631 

1221 # END handle remotes 

1222 del rrefs 

1223 del remote 

1224 # END for each remote 

1225 

1226 # Finally delete our own submodule. 

1227 if not dry_run: 

1228 self._clear_cache() 

1229 wtd = mod.working_tree_dir 

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

1231 gc.collect() 

1232 rmtree(str(wtd)) 

1233 # END delete tree if possible 

1234 # END handle force 

1235 

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

1237 self._clear_cache() 

1238 rmtree(git_dir) 

1239 # END handle separate bare repository 

1240 # END handle module deletion 

1241 

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

1243 if not dry_run: 

1244 self._clear_cache() 

1245 

1246 # DELETE CONFIGURATION 

1247 ###################### 

1248 if configuration and not dry_run: 

1249 # First the index-entry. 

1250 parent_index = self.repo.index 

1251 try: 

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

1253 except KeyError: 

1254 pass 

1255 # END delete entry 

1256 parent_index.write() 

1257 

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

1259 # information anymore. 

1260 

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

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

1263 

1264 with self.config_writer() as sc_writer: 

1265 sc_writer.remove_section() 

1266 # END delete configuration 

1267 

1268 return self 

1269 

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

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

1272 contain the ``.gitmodules`` blob. 

1273 

1274 :param commit: 

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

1276 to the most recent commit. 

1277 

1278 :param check: 

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

1280 validity of the submodule. 

1281 

1282 :raise ValueError: 

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

1284 

1285 :raise ValueError: 

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

1287 

1288 :return: 

1289 self 

1290 """ 

1291 if commit is None: 

1292 self._parent_commit = None 

1293 return self 

1294 # END handle None 

1295 pcommit = self.repo.commit(commit) 

1296 pctree = pcommit.tree 

1297 if self.k_modules_file not in pctree: 

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

1299 # END handle exceptions 

1300 

1301 prev_pc = self._parent_commit 

1302 self._parent_commit = pcommit 

1303 

1304 if check: 

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

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

1307 self._parent_commit = prev_pc 

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

1309 # END handle submodule did not exist 

1310 # END handle checking mode 

1311 

1312 # Update our sha, it could have changed. 

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

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

1315 try: 

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

1317 except KeyError: 

1318 self.binsha = self.NULL_BIN_SHA 

1319 

1320 self._clear_cache() 

1321 return self 

1322 

1323 @unbare_repo 

1324 def config_writer( 

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

1326 ) -> SectionConstraint["SubmoduleConfigParser"]: 

1327 """ 

1328 :return: 

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

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

1331 

1332 :param index: 

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

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

1335 repository. 

1336 

1337 :param write: 

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

1339 

1340 :note: 

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

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

1343 yourself once the whole operation is complete. 

1344 

1345 :raise ValueError: 

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

1347 current head commit. 

1348 

1349 :raise IOError: 

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

1351 """ 

1352 writer = self._config_parser_constrained(read_only=False) 

1353 if index is not None: 

1354 writer.config._index = index 

1355 writer.config._auto_write = write 

1356 return writer 

1357 

1358 @unbare_repo 

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

1360 """Rename this submodule. 

1361 

1362 :note: 

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

1364 

1365 * ``$parent_git_dir / config`` 

1366 * ``$working_tree_dir / .gitmodules`` 

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

1368 

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

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

1371 

1372 :return: 

1373 This :class:`Submodule` instance 

1374 """ 

1375 if self.name == new_name: 

1376 return self 

1377 

1378 # .git/config 

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

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

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

1382 # entries. 

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

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

1385 

1386 # .gitmodules 

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

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

1389 

1390 self._name = new_name 

1391 

1392 # .git/modules 

1393 mod = self.module() 

1394 if mod.has_separate_working_tree(): 

1395 destination_module_abspath = self._module_abspath(self.repo, self.path, new_name) 

1396 source_dir = mod.git_dir 

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

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

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

1400 os.renames(source_dir, tmp_dir) 

1401 source_dir = tmp_dir 

1402 # END handle self-containment 

1403 os.renames(source_dir, destination_module_abspath) 

1404 if mod.working_tree_dir: 

1405 self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath) 

1406 # END move separate git repository 

1407 

1408 return self 

1409 

1410 # } END edit interface 

1411 

1412 # { Query Interface 

1413 

1414 @unbare_repo 

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

1416 """ 

1417 :return: 

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

1419 submodule path 

1420 

1421 :raise git.exc.InvalidGitRepositoryError: 

1422 If a repository was not available. 

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

1424 """ 

1425 module_checkout_abspath = self.abspath 

1426 try: 

1427 repo = git.Repo(module_checkout_abspath) 

1428 if repo != self.repo: 

1429 return repo 

1430 # END handle repo uninitialized 

1431 except (InvalidGitRepositoryError, NoSuchPathError) as e: 

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

1433 else: 

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

1435 # END handle exceptions 

1436 

1437 def module_exists(self) -> bool: 

1438 """ 

1439 :return: 

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

1441 See the :meth:`module` method. 

1442 """ 

1443 try: 

1444 self.module() 

1445 return True 

1446 except Exception: 

1447 return False 

1448 # END handle exception 

1449 

1450 def exists(self) -> bool: 

1451 """ 

1452 :return: 

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

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

1455 though its module doesn't exist on disk. 

1456 """ 

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

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

1459 loc = locals() 

1460 for attr in self._cache_attrs: 

1461 try: 

1462 if hasattr(self, attr): 

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

1464 # END if we have the attribute cache 

1465 except (cp.NoSectionError, ValueError): 

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

1467 # happen on PY2. 

1468 pass 

1469 # END for each attr 

1470 self._clear_cache() 

1471 

1472 try: 

1473 try: 

1474 self.path # noqa: B018 

1475 return True 

1476 except Exception: 

1477 return False 

1478 # END handle exceptions 

1479 finally: 

1480 for attr in self._cache_attrs: 

1481 if attr in loc: 

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

1483 # END if we have a cache 

1484 # END reapply each attribute 

1485 # END handle object state consistency 

1486 

1487 @property 

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

1489 """ 

1490 :return: 

1491 The branch instance that we are to checkout 

1492 

1493 :raise git.exc.InvalidGitRepositoryError: 

1494 If our module is not yet checked out. 

1495 """ 

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

1497 

1498 @property 

1499 def branch_path(self) -> PathLike: 

1500 """ 

1501 :return: 

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

1503 the remote and track 

1504 """ 

1505 return self._branch_path 

1506 

1507 @property 

1508 def branch_name(self) -> str: 

1509 """ 

1510 :return: 

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

1512 """ 

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

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

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

1516 

1517 @property 

1518 def url(self) -> str: 

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

1520 return self._url 

1521 

1522 @property 

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

1524 """ 

1525 :return: 

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

1527 ``.gitmodules`` file 

1528 

1529 :note: 

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

1531 """ 

1532 if self._parent_commit is None: 

1533 return self.repo.commit() 

1534 return self._parent_commit 

1535 

1536 @property 

1537 def name(self) -> str: 

1538 """ 

1539 :return: 

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

1541 ``.gitmodules`` file. 

1542 

1543 :note: 

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

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

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

1547 """ 

1548 return self._name 

1549 

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

1551 """ 

1552 :return: 

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

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

1555 

1556 :note: 

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

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

1559 

1560 :note: 

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

1562 

1563 :raise IOError: 

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

1565 """ 

1566 return self._config_parser_constrained(read_only=True) 

1567 

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

1569 """ 

1570 :return: 

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

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

1573 checked out. 

1574 """ 

1575 return self._get_intermediate_items(self) 

1576 

1577 # } END query interface 

1578 

1579 # { Iterable Interface 

1580 

1581 @classmethod 

1582 def iter_items( 

1583 cls, 

1584 repo: "Repo", 

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

1586 *args: Any, 

1587 **kwargs: Any, 

1588 ) -> Iterator["Submodule"]: 

1589 """ 

1590 :return: 

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

1592 repository 

1593 """ 

1594 try: 

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

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

1597 except (IOError, BadName): 

1598 return 

1599 # END handle empty iterator 

1600 

1601 for sms in parser.sections(): 

1602 n = sm_name(sms) 

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

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

1605 b = cls.k_head_default 

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

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

1608 # END handle optional information 

1609 

1610 # Get the binsha. 

1611 index = repo.index 

1612 try: 

1613 rt = pc.tree # Root tree 

1614 sm = rt[p] 

1615 except KeyError: 

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

1617 try: 

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

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

1620 except KeyError: 

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

1622 # .gitmodules file. 

1623 continue 

1624 # END handle keyerror 

1625 # END handle critical error 

1626 

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

1628 if type(sm) is not git.objects.submodule.base.Submodule: 

1629 continue 

1630 

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

1632 sm._name = n 

1633 if pc != repo.commit(): 

1634 sm._parent_commit = pc 

1635 # END set only if not most recent! 

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

1637 sm._url = u 

1638 

1639 yield sm 

1640 # END for each section 

1641 

1642 # } END iterable interface