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

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

613 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 List, 

53 Mapping, 

54 Sequence, 

55 TYPE_CHECKING, 

56 Union, 

57 cast, 

58) 

59 

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

61 from typing import Literal 

62else: 

63 from typing_extensions import Literal 

64 

65from git.types import Commit_ish, PathLike, TBD 

66 

67if TYPE_CHECKING: 

68 from git.index import IndexFile 

69 from git.objects.commit import Commit 

70 from git.refs import Head, RemoteReference 

71 from git.repo import Repo 

72 

73# ----------------------------------------------------------------------------- 

74 

75_logger = logging.getLogger(__name__) 

76 

77 

78class UpdateProgress(RemoteProgress): 

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

80 derive from it and implement the 

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

82 

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

84 _num_op_codes: int = RemoteProgress._num_op_codes + 3 

85 

86 __slots__ = () 

87 

88 

89BEGIN = UpdateProgress.BEGIN 

90END = UpdateProgress.END 

91CLONE = UpdateProgress.CLONE 

92FETCH = UpdateProgress.FETCH 

93UPDWKTREE = UpdateProgress.UPDWKTREE 

94 

95 

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

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

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

99class Submodule(IndexObject, TraversableIterableObj): 

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

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

102 at the path of this instance. 

103 

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

105 solely as a marker in the tree and index. 

106 

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

108 """ 

109 

110 _id_attribute_ = "name" 

111 k_modules_file = ".gitmodules" 

112 k_head_option = "branch" 

113 k_head_default = "master" 

114 k_default_mode = stat.S_IFDIR | stat.S_IFLNK 

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

116 

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

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

119 

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

121 

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

123 

124 def __init__( 

125 self, 

126 repo: "Repo", 

127 binsha: bytes, 

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

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

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

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

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

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

134 ) -> None: 

135 """Initialize this instance with its attributes. 

136 

137 We only document the parameters that differ from 

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

139 

140 :param repo: 

141 Our parent repository. 

142 

143 :param binsha: 

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

145 See the `url` parameter. 

146 

147 :param parent_commit: 

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

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

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

151 

152 :param url: 

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

154 

155 :param branch_path: 

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

157 repository. 

158 """ 

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

160 self.size = 0 

161 self._parent_commit = parent_commit 

162 if url is not None: 

163 self._url = url 

164 if branch_path is not None: 

165 self._branch_path = branch_path 

166 if name is not None: 

167 self._name = name 

168 

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

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

171 reader: SectionConstraint = self.config_reader() 

172 # Default submodule values. 

173 try: 

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

175 except cp.NoSectionError as e: 

176 if self.repo.working_tree_dir is not None: 

177 raise ValueError( 

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

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

180 ) from e 

181 

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

183 # GitPython extension values - optional. 

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

185 elif attr == "_name": 

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

187 else: 

188 super()._set_cache_(attr) 

189 # END handle attribute name 

190 

191 @classmethod 

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

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

194 try: 

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

196 except InvalidGitRepositoryError: 

197 return IterableList("") 

198 # END handle intermediate items 

199 

200 @classmethod 

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

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

203 

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

205 """Compare with another submodule.""" 

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

207 # Otherwise this type wouldn't be hashable. 

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

209 return self._name == other._name 

210 

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

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

213 return not (self == other) 

214 

215 def __hash__(self) -> int: 

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

217 return hash(self._name) 

218 

219 def __str__(self) -> str: 

220 return self._name 

221 

222 def __repr__(self) -> str: 

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

224 type(self).__name__, 

225 self._name, 

226 self.path, 

227 self.url, 

228 self.branch_path, 

229 ) 

230 

231 @classmethod 

232 def _config_parser( 

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

234 ) -> SubmoduleConfigParser: 

235 """ 

236 :return: 

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

238 

239 :raise IOError: 

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

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

242 delayed until the first access of the config parser. 

