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

643 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 ntpath 

10import os 

11import os.path as osp 

12import shlex 

13import stat 

14import sys 

15import uuid 

16import urllib 

17 

18import git 

19from git.cmd import Git 

20from git.compat import defenc 

21from git.config import GitConfigParser, SectionConstraint, cp 

22from git.exc import ( 

23 BadName, 

24 InvalidGitRepositoryError, 

25 NoSuchPathError, 

26 RepositoryDirtyError, 

27) 

28from git.objects.base import IndexObject, Object 

29from git.objects.util import TraversableIterableObj 

30from git.util import ( 

31 IterableList, 

32 RemoteProgress, 

33 _to_relative_path, 

34 join_path_native, 

35 rmtree, 

36 to_native_path_linux, 

37 unbare_repo, 

38) 

39 

40from .util import ( 

41 SubmoduleConfigParser, 

42 find_first_remote_branch, 

43 mkhead, 

44 sm_name, 

45 sm_section, 

46) 

47 

48# typing ---------------------------------------------------------------------- 

49 

50from typing import ( 

51 Any, 

52 Callable, 

53 Dict, 

54 Iterator, 

55 List, 

56 Mapping, 

57 Sequence, 

58 TYPE_CHECKING, 

59 Union, 

60 cast, 

61) 

62 

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

64 from typing import Literal 

65else: 

66 from typing_extensions import Literal 

67 

68from git.types import Commit_ish, PathLike, TBD 

69 

70if TYPE_CHECKING: 

71 from git.index import IndexFile 

72 from git.objects.commit import Commit 

73 from git.refs import Head, RemoteReference 

74 from git.repo import Repo 

75 

76# ----------------------------------------------------------------------------- 

77 

78_logger = logging.getLogger(__name__) 

79 

80 

81class UpdateProgress(RemoteProgress): 

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

83 derive from it and implement the 

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

85 

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

87 _num_op_codes: int = RemoteProgress._num_op_codes + 3 

88 

89 __slots__ = () 

90 

91 

92BEGIN = UpdateProgress.BEGIN 

93END = UpdateProgress.END 

94CLONE = UpdateProgress.CLONE 

95FETCH = UpdateProgress.FETCH 

96UPDWKTREE = UpdateProgress.UPDWKTREE 

97 

98 

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

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

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

102class Submodule(IndexObject, TraversableIterableObj): 

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

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

105 at the path of this instance. 

106 

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

108 solely as a marker in the tree and index. 

109 

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

111 """ 

112 

113 _id_attribute_ = "name" 

114 k_modules_file = ".gitmodules" 

115 k_head_option = "branch" 

116 k_head_default = "master" 

117 k_default_mode = stat.S_IFDIR | stat.S_IFLNK 

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

119 

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

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

122 

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

124 

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

126 

127 def __init__( 

128 self, 

129 repo: "Repo", 

130 binsha: bytes, 

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

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

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

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

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

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

137 ) -> None: 

138 """Initialize this instance with its attributes. 

139 

140 We only document the parameters that differ from 

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

142 

143 :param repo: 

144 Our parent repository. 

145 

146 :param binsha: 

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

148 See the `url` parameter. 

149 

150 :param parent_commit: 

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

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

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

154 

155 :param url: 

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

157 

158 :param branch_path: 

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

160 repository. 

161 """ 

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

163 self.size = 0 

164 self._parent_commit = parent_commit 

165 if url is not None: 

166 self._url = url 

167 if branch_path is not None: 

168 self._branch_path = branch_path 

169 if name is not None: 

170 self._name = name 

171 

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

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

174 reader: SectionConstraint = self.config_reader() 

175 # Default submodule values. 

176 try: 

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

178 except cp.NoSectionError as e: 

179 if self.repo.working_tree_dir is not None: 

180 raise ValueError( 

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

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

183 ) from e 

184 

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

186 # GitPython extension values - optional. 

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

188 elif attr == "_name": 

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

190 else: 

191 super()._set_cache_(attr) 

192 # END handle attribute name 

193 

194 @classmethod 

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

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

197 try: 

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

199 except InvalidGitRepositoryError: 

200 return IterableList("") 

201 # END handle intermediate items 

202 

203 @classmethod 

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

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

206 

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

208 """Compare with another submodule.""" 

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

210 # Otherwise this type wouldn't be hashable. 

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

212 return self._name == other._name 

213 

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

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

216 return not (self == other) 

217 

218 def __hash__(self) -> int: 

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

220 return hash(self._name) 

221 

222 def __str__(self) -> str: 

223 return self._name 

224 

225 def __repr__(self) -> str: 

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

227 type(self).__name__, 

228 self._name, 

229 self.path, 

230 self.url, 

231 self.branch_path, 

232 ) 

233 

234 @classmethod 

235 def _config_parser( 

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

237 ) -> SubmoduleConfigParser: 

238 """ 