243 """ 

244 parent_matches_head = True 

245 if parent_commit is not None: 

246 try: 

247 parent_matches_head = repo.head.commit == parent_commit 

248 except ValueError: 

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

250 # to a valid ref. 

251 pass 

252 # END handle parent_commit 

253 fp_module: Union[str, BytesIO] 

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

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

256 else: 

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

258 try: 

259 fp_module = cls._sio_modules(parent_commit) 

260 except KeyError as e: 

261 raise IOError( 

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

263 ) from e 

264 # END handle exceptions 

265 # END handle non-bare working tree 

266 

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

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

269 # END handle writes of historical submodules 

270 

271 return SubmoduleConfigParser(fp_module, read_only=read_only) 

272 

273 def _clear_cache(self) -> None: 

274 """Clear the possibly changed values.""" 

275 for name in self._cache_attrs: 

276 try: 

277 delattr(self, name) 

278 except AttributeError: 

279 pass 

280 # END try attr deletion 

281 # END for each name to delete 

282 

283 @classmethod 

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

285 """ 

286 :return: 

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

288 respective blob's data 

289 """ 

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

291 sio.name = cls.k_modules_file 

292 return sio 

293 

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

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

296 try: 

297 pc = self.parent_commit 

298 except ValueError: 

299 pc = None 

300 # END handle empty parent repository 

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

302 parser.set_submodule(self) 

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

304 

305 @classmethod 

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

307 if cls._need_gitfile_submodules(parent_repo.git): 

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

309 if parent_repo.working_tree_dir: 

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

311 raise NotADirectoryError() 

312 

313 @classmethod 

314 def _clone_repo( 

315 cls, 

316 repo: "Repo", 

317 url: str, 

318 path: PathLike, 

319 name: str, 

320 allow_unsafe_options: bool = False, 

321 allow_unsafe_protocols: bool = False, 

322 **kwargs: Any, 

323 ) -> "Repo": 

324 """ 

325 :return: 

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

327 

328 :param repo: 

329 Our parent repository. 

330 

331 :param url: 

332 URL to clone from. 

333 

334 :param path: 

335 Repository-relative path to the submodule checkout location. 

336 

337 :param name: 

338 Canonical name of the submodule. 

339 

340 :param allow_unsafe_protocols: 

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

342 

343 :param allow_unsafe_options: 

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

345 

346 :param kwargs: 

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

348 """ 

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

350 module_checkout_path = module_abspath 

351 if cls._need_gitfile_submodules(repo.git): 

352 kwargs["separate_git_dir"] = module_abspath 

353 module_abspath_dir = osp.dirname(module_abspath) 

354 if not osp.isdir(module_abspath_dir): 

355 os.makedirs(module_abspath_dir) 

356 module_checkout_path = osp.join(repo.working_tree_dir, path) # type: ignore[arg-type] 

357 

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

359 remote_name = cast("RemoteReference", repo.active_branch.tracking_branch()).remote_name 

360 repo_remote_url = repo.remote(remote_name).url 

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

362 

363 clone = git.Repo.clone_from( 

364 url, 

365 module_checkout_path, 

366 allow_unsafe_options=allow_unsafe_options, 

367 allow_unsafe_protocols=allow_unsafe_protocols, 

368 **kwargs, 

369 ) 

370 if cls._need_gitfile_submodules(repo.git): 

371 cls._write_git_file_and_module_config(module_checkout_path, module_abspath) 

372 

373 return clone 

374 

375 @classmethod 

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

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

378 

379 :raise ValueError: 

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

381 """ 

382 path = to_native_path_linux(path) 

383 if path.endswith("/"): 

384 path = path[:-1] 

385 # END handle trailing slash 

386 

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

388 working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir) 

389 if not path.startswith(working_tree_linux): 

390 raise ValueError( 

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

392 % (working_tree_linux, path) 

393 ) 

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

395 if not path: 

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

397 # END verify converted relative path makes sense 

398 # END convert to a relative path 

399 

400 return path 

401 

402 @classmethod 

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

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

405 git module repository. 

406 

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

408 relative to the `working_tree_dir`. 

409 

410 :note: 

411 This will overwrite existing files! 

412 

413 :note: 

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

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

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

417 one. 

418 

419 :param working_tree_dir: 

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

421 

422 :param module_abspath: 

423 Absolute path to the bare repository. 