239 :return: 

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

241 

242 :raise IOError: 

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

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

245 delayed until the first access of the config parser. 

246 """ 

247 parent_matches_head = True 

248 if parent_commit is not None: 

249 try: 

250 parent_matches_head = repo.head.commit == parent_commit 

251 except ValueError: 

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

253 # to a valid ref. 

254 pass 

255 # END handle parent_commit 

256 fp_module: Union[str, BytesIO] 

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

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

259 else: 

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

261 try: 

262 fp_module = cls._sio_modules(parent_commit) 

263 except KeyError as e: 

264 raise IOError( 

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

266 ) from e 

267 # END handle exceptions 

268 # END handle non-bare working tree 

269 

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

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

272 # END handle writes of historical submodules 

273 

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

275 

276 def _clear_cache(self) -> None: 

277 """Clear the possibly changed values.""" 

278 for name in self._cache_attrs: 

279 try: 

280 delattr(self, name) 

281 except AttributeError: 

282 pass 

283 # END try attr deletion 

284 # END for each name to delete 

285 

286 @classmethod 

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

288 """ 

289 :return: 

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

291 respective blob's data 

292 """ 

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

294 sio.name = cls.k_modules_file 

295 return sio 

296 

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

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

299 try: 

300 pc = self.parent_commit 

301 except ValueError: 

302 pc = None 

303 # END handle empty parent repository 

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

305 parser.set_submodule(self) 

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

307 

308 @classmethod 

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

310 if ( 

311 not name 

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

313 or ntpath.splitdrive(name)[0] 

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

315 ): 

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

317 return name 

318 

319 @classmethod 

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

321 name = cls._validated_name(name) 

322 if cls._need_gitfile_submodules(parent_repo.git): 

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

324 if parent_repo.working_tree_dir: 

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

326 raise NotADirectoryError() 

327 

328 @classmethod 

329 def _clone_repo( 

330 cls, 

331 repo: "Repo", 

332 url: str, 

333 path: PathLike, 

334 name: str, 

335 allow_unsafe_options: bool = False, 

336 allow_unsafe_protocols: bool = False, 

337 **kwargs: Any, 

338 ) -> "Repo": 

339 """ 

340 :return: 

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

342 

343 :param repo: 

344 Our parent repository. 

345 

346 :param url: 

347 URL to clone from. 

348 

349 :param path: 

350 Repository-relative path to the submodule checkout location. 

351 

352 :param name: 

353 Canonical name of the submodule. 

354 

355 :param allow_unsafe_protocols: 

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

357 

358 :param allow_unsafe_options: 

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

360 

361 :param kwargs: 

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

363 """ 

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

365 module_checkout_path = module_abspath 

366 if cls._need_gitfile_submodules(repo.git): 

367 if not allow_unsafe_options: 

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

369 multi_options = kwargs.get("multi_options") 

370 if multi_options: 

371 Git.check_unsafe_options( 

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

373 repo.unsafe_git_clone_options, 

374 ) 

375 allow_unsafe_options = True 

376 kwargs["separate_git_dir"] = module_abspath 

377 module_abspath_dir = osp.dirname(module_abspath) 

378 if not osp.isdir(module_abspath_dir): 

379 os.makedirs(module_abspath_dir) 

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

381 

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

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

384 repo_remote_url = repo.remote(remote_name).url 

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

386 

387 clone = git.Repo.clone_from( 

388 url, 

389 module_checkout_path, 

390 allow_unsafe_options=allow_unsafe_options, 

391 allow_unsafe_protocols=allow_unsafe_protocols, 

392 **kwargs, 

393 ) 

394 if cls._need_gitfile_submodules(repo.git): 

395 cls._write_git_file_and_module_config(module_checkout_path, module_abspath) 

396 

397 return clone 

398 

399 @classmethod 

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

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

402 

403 :raise ValueError: 

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

405 """ 

406 if parent_repo.working_tree_dir: 

407 path = _to_relative_path(parent_repo.working_tree_dir, path) 

408 else: 

409 path = to_native_path_linux(path) 

410 if path.endswith("/"): 

411 path = path[:-1] 

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

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

414 

415 return path 

416 

417 @property 

418 def abspath(self) -> PathLike: 

419 root = self.repo.working_tree_dir 

420 if root is None: 

421 return super().abspath 

422 path = root 

423 for component in os.fspath(self._to_relative_path(self.repo, self.path)).split("/"): 

424 path = join_path_native(path, component) 

425 if osp.islink(path): 

426 raise ValueError("Submodule checkout path %r contains a symbolic link" % self.path) 

427 return path 

428 

429 @classmethod 

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

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

432 git module repository. 

433 

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

435 relative to the `working_tree_dir`. 

436 

437 :note: 

438 This will overwrite existing files! 

439 

440 :note: 

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

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

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

444 one. 

445 

446 :param working_tree_dir: 

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

448 

449 :param module_abspath: 

450 Absolute path to the bare repository. 

451 """ 

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

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

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

455 os.remove(git_file) 

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

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

458 

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

460 writer.set_value( 

461 "core", 

462 "worktree", 

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

464 ) 

465 

466 # { Edit Interface 

467 

468 @classmethod 

469 def add( 

470 cls, 

471 repo: "Repo", 

472 name: str, 

473 path: PathLike, 

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

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

476 no_checkout: bool = False, 

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

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

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

480 allow_unsafe_options: bool = False, 

481 allow_unsafe_protocols: bool = False, 

482 ) -> "Submodule": 

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

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

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

486 provided, the existing submodule will be returned. 

487 

488 :param repo: 

489 Repository instance which should receive the submodule. 

490 

491 :param name: 

492 The name/identifier for the submodule. 

493 

494 :param path: 

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

496 located. 

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

498 

499 :param url: 

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

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

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

503 existing repository a submodule of another one. 

504 

505 :param branch: 

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

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

508 locally as a tracking branch. 

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

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

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

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

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

514 

515 :param no_checkout: 

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

517 will be performed. 

518 

519 :param depth: 

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

521 commits. 

522 

523 :param env: 

524 Optional dictionary containing the desired environment variables. 

525 

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

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

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

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

530 value. 

531 

532 :param clone_multi_options: 

533 A list of clone options. Please see 

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

535 

536 :param allow_unsafe_protocols: 

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

538 

539 :param allow_unsafe_options: 

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

541 

542 :return: 

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

544 

545 :note: 

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

547 repository update fails. 

548 """ 

549 if repo.bare: 

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

551 # END handle bare repos 

552 

553 cls._validated_name(name) 

554 path = cls._to_relative_path(repo, path) 

555 

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

557 if url is not None: 

558 url = to_native_path_linux(url) 

559 # END ensure URL correctness 

560 

561 # INSTANTIATE INTERMEDIATE SM 

562 sm = cls( 

563 repo, 

564 cls.NULL_BIN_SHA, 

565 cls.k_default_mode, 

566 path, 

567 name, 

568 url="invalid-temporary", 

569 ) 

570 if sm.exists(): 

571 # Reretrieve submodule from tree. 

572 try: 

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

574 sm._name = name 

575 return sm 

576 except KeyError: 

577 # Could only be in index. 

578 index = repo.index 

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

580 sm.binsha = entry.binsha 

581 return sm 

582 # END handle exceptions 

583 # END handle existing 

584 

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

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

587 has_module = sm.module_exists() 

588 branch_is_default = branch is None 

589 if has_module and url is not None: 

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

591 raise ValueError( 

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

593 ) 

594 # END check url 

595 # END verify urls match 

596 

597 mrepo: Union[Repo, None] = None 

598 

599 if url is None: 

600 if not has_module: 

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

602 # END check url 

603 mrepo = sm.module() 

604 # assert isinstance(mrepo, git.Repo) 

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

606 if not urls: 

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

608 # END verify we have url 

609 url = urls[0] 

610 else: 

611 # Clone new repo. 

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

613 if not branch_is_default: 

614 kwargs["b"] = br.name 

615 # END setup checkout-branch 

616 

617 if depth: 

618 if isinstance(depth, int): 

619 kwargs["depth"] = depth 

620 else: 

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

622 if clone_multi_options: 

623 kwargs["multi_options"] = clone_multi_options 

624 

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

626 mrepo = cls._clone_repo( 

627 repo, 

628 url, 

629 path, 

630 name, 

631 env=env, 

632 allow_unsafe_options=allow_unsafe_options, 

633 allow_unsafe_protocols=allow_unsafe_protocols, 

634 **kwargs, 

635 ) 

636 # END verify url 

637 

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

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

640 

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

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

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

644 # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one 

645 writer: Union[GitConfigParser, SectionConstraint] 

646 

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

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

649 

650 # Update configuration and index. 

651 index = sm.repo.index 

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

653 writer.set_value("url", url) 

654 writer.set_value("path", path) 

655 