424 """ 

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

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

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

428 os.remove(git_file) 

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

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

431 

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

433 writer.set_value( 

434 "core", 

435 "worktree", 

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

437 ) 

438 

439 # { Edit Interface 

440 

441 @classmethod 

442 def add( 

443 cls, 

444 repo: "Repo", 

445 name: str, 

446 path: PathLike, 

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

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

449 no_checkout: bool = False, 

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

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

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

453 allow_unsafe_options: bool = False, 

454 allow_unsafe_protocols: bool = False, 

455 ) -> "Submodule": 

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

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

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

459 provided, the existing submodule will be returned. 

460 

461 :param repo: 

462 Repository instance which should receive the submodule. 

463 

464 :param name: 

465 The name/identifier for the submodule. 

466 

467 :param path: 

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

469 located. 

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

471 

472 :param url: 

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

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

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

476 existing repository a submodule of another one. 

477 

478 :param branch: 

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

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

481 locally as a tracking branch. 

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

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

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

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

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

487 

488 :param no_checkout: 

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

490 will be performed. 

491 

492 :param depth: 

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

494 commits. 

495 

496 :param env: 

497 Optional dictionary containing the desired environment variables. 

498 

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

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

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

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

503 value. 

504 

505 :param clone_multi_options: 

506 A list of clone options. Please see 

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

508 

509 :param allow_unsafe_protocols: 

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

511 

512 :param allow_unsafe_options: 

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

514 

515 :return: 

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

517 

518 :note: 

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

520 repository update fails. 

521 """ 

522 if repo.bare: 

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

524 # END handle bare repos 

525 

526 path = cls._to_relative_path(repo, path) 

527 

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

529 if url is not None: 

530 url = to_native_path_linux(url) 

531 # END ensure URL correctness 

532 

533 # INSTANTIATE INTERMEDIATE SM 

534 sm = cls( 

535 repo, 

536 cls.NULL_BIN_SHA, 

537 cls.k_default_mode, 

538 path, 

539 name, 

540 url="invalid-temporary", 

541 ) 

542 if sm.exists(): 

543 # Reretrieve submodule from tree. 

544 try: 

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

546 sm._name = name 

547 return sm 

548 except KeyError: 

549 # Could only be in index. 

550 index = repo.index 

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

552 sm.binsha = entry.binsha 

553 return sm 

554 # END handle exceptions 

555 # END handle existing 

556 

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

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

559 has_module = sm.module_exists() 

560 branch_is_default = branch is None 

561 if has_module and url is not None: 

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

563 raise ValueError( 

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

565 ) 

566 # END check url 

567 # END verify urls match 

568 

569 mrepo: Union[Repo, None] = None 

570 

571 if url is None: 

572 if not has_module: 

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

574 # END check url 

575 mrepo = sm.module() 

576 # assert isinstance(mrepo, git.Repo) 

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

578 if not urls: 

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

580 # END verify we have url 

581 url = urls[0] 

582 else: 

583 # Clone new repo. 

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

585 if not branch_is_default: 

586 kwargs["b"] = br.name 

587 # END setup checkout-branch 

588 

589 if depth: 

590 if isinstance(depth, int): 

591 kwargs["depth"] = depth 

592 else: 

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

594 if clone_multi_options: 

595 kwargs["multi_options"] = clone_multi_options 

596 

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

598 mrepo = cls._clone_repo( 

599 repo, 

600 url, 

601 path, 

602 name, 

603 env=env, 

604 allow_unsafe_options=allow_unsafe_options, 

605 allow_unsafe_protocols=allow_unsafe_protocols, 

606 **kwargs, 

607 ) 

608 # END verify url 

609 

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

611 url = Git.polish_url(url) 

612 

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

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

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

616 # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one 

617 writer: Union[GitConfigParser, SectionConstraint] 

618 

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

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

621 

622 # Update configuration and index. 

623 index = sm.repo.index 

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

625 writer.set_value("url", url) 

626 writer.set_value("path", path) 

627 

628 sm._url = url 

629 if not branch_is_default: 

630 # Store full path. 

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

632 sm._branch_path = br.path 

633 

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

635 if mrepo: 

636 sm.binsha = mrepo.head.commit.binsha 

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

638 

639 return sm 

640 

641 def update( 

642 self, 

643 recursive: bool = False, 

644 init: bool = True, 

645 to_latest_revision: bool = False, 

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

647 dry_run: bool = False, 

648 force: bool = False, 

649 keep_going: bool = False, 

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

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

652 allow_unsafe_options: bool = False, 

653 allow_unsafe_protocols: bool = False, 

654 ) -> "Submodule": 

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

656 with the binsha of this instance. 

657 

658 :param recursive: 

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

660 

661 :param init: 

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

663 

664 :param to_latest_revision: 

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

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

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

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

669 for this submodule and the branch existed remotely. 

670 

671 :param progress: 

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