656 sm._url = url 

657 if not branch_is_default: 

658 # Store full path. 

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

660 sm._branch_path = br.path 

661 

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

663 if mrepo: 

664 sm.binsha = mrepo.head.commit.binsha 

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

666 

667 return sm 

668 

669 def update( 

670 self, 

671 recursive: bool = False, 

672 init: bool = True, 

673 to_latest_revision: bool = False, 

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

675 dry_run: bool = False, 

676 force: bool = False, 

677 keep_going: bool = False, 

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

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

680 allow_unsafe_options: bool = False, 

681 allow_unsafe_protocols: bool = False, 

682 ) -> "Submodule": 

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

684 with the binsha of this instance. 

685 

686 :param recursive: 

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

688 

689 :param init: 

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

691 

692 :param to_latest_revision: 

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

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

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

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

697 for this submodule and the branch existed remotely. 

698 

699 :param progress: 

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

701 shown. 

702 

703 :param dry_run: 

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

705 All performed operations are read-only. 

706 

707 :param force: 

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

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

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

711 This will essentially 'forget' commits. 

712 

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

714 respective remote branches will simply not be moved. 

715 

716 :param keep_going: 

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

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

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

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

721 when updating submodules. 

722 

723 :param env: 

724 Optional dictionary containing the desired environment variables. 

725 

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

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

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

729 

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

731 its value. 

732 

733 :param clone_multi_options: 

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

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

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

737 

738 :param allow_unsafe_protocols: 

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

740 

741 :param allow_unsafe_options: 

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

743 

744 :note: 

745 Does nothing in bare repositories. 

746 

747 :note: 

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

749 

750 :return: 

751 self 

752 """ 

753 if self.repo.bare: 

754 return self 

755 # END pass in bare mode 

756 

757 if progress is None: 

758 progress = UpdateProgress() 

759 # END handle progress 

760 prefix = "" 

761 if dry_run: 

762 prefix = "DRY-RUN: " 

763 # END handle prefix 

764 

765 mrepo = None 

766 # END init mrepo 

767 

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

769 rmts = module_repo.remotes 

770 len_rmts = len(rmts) 

771 for i, remote in enumerate(rmts): 

772 op = FETCH 

773 if i == 0: 

774 op |= BEGIN 

775 # END handle start 

776 

777 progress.update( 

778 op, 

779 i, 

780 len_rmts, 

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

782 ) 

783 # =============================== 

784 if not dry_run: 

785 remote.fetch(progress=progress) 

786 # END handle dry-run 

787 # =============================== 

788 if i == len_rmts - 1: 

789 op |= END 

790 # END handle end 

791 progress.update( 

792 op, 

793 i, 

794 len_rmts, 

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

796 ) 

797 # END fetch new data 

798 

799 try: 

800 self._validated_name(self.name) 

801 

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

803 ####################################### 

804 try: 

805 mrepo = self.module() 

806 fetch_remotes(mrepo) 

807 except InvalidGitRepositoryError: 

808 mrepo = None 

809 if not init: 

810 return self 

811 # END early abort if init is not allowed 

812 

813 checkout_module_abspath = self.abspath 

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

815 

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

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

818 # repository instead of trying to clone over it. 

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

820 try: 

821 git.Repo(module_abspath) 

822 except InvalidGitRepositoryError: 

823 pass 

824 else: 

825 if osp.lexists(checkout_module_abspath) and ( 

826 osp.islink(checkout_module_abspath) 

827 or not osp.isdir(checkout_module_abspath) 

828 or os.listdir(checkout_module_abspath) 

829 ): 

830 raise OSError( 

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

832 ) 

833 os.makedirs(checkout_module_abspath, exist_ok=True) 

834 self._write_git_file_and_module_config(checkout_module_abspath, module_abspath) 

835 mrepo = git.Repo(checkout_module_abspath) 

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

837 fetch_remotes(mrepo) 

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

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

840 

841 if mrepo is None: 

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

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

844 try: 

845 os.rmdir(checkout_module_abspath) 

846 except OSError as e: 

847 raise OSError( 

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

849 ) from e 

850 # END handle directory removal 

851 

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

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

854 progress.update( 

855 BEGIN | CLONE, 

856 0, 

857 1, 

858 prefix 

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

860 ) 

861 if not dry_run: 

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

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

864 else: 

865 url = self.url 

866 mrepo = self._clone_repo( 

867 self.repo, 

868 url, 

869 self.path, 

870 self.name, 

871 n=True, 

872 env=env, 

873 multi_options=clone_multi_options, 

874 allow_unsafe_options=allow_unsafe_options, 

875 allow_unsafe_protocols=allow_unsafe_protocols, 

876 ) 

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

878 

879 if not dry_run: 

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

881 try: 

882 mrepo = cast("Repo", mrepo) 

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

884 local_branch = mkhead(mrepo, self.branch_path) 

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

886 mrepo.head.set_reference( 

887 local_branch, 

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

889 ) 

890 mrepo.head.reference.set_tracking_branch(remote_branch) 

891 except (IndexError, InvalidGitRepositoryError): 

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

893 

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

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

896 # END handle initialization 

897 

898 # DETERMINE SHAS TO CHECK OUT 

899 ############################# 

900 binsha = self.binsha 

901 hexsha = self.hexsha 

902 is_detached = False 

903 if mrepo is not None: 

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

905 # existed. 

906 is_detached = mrepo.head.is_detached 

907 # END handle dry_run 

908 

909 if mrepo is not None and to_latest_revision: 

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

911 if not is_detached: 

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

913 if rref is not None: 

914 rcommit = rref.commit 

915 binsha = rcommit.binsha 

916 hexsha = rcommit.hexsha 

917 else: 

918 _logger.error( 

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

920 msg_base, 

921 mrepo.head.reference, 

922 ) 

923 # END handle remote ref 

924 else: 

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

926 # END handle detached head 

927 # END handle to_latest_revision option 

928 

929 # Update the working tree. 

930 # Handles dry_run. 

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

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

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

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

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

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

937 # changes the user had done. 

938 may_reset = True 

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

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

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

942 if force: 

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

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

945 _logger.debug(msg) 

946 else: 

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

948 msg %= ( 

949 is_detached and "checkout" or "reset", 

950 mrepo.head, 

951 mrepo, 

952 ) 

953 _logger.info(msg) 

954 may_reset = False 

955 # END handle force 

956 # END handle if we are in the future 

957 

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

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

960 # END handle force and dirty state 

961 # END handle empty repo 

962 

963 # END verify future/past 

964 progress.update( 

965 BEGIN | UPDWKTREE, 

966 0, 

967 1, 

968 prefix 

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

970 ) 

971 

972 if not dry_run and may_reset: 

973 if is_detached: 

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

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

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

977 # for future options regarding rebase and merge. 

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

979 else: 

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

981 # END handle checkout 

982 # If we may reset/checkout. 

983 progress.update( 

984 END | UPDWKTREE, 

985 0, 

986 1, 

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

988 ) 

989 # END update to new commit only if needed 

990 except Exception as err: 

991 if not keep_going: 

992 raise 

993 _logger.error(str(err)) 

994 # END handle keep_going 

995 

996 # HANDLE RECURSION 

997 ################## 

998 if recursive: 

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

1000 if mrepo is not None: 

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

1002 submodule.update( 

1003 recursive, 

1004 init, 

1005 to_latest_revision, 

1006 progress=progress, 

1007 dry_run=dry_run, 

1008 force=force, 

1009 keep_going=keep_going, 

1010 ) 

1011 # END handle recursive update 

1012 # END handle dry run 

1013 # END for each submodule 

1014 

1015 return self 

1016 

1017 @unbare_repo 

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

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

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

1021 adjusting our index entry accordingly. 

1022 

1023 :param module_path: 

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

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

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

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

1028 

1029 :param configuration: 

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

1031 to the given path. 

1032 

1033 :param module: 

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

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

1036 parent repository in an inconsistent state. 

1037 

1038 :return: 

1039 self 

1040 

1041 :raise ValueError: 

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

1043 

1044 :note: 

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

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

1047 """ 

1048 if module + configuration < 1: 

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

1050 # END handle input 

1051 

1052 self._validated_name(self.name) 

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

1054 

1055 # VERIFY DESTINATION 

1056 if module_checkout_path == self.path: 

1057 return self 

1058 # END handle no change 

1059 

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

1061 if osp.isfile(module_checkout_abspath): 

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

1063 # END handle target files 

1064 

1065 index = self.repo.index 

1066 tekey = index.entry_key(module_checkout_path, 0) 

1067 # if the target item already exists, fail 

1068 if configuration and tekey in index.entries: 

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

1070 # END handle index key already there 

1071 

1072 # Remove existing destination. 

1073 if module: 

1074 if osp.exists(module_checkout_abspath): 

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

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

1077 # END handle non-emptiness 

1078 

1079 if osp.islink(module_checkout_abspath): 

1080 os.remove(module_checkout_abspath) 

1081 else: 

1082 os.rmdir(module_checkout_abspath) 

1083 # END handle link 

1084 else: 

1085 # Recreate parent directories. 