673 shown. 

674 

675 :param dry_run: 

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

677 All performed operations are read-only. 

678 

679 :param force: 

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

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

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

683 This will essentially 'forget' commits. 

684 

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

686 respective remote branches will simply not be moved. 

687 

688 :param keep_going: 

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

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

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

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

693 when updating submodules. 

694 

695 :param env: 

696 Optional dictionary containing the desired environment variables. 

697 

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

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

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

701 

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

703 its value. 

704 

705 :param clone_multi_options: 

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

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

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

709 

710 :param allow_unsafe_protocols: 

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

712 

713 :param allow_unsafe_options: 

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

715 

716 :note: 

717 Does nothing in bare repositories. 

718 

719 :note: 

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

721 

722 :return: 

723 self 

724 """ 

725 if self.repo.bare: 

726 return self 

727 # END pass in bare mode 

728 

729 if progress is None: 

730 progress = UpdateProgress() 

731 # END handle progress 

732 prefix = "" 

733 if dry_run: 

734 prefix = "DRY-RUN: " 

735 # END handle prefix 

736 

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

738 if dry_run: 

739 mrepo = None 

740 # END init mrepo 

741 

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

743 rmts = module_repo.remotes 

744 len_rmts = len(rmts) 

745 for i, remote in enumerate(rmts): 

746 op = FETCH 

747 if i == 0: 

748 op |= BEGIN 

749 # END handle start 

750 

751 progress.update( 

752 op, 

753 i, 

754 len_rmts, 

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

756 ) 

757 # =============================== 

758 if not dry_run: 

759 remote.fetch(progress=progress) 

760 # END handle dry-run 

761 # =============================== 

762 if i == len_rmts - 1: 

763 op |= END 

764 # END handle end 

765 progress.update( 

766 op, 

767 i, 

768 len_rmts, 

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

770 ) 

771 # END fetch new data 

772 

773 try: 

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

775 ####################################### 

776 try: 

777 mrepo = self.module() 

778 fetch_remotes(mrepo) 

779 except InvalidGitRepositoryError: 

780 mrepo = None 

781 if not init: 

782 return self 

783 # END early abort if init is not allowed 

784 

785 checkout_module_abspath = self.abspath 

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

787 

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

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

790 # repository instead of trying to clone over it. 

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

792 try: 

793 git.Repo(module_abspath) 

794 except InvalidGitRepositoryError: 

795 pass 

796 else: 

797 if osp.lexists(checkout_module_abspath) and ( 

798 osp.islink(checkout_module_abspath) 

799 or not osp.isdir(checkout_module_abspath) 

800 or os.listdir(checkout_module_abspath) 

801 ): 

802 raise OSError( 

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

804 ) 

805 os.makedirs(checkout_module_abspath, exist_ok=True) 

806 self._write_git_file_and_module_config(checkout_module_abspath, module_abspath) 

807 mrepo = git.Repo(checkout_module_abspath) 

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

809 fetch_remotes(mrepo) 

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

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

812 

813 if mrepo is None: 

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

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

816 try: 

817 os.rmdir(checkout_module_abspath) 

818 except OSError as e: 

819 raise OSError( 

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

821 ) from e 

822 # END handle directory removal 

823 

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

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

826 progress.update( 

827 BEGIN | CLONE, 

828 0, 

829 1, 

830 prefix 

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

832 ) 

833 if not dry_run: 

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

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

836 else: 

837 url = self.url 

838 mrepo = self._clone_repo( 

839 self.repo, 

840 url, 

841 self.path, 

842 self.name, 

843 n=True, 

844 env=env, 

845 multi_options=clone_multi_options, 

846 allow_unsafe_options=allow_unsafe_options, 

847 allow_unsafe_protocols=allow_unsafe_protocols, 

848 ) 

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

850 

851 if not dry_run: 

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

853 try: 

854 mrepo = cast("Repo", mrepo) 

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

856 local_branch = mkhead(mrepo, self.branch_path) 

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

858 mrepo.head.set_reference( 

859 local_branch, 

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

861 ) 

862 mrepo.head.reference.set_tracking_branch(remote_branch) 

863 except (IndexError, InvalidGitRepositoryError): 

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

865 

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

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

868 # END handle initialization 

869 

870 # DETERMINE SHAS TO CHECK OUT 

871 ############################# 

872 binsha = self.binsha 

873 hexsha = self.hexsha 

874 if mrepo is not None: 

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

876 # existed. 

877 is_detached = mrepo.head.is_detached 

878 # END handle dry_run 

879 

880 if mrepo is not None and to_latest_revision: 

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

882 if not is_detached: 

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

884 if rref is not None: 

885 rcommit = rref.commit 

886 binsha = rcommit.binsha 

887 hexsha = rcommit.hexsha 

888 else: 

889 _logger.error( 

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

891 msg_base, 

892 mrepo.head.reference, 

893 ) 

894 # END handle remote ref 

895 else: 

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

897 # END handle detached head 

898 # END handle to_latest_revision option 

899 

900 # Update the working tree. 

901 # Handles dry_run. 

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

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

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

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

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

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

908 # changes the user had done. 

909 may_reset = True 

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

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

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

913 if force: 

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

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

916 _logger.debug(msg) 

917 else: 

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

919 msg %= ( 

920 is_detached and "checkout" or "reset", 

921 mrepo.head, 

922 mrepo, 

923 ) 

924 _logger.info(msg) 

925 may_reset = False 

926 # END handle force 

927 # END handle if we are in the future 

928 

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

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

931 # END handle force and dirty state 

932 # END handle empty repo 

933 

934 # END verify future/past 

935 progress.update( 

936 BEGIN | UPDWKTREE, 

937 0, 

938 1, 

939 prefix 

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

941 ) 

942 

943 if not dry_run and may_reset: 

944 if is_detached: 

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

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

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

948 # for future options regarding rebase and merge. 

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

950 else: 

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

952 # END handle checkout 

953 # If we may reset/checkout. 

954 progress.update( 

955 END | UPDWKTREE, 

956 0, 

957 1, 

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

959 ) 

960 # END update to new commit only if needed 

961 except Exception as err: 

962 if not keep_going: 

963 raise 

964 _logger.error(str(err)) 

965 # END handle keep_going 

966 

967 # HANDLE RECURSION 

968 ################## 

969 if recursive: 

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

971 if mrepo is not None: 

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

973 submodule.update( 

974 recursive, 

975 init, 

976 to_latest_revision, 

977 progress=progress, 

978 dry_run=dry_run, 

979 force=force, 

980 keep_going=keep_going, 

981 ) 

982 # END handle recursive update 

983 # END handle dry run 

984 # END for each submodule 

985 

986 return self 

987 

988 @unbare_repo 

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

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

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

992 adjusting our index entry accordingly. 

993 

994 :param module_path: 

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

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

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

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

999 

1000 :param configuration: 

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

1002 to the given path. 

1003 

1004 :param module: 

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

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

1007 parent repository in an inconsistent state. 

1008 

1009 :return: 

1010 self 

1011 

1012 :raise ValueError: 

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

1014 

1015 :note: 

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

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

1018 """ 

1019 if module + configuration < 1: 

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

1021 # END handle input 

1022 

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

1024 

1025 # VERIFY DESTINATION 

1026 if module_checkout_path == self.path: 

1027 return self 

1028 # END handle no change 

1029 

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

1031 if osp.isfile(module_checkout_abspath): 

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

1033 # END handle target files 

1034 

1035 index = self.repo.index 

1036 tekey = index.entry_key(module_checkout_path, 0) 

1037 # if the target item already exists, fail 

1038 if configuration and tekey in index.entries: 

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

1040 # END handle index key already there 

1041 

1042 # Remove existing destination. 

1043 if module: 

1044 if osp.exists(module_checkout_abspath): 

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

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

1047 # END handle non-emptiness 

1048 

1049 if osp.islink(module_checkout_abspath): 

1050 os.remove(module_checkout_abspath) 

1051 else: 

1052 os.rmdir(module_checkout_abspath) 

1053 # END handle link 

1054 else: 

1055 # Recreate parent directories. 

1056 # NOTE: renames() does that now. 

1057 pass 

1058 # END handle existence 

1059 # END handle module 

1060 

1061 # Move the module into place if possible. 

1062 cur_path = self.abspath 

1063 renamed_module = False 

1064 if module and osp.exists(cur_path): 

1065 os.renames(cur_path, module_checkout_abspath) 

1066 renamed_module = True 

1067 

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

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

1070 self._write_git_file_and_module_config(module_checkout_abspath, module_abspath) 

1071 # END handle git file rewrite 