1086 # NOTE: renames() does that now. 

1087 pass 

1088 # END handle existence 

1089 # END handle module 

1090 

1091 # Move the module into place if possible. 

1092 cur_path = self.abspath 

1093 renamed_module = False 

1094 if module and osp.exists(cur_path): 

1095 os.renames(cur_path, module_checkout_abspath) 

1096 renamed_module = True 

1097 

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

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

1100 self._write_git_file_and_module_config(module_checkout_abspath, module_abspath) 

1101 # END handle git file rewrite 

1102 # END move physical module 

1103 

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

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

1106 previous_sm_path = self.path 

1107 try: 

1108 if configuration: 

1109 try: 

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

1111 entry = index.entries[ekey] 

1112 del index.entries[ekey] 

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

1114 index.entries[tekey] = nentry 

1115 except KeyError as e: 

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

1117 # END handle submodule doesn't exist 

1118 

1119 # Update configuration. 

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

1121 writer.set_value("path", module_checkout_path) 

1122 self.path = module_checkout_path 

1123 # END handle configuration flag 

1124 except Exception: 

1125 if renamed_module: 

1126 os.renames(module_checkout_abspath, cur_path) 

1127 # END undo module renaming 

1128 raise 

1129 # END handle undo rename 

1130 

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

1132 # directory. 

1133 if previous_sm_path == self.name: 

1134 self.rename(module_checkout_path) 

1135 

1136 return self 

1137 

1138 @unbare_repo 

1139 def remove( 

1140 self, 

1141 module: bool = True, 

1142 force: bool = False, 

1143 configuration: bool = True, 

1144 dry_run: bool = False, 

1145 ) -> "Submodule": 

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

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

1148 

1149 :param module: 

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

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

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

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

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

1155 have been altered. 

1156 

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

1158 to touching the direct submodule. 

1159 

1160 :param force: 

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

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

1163 

1164 :param configuration: 

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

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

1167 you to safely delete the repository of your submodule. 

1168 

1169 :param dry_run: 

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

1171 usually throw. 

1172 

1173 :return: 

1174 self 

1175 

1176 :note: 

1177 Doesn't work in bare repositories. 

1178 

1179 :note: 

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

1181 leave an inconsistent state. 

1182 

1183 :raise git.exc.InvalidGitRepositoryError: 

1184 Thrown if the repository cannot be deleted. 

1185 

1186 :raise OSError: 

1187 If directories or files could not be removed. 

1188 """ 

1189 if not (module or configuration): 

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

1191 # END handle parameters 

1192 

1193 self._validated_name(self.name) 

1194 # Recursively remove children of this submodule. 

1195 nc = 0 

1196 for csm in self.children(): 

1197 nc += 1 

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

1199 del csm 

1200 

1201 if configuration and not dry_run and nc > 0: 

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

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

1204 # expected. 

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

1206 # END handle recursion 

1207 

1208 # DELETE REPOSITORY WORKING TREE 

1209 ################################ 

1210 if module and self.module_exists(): 

1211 mod = self.module() 

1212 git_dir = mod.git_dir 

1213 if force: 

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

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

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

1217 # submodules first. 

1218 mp = self.abspath 

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

1220 if osp.islink(mp): 

1221 method = os.remove 

1222 elif osp.isdir(mp): 

1223 method = rmtree 

1224 elif osp.exists(mp): 

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

1226 # END handle brutal deletion 

1227 if not dry_run: 

1228 assert method 

1229 method(mp) 

1230 # END apply deletion method 

1231 else: 

1232 # Verify we may delete our module. 

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

1234 raise InvalidGitRepositoryError( 

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

1236 % mod.working_tree_dir 

1237 ) 

1238 # END check for dirt 

1239 

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

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

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

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

1244 for remote in mod.remotes: 

1245 num_branches_with_new_commits = 0 

1246 rrefs = remote.refs 

1247 rref = None 

1248 for rref in rrefs: 

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

1250 # END for each remote ref 

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

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

1253 raise InvalidGitRepositoryError( 

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

1255 ) 

1256 # END handle new commits 

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

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

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

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

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

1262 if len(rrefs): 

1263 del rref # skipcq: PYL-W0631 

1264 # END handle remotes 

1265 del rrefs 

1266 del remote 

1267 # END for each remote 

1268 

1269 # Finally delete our own submodule. 

1270 if not dry_run: 

1271 self._clear_cache() 

1272 wtd = mod.working_tree_dir 

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

1274 gc.collect() 

1275 rmtree(str(wtd)) 

1276 # END delete tree if possible 

1277 # END handle force 

1278 

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

1280 self._clear_cache() 

1281 rmtree(git_dir) 

1282 # END handle separate bare repository 

1283 # END handle module deletion 

1284 

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

1286 if not dry_run: 

1287 self._clear_cache() 

1288 

1289 # DELETE CONFIGURATION 

1290 ###################### 

1291 if configuration and not dry_run: 

1292 # First the index-entry. 

1293 parent_index = self.repo.index 

1294 try: 

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

1296 except KeyError: 

1297 pass 

1298 # END delete entry 

1299 parent_index.write() 

1300 

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

1302 # information anymore. 

1303 

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

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

1306 

1307 with self.config_writer() as sc_writer: 

1308 sc_writer.remove_section() 

1309 # END delete configuration 

1310 

1311 return self 

1312 

1313 @unbare_repo 

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

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

1316 

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

1318 which unregisters the submodule (removes its entry from 

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

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

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

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

1323 submodule from the retained contents. 

1324 

1325 :param force: 

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

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

1328 local modifications that would otherwise block the command. 

1329 

1330 :return: 

1331 self 

1332 

1333 :note: 

1334 Doesn't work in bare repositories. 

1335 """ 