1072 # END move physical module 

1073 

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

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

1076 previous_sm_path = self.path 

1077 try: 

1078 if configuration: 

1079 try: 

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

1081 entry = index.entries[ekey] 

1082 del index.entries[ekey] 

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

1084 index.entries[tekey] = nentry 

1085 except KeyError as e: 

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

1087 # END handle submodule doesn't exist 

1088 

1089 # Update configuration. 

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

1091 writer.set_value("path", module_checkout_path) 

1092 self.path = module_checkout_path 

1093 # END handle configuration flag 

1094 except Exception: 

1095 if renamed_module: 

1096 os.renames(module_checkout_abspath, cur_path) 

1097 # END undo module renaming 

1098 raise 

1099 # END handle undo rename 

1100 

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

1102 # directory. 

1103 if previous_sm_path == self.name: 

1104 self.rename(module_checkout_path) 

1105 

1106 return self 

1107 

1108 @unbare_repo 

1109 def remove( 

1110 self, 

1111 module: bool = True, 

1112 force: bool = False, 

1113 configuration: bool = True, 

1114 dry_run: bool = False, 

1115 ) -> "Submodule": 

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

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

1118 

1119 :param module: 

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

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

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

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

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

1125 have been altered. 

1126 

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

1128 to touching the direct submodule. 

1129 

1130 :param force: 

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

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

1133 

1134 :param configuration: 

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

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

1137 you to safely delete the repository of your submodule. 

1138 

1139 :param dry_run: 

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

1141 usually throw. 

1142 

1143 :return: 

1144 self 

1145 

1146 :note: 

1147 Doesn't work in bare repositories. 

1148 

1149 :note: 

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

1151 leave an inconsistent state. 

1152 

1153 :raise git.exc.InvalidGitRepositoryError: 

1154 Thrown if the repository cannot be deleted. 

1155 

1156 :raise OSError: 

1157 If directories or files could not be removed. 

1158 """ 

1159 if not (module or configuration): 

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

1161 # END handle parameters 

1162 

1163 # Recursively remove children of this submodule. 

1164 nc = 0 

1165 for csm in self.children(): 

1166 nc += 1 

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

1168 del csm 

1169 

1170 if configuration and not dry_run and nc > 0: 

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

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

1173 # expected. 

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

1175 # END handle recursion 

1176 

1177 # DELETE REPOSITORY WORKING TREE 

1178 ################################ 

1179 if module and self.module_exists(): 

1180 mod = self.module() 

1181 git_dir = mod.git_dir 

1182 if force: 

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

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

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

1186 # submodules first. 

1187 mp = self.abspath 

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

1189 if osp.islink(mp): 

1190 method = os.remove 

1191 elif osp.isdir(mp): 

1192 method = rmtree 

1193 elif osp.exists(mp): 

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

1195 # END handle brutal deletion 

1196 if not dry_run: 

1197 assert method 

1198 method(mp) 

1199 # END apply deletion method 

1200 else: 

1201 # Verify we may delete our module. 

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

1203 raise InvalidGitRepositoryError( 

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

1205 % mod.working_tree_dir 

1206 ) 

1207 # END check for dirt 

1208 

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

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

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

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

1213 for remote in mod.remotes: 

1214 num_branches_with_new_commits = 0 

1215 rrefs = remote.refs 

1216 for rref in rrefs: 

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

1218 # END for each remote ref 

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

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

1221 raise InvalidGitRepositoryError( 

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

1223 ) 

1224 # END handle new commits 

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

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

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

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

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

1230 if len(rrefs): 

1231 del rref # skipcq: PYL-W0631 

1232 # END handle remotes 

1233 del rrefs 

1234 del remote 

1235 # END for each remote 

1236 

1237 # Finally delete our own submodule. 

1238 if not dry_run: 

1239 self._clear_cache() 

1240 wtd = mod.working_tree_dir 

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

1242 gc.collect() 

1243 rmtree(str(wtd)) 

1244 # END delete tree if possible 

1245 # END handle force 

1246 

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

1248 self._clear_cache() 

1249 rmtree(git_dir) 

1250 # END handle separate bare repository 

1251 # END handle module deletion 

1252 

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

1254 if not dry_run: 

1255 self._clear_cache() 

1256 

1257 # DELETE CONFIGURATION 

1258 ###################### 

1259 if configuration and not dry_run: 

1260 # First the index-entry. 

1261 parent_index = self.repo.index 

1262 try: 

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

1264 except KeyError: 

1265 pass 

1266 # END delete entry 

1267 parent_index.write() 

1268 

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

1270 # information anymore. 

1271 

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

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

1274 

1275 with self.config_writer() as sc_writer: 

1276 sc_writer.remove_section() 

1277 # END delete configuration 

1278 

1279 return self 

1280 

1281 @unbare_repo 

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

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

1284 

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

1286 which unregisters the submodule (removes its entry from 

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

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

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

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

1291 submodule from the retained contents. 

1292 

1293 :param force: 

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

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

1296 local modifications that would otherwise block the command. 

1297 

1298 :return: 

1299 self 

1300 

1301 :note: 

1302 Doesn't work in bare repositories. 

1303 """ 