1336 args: List[str] = [] 

1337 if force: 

1338 args.append("--force") 

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

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

1341 return self 

1342 

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

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

1345 contain the ``.gitmodules`` blob. 

1346 

1347 :param commit: 

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

1349 to the most recent commit. 

1350 

1351 :param check: 

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

1353 validity of the submodule. 

1354 

1355 :raise ValueError: 

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

1357 

1358 :raise ValueError: 

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

1360 

1361 :return: 

1362 self 

1363 """ 

1364 if commit is None: 

1365 self._parent_commit = None 

1366 return self 

1367 # END handle None 

1368 pcommit = self.repo.commit(commit) 

1369 pctree = pcommit.tree 

1370 if self.k_modules_file not in pctree: 

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

1372 # END handle exceptions 

1373 

1374 prev_pc = self._parent_commit 

1375 self._parent_commit = pcommit 

1376 

1377 if check: 

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

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

1380 self._parent_commit = prev_pc 

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

1382 # END handle submodule did not exist 

1383 # END handle checking mode 

1384 

1385 # Update our sha, it could have changed. 

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

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

1388 try: 

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

1390 except KeyError: 

1391 self.binsha = self.NULL_BIN_SHA 

1392 

1393 self._clear_cache() 

1394 return self 

1395 

1396 @unbare_repo 

1397 def config_writer( 

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

1399 ) -> SectionConstraint["SubmoduleConfigParser"]: 

1400 """ 

1401 :return: 

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

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

1404 

1405 :param index: 

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

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

1408 repository. 

1409 

1410 :param write: 

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

1412 

1413 :note: 

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

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

1416 yourself once the whole operation is complete. 

1417 

1418 :raise ValueError: 

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

1420 current head commit. 

1421 

1422 :raise IOError: 

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

1424 """ 

1425 writer = self._config_parser_constrained(read_only=False) 

1426 if index is not None: 

1427 writer.config._index = index 

1428 writer.config._auto_write = write 

1429 return writer 

1430 

1431 @unbare_repo 

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

1433 """Rename this submodule. 

1434 

1435 :note: 

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

1437 

1438 * ``$parent_git_dir / config`` 

1439 * ``$working_tree_dir / .gitmodules`` 

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

1441 

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

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

1444 

1445 :return: 

1446 This :class:`Submodule` instance 

1447 """ 

1448 if self.name == new_name: 

1449 return self 

1450 

1451 self._validated_name(self.name) 

1452 self._validated_name(new_name) 

1453 

1454 # .git/config 

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

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

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

1458 # entries. 

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

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

1461 

1462 # .gitmodules 

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

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

1465 

1466 self._name = new_name 

1467 

1468 # .git/modules 

1469 mod = self.module() 

1470 if mod.has_separate_working_tree(): 

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

1472 source_dir = mod.git_dir 

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

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

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

1476 os.renames(source_dir, tmp_dir) 

1477 source_dir = tmp_dir 

1478 # END handle self-containment 

1479 os.renames(source_dir, destination_module_abspath) 

1480 if mod.working_tree_dir: 

1481 self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath) 

1482 # END move separate git repository 

1483 

1484 return self 

1485 

1486 # } END edit interface 

1487 

1488 # { Query Interface 

1489 

1490 @unbare_repo 

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

1492 """ 

1493 :return: 

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

1495 submodule path 

1496 

1497 :raise git.exc.InvalidGitRepositoryError: 

1498 If a repository was not available. 

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

1500 """ 

1501 self._validated_name(self.name) 

1502 module_checkout_abspath = self.abspath 

1503 try: 

1504 repo = git.Repo(module_checkout_abspath) 

1505 if repo != self.repo: 

1506 return repo 

1507 # END handle repo uninitialized 

1508 except (InvalidGitRepositoryError, NoSuchPathError) as e: 

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

1510 else: 

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

1512 # END handle exceptions 

1513 

1514 def module_exists(self) -> bool: 

1515 """ 

1516 :return: 

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

1518 See the :meth:`module` method. 

1519 """ 

1520 try: 

1521 self.module() 

1522 return True 

1523 except Exception: 

1524 return False 

1525 # END handle exception 

1526 

1527 def exists(self) -> bool: 

1528 """ 

1529 :return: 

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

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

1532 though its module doesn't exist on disk. 

1533 """ 

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

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

1536 loc = locals() 

1537 for attr in self._cache_attrs: 

1538 try: 

1539 if hasattr(self, attr): 

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

1541 # END if we have the attribute cache 

1542 except (cp.NoSectionError, ValueError): 

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

1544 # happen on PY2. 

1545 pass 

1546 # END for each attr 

1547 self._clear_cache() 

1548 

1549 try: 

1550 try: 

1551 self.path # noqa: B018 

1552 return True 

1553 except Exception: 

1554 return False 

1555 # END handle exceptions 

1556 finally: 

1557 for attr in self._cache_attrs: 

1558 if attr in loc: 

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

1560 # END if we have a cache 

1561 # END reapply each attribute 

1562 # END handle object state consistency 

1563 

1564 @property 

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

1566 """ 

1567 :return: 

1568 The branch instance that we are to checkout 

1569 

1570 :raise git.exc.InvalidGitRepositoryError: 

1571 If our module is not yet checked out. 

1572 """ 

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

1574 

1575 @property 

1576 def branch_path(self) -> PathLike: 

1577 """ 

1578 :return: 

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

1580 the remote and track 

1581 """ 

1582 return self._branch_path 

1583 

1584 @property 

1585 def branch_name(self) -> str: 

1586 """ 

1587 :return: 

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

1589 """ 

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

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

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

1593 

1594 @property 

1595 def url(self) -> str: 

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

1597 return self._url 

1598 

1599 @property 

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

1601 """ 

1602 :return: 

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

1604 ``.gitmodules`` file 

1605 

1606 :note: 

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

1608 """ 

1609 if self._parent_commit is None: 

1610 return self.repo.commit() 

1611 return self._parent_commit 

1612 

1613 @property 

1614 def name(self) -> str: 

1615 """ 

1616 :return: 

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

1618 ``.gitmodules`` file. 

1619 

1620 :note: 

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

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

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

1624 """ 

1625 return self._name 

1626 

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

1628 """ 

1629 :return: 

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

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

1632 

1633 :note: 

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

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

1636 

1637 :note: 

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

1639 

1640 :raise IOError: 

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

1642 """ 

1643 return self._config_parser_constrained(read_only=True) 

1644 

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

1646 """ 

1647 :return: 

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

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

1650 checked out. 

1651 """ 

1652 return self._get_intermediate_items(self) 

1653 

1654 # } END query interface 

1655 

1656 # { Iterable Interface 

1657 

1658 @classmethod 

1659 def iter_items( 

1660 cls, 

1661 repo: "Repo", 

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

1663 *args: Any, 

1664 **kwargs: Any, 

1665 ) -> Iterator["Submodule"]: 

1666 """ 

1667 :return: 

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

1669 repository 

1670 """ 

1671 try: 

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

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

1674 except (IOError, BadName): 

1675 return 

1676 # END handle empty iterator 

1677 

1678 for sms in parser.sections(): 

1679 n = sm_name(sms) 

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

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

1682 b = cls.k_head_default 

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

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

1685 # END handle optional information 

1686 

1687 # Get the binsha. 

1688 index = repo.index 

1689 try: 

1690 rt = pc.tree # Root tree 

1691 sm = rt[p] 

1692 except KeyError: 

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

1694 try: 

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

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

1697 except KeyError: 

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

1699 # .gitmodules file. 

1700 continue 

1701 # END handle keyerror 

1702 # END handle critical error 

1703 

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

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

1706 continue 

1707 

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

1709 sm._name = n 

1710 if pc != repo.commit(): 

1711 sm._parent_commit = pc 

1712 # END set only if not most recent! 

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

1714 sm._url = u 

1715 

1716 yield sm 

1717 # END for each section 

1718 

1719 # } END iterable interface