1304 args: List[str] = [] 

1305 if force: 

1306 args.append("--force") 

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

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

1309 return self 

1310 

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

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

1313 contain the ``.gitmodules`` blob. 

1314 

1315 :param commit: 

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

1317 to the most recent commit. 

1318 

1319 :param check: 

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

1321 validity of the submodule. 

1322 

1323 :raise ValueError: 

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

1325 

1326 :raise ValueError: 

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

1328 

1329 :return: 

1330 self 

1331 """ 

1332 if commit is None: 

1333 self._parent_commit = None 

1334 return self 

1335 # END handle None 

1336 pcommit = self.repo.commit(commit) 

1337 pctree = pcommit.tree 

1338 if self.k_modules_file not in pctree: 

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

1340 # END handle exceptions 

1341 

1342 prev_pc = self._parent_commit 

1343 self._parent_commit = pcommit 

1344 

1345 if check: 

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

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

1348 self._parent_commit = prev_pc 

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

1350 # END handle submodule did not exist 

1351 # END handle checking mode 

1352 

1353 # Update our sha, it could have changed. 

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

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

1356 try: 

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

1358 except KeyError: 

1359 self.binsha = self.NULL_BIN_SHA 

1360 

1361 self._clear_cache() 

1362 return self 

1363 

1364 @unbare_repo 

1365 def config_writer( 

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

1367 ) -> SectionConstraint["SubmoduleConfigParser"]: 

1368 """ 

1369 :return: 

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

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

1372 

1373 :param index: 

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

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

1376 repository. 

1377 

1378 :param write: 

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

1380 

1381 :note: 

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

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

1384 yourself once the whole operation is complete. 

1385 

1386 :raise ValueError: 

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

1388 current head commit. 

1389 

1390 :raise IOError: 

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

1392 """ 

1393 writer = self._config_parser_constrained(read_only=False) 

1394 if index is not None: 

1395 writer.config._index = index 

1396 writer.config._auto_write = write 

1397 return writer 

1398 

1399 @unbare_repo 

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

1401 """Rename this submodule. 

1402 

1403 :note: 

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

1405 

1406 * ``$parent_git_dir / config`` 

1407 * ``$working_tree_dir / .gitmodules`` 

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

1409 

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

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

1412 

1413 :return: 

1414 This :class:`Submodule` instance 

1415 """ 

1416 if self.name == new_name: 

1417 return self 

1418 

1419 # .git/config 

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

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

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

1423 # entries. 

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

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

1426 

1427 # .gitmodules 

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

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

1430 

1431 self._name = new_name 

1432 

1433 # .git/modules 

1434 mod = self.module() 

1435 if mod.has_separate_working_tree(): 

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

1437 source_dir = mod.git_dir 

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

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

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

1441 os.renames(source_dir, tmp_dir) 

1442 source_dir = tmp_dir 

1443 # END handle self-containment 

1444 os.renames(source_dir, destination_module_abspath) 

1445 if mod.working_tree_dir: 

1446 self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath) 

1447 # END move separate git repository 

1448 

1449 return self 

1450 

1451 # } END edit interface 

1452 

1453 # { Query Interface 

1454 

1455 @unbare_repo 

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

1457 """ 

1458 :return: 

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

1460 submodule path 

1461 

1462 :raise git.exc.InvalidGitRepositoryError: 

1463 If a repository was not available. 

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

1465 """ 

1466 module_checkout_abspath = self.abspath 

1467 try: 

1468 repo = git.Repo(module_checkout_abspath) 

1469 if repo != self.repo: 

1470 return repo 

1471 # END handle repo uninitialized 

1472 except (InvalidGitRepositoryError, NoSuchPathError) as e: 

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

1474 else: 

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

1476 # END handle exceptions 

1477 

1478 def module_exists(self) -> bool: 

1479 """ 

1480 :return: 

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

1482 See the :meth:`module` method. 

1483 """ 

1484 try: 

1485 self.module() 

1486 return True 

1487 except Exception: 

1488 return False 

1489 # END handle exception 

1490 

1491 def exists(self) -> bool: 

1492 """ 

1493 :return: 

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

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

1496 though its module doesn't exist on disk. 

1497 """ 

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

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

1500 loc = locals() 

1501 for attr in self._cache_attrs: 

1502 try: 

1503 if hasattr(self, attr): 

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

1505 # END if we have the attribute cache 

1506 except (cp.NoSectionError, ValueError): 

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

1508 # happen on PY2. 

1509 pass 

1510 # END for each attr 

1511 self._clear_cache() 

1512 

1513 try: 

1514 try: 

1515 self.path # noqa: B018 

1516 return True 

1517 except Exception: 

1518 return False 

1519 # END handle exceptions 

1520 finally: 

1521 for attr in self._cache_attrs: 

1522 if attr in loc: 

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

1524 # END if we have a cache 

1525 # END reapply each attribute 

1526 # END handle object state consistency 

1527 

1528 @property 

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

1530 """ 

1531 :return: 

1532 The branch instance that we are to checkout 

1533 

1534 :raise git.exc.InvalidGitRepositoryError: 

1535 If our module is not yet checked out. 

1536 """ 

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

1538 

1539 @property 

1540 def branch_path(self) -> PathLike: 

1541 """ 

1542 :return: 

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

1544 the remote and track 

1545 """ 

1546 return self._branch_path 

1547 

1548 @property 

1549 def branch_name(self) -> str: 

1550 """ 

1551 :return: 

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

1553 """ 

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

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

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

1557 

1558 @property 

1559 def url(self) -> str: 

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

1561 return self._url 

1562 

1563 @property 

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

1565 """ 

1566 :return: 

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

1568 ``.gitmodules`` file 

1569 

1570 :note: 

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

1572 """ 

1573 if self._parent_commit is None: 

1574 return self.repo.commit() 

1575 return self._parent_commit 

1576 

1577 @property 

1578 def name(self) -> str: 

1579 """ 

1580 :return: 

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

1582 ``.gitmodules`` file. 

1583 

1584 :note: 

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

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

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

1588 """ 

1589 return self._name 

1590 

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

1592 """ 

1593 :return: 

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

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

1596 

1597 :note: 

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

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

1600 

1601 :note: 

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

1603 

1604 :raise IOError: 

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

1606 """ 

1607 return self._config_parser_constrained(read_only=True) 

1608 

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

1610 """ 

1611 :return: 

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

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

1614 checked out. 

1615 """ 

1616 return self._get_intermediate_items(self) 

1617 

1618 # } END query interface 

1619 

1620 # { Iterable Interface 

1621 

1622 @classmethod 

1623 def iter_items( 

1624 cls, 

1625 repo: "Repo", 

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

1627 *args: Any, 

1628 **kwargs: Any, 

1629 ) -> Iterator["Submodule"]: 

1630 """ 

1631 :return: 

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

1633 repository 

1634 """ 

1635 try: 

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

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

1638 except (IOError, BadName): 

1639 return 

1640 # END handle empty iterator 

1641 

1642 for sms in parser.sections(): 

1643 n = sm_name(sms) 

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

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

1646 b = cls.k_head_default 

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

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

1649 # END handle optional information 

1650 

1651 # Get the binsha. 

1652 index = repo.index 

1653 try: 

1654 rt = pc.tree # Root tree 

1655 sm = rt[p] 

1656 except KeyError: 

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

1658 try: 

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

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

1661 except KeyError: 

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

1663 # .gitmodules file. 

1664 continue 

1665 # END handle keyerror 

1666 # END handle critical error 

1667 

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

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

1670 continue 

1671 

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

1673 sm._name = n 

1674 if pc != repo.commit(): 

1675 sm._parent_commit = pc 

1676 # END set only if not most recent! 

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

1678 sm._url = u 

1679 

1680 yield sm 

1681 # END for each section 

1682 

1683 # } END iterable interface