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

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

583 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 

14 

15import git 

16from git.cmd import Git 

17from git.compat import defenc 

18from git.config import GitConfigParser, SectionConstraint, cp 

19from git.exc import ( 

20 BadName, 

21 InvalidGitRepositoryError, 

22 NoSuchPathError, 

23 RepositoryDirtyError, 

24) 

25from git.objects.base import IndexObject, Object 

26from git.objects.util import TraversableIterableObj 

27from git.util import ( 

28 IterableList, 

29 RemoteProgress, 

30 join_path_native, 

31 rmtree, 

32 to_native_path_linux, 

33 unbare_repo, 

34) 

35 

36from .util import ( 

37 SubmoduleConfigParser, 

38 find_first_remote_branch, 

39 mkhead, 

40 sm_name, 

41 sm_section, 

42) 

43 

44# typing ---------------------------------------------------------------------- 

45 

46from typing import ( 

47 Any, 

48 Callable, 

49 Dict, 

50 Iterator, 

51 Mapping, 

52 Sequence, 

53 TYPE_CHECKING, 

54 Union, 

55 cast, 

56) 

57 

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

59 from typing import Literal 

60else: 

61 from typing_extensions import Literal 

62 

63from git.types import Commit_ish, PathLike, TBD 

64 

65if TYPE_CHECKING: 

66 from git.index import IndexFile 

67 from git.objects.commit import Commit 

68 from git.refs import Head 

69 from git.repo import Repo 

70 

71# ----------------------------------------------------------------------------- 

72 

73_logger = logging.getLogger(__name__) 

74 

75 

76class UpdateProgress(RemoteProgress): 

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

78 derive from it and implement the 

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

80 

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

82 _num_op_codes: int = RemoteProgress._num_op_codes + 3 

83 

84 __slots__ = () 

85 

86 

87BEGIN = UpdateProgress.BEGIN 

88END = UpdateProgress.END 

89CLONE = UpdateProgress.CLONE 

90FETCH = UpdateProgress.FETCH 

91UPDWKTREE = UpdateProgress.UPDWKTREE 

92 

93 

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

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

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

97class Submodule(IndexObject, TraversableIterableObj): 

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

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

100 at the path of this instance. 

101 

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

103 solely as a marker in the tree and index. 

104 

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

106 """ 

107 

108 _id_attribute_ = "name" 

109 k_modules_file = ".gitmodules" 

110 k_head_option = "branch" 

111 k_head_default = "master" 

112 k_default_mode = stat.S_IFDIR | stat.S_IFLNK 

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

114 

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

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

117 

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

119 

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

121 

122 def __init__( 

123 self, 

124 repo: "Repo", 

125 binsha: bytes, 

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

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

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

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

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

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

132 ) -> None: 

133 """Initialize this instance with its attributes. 

134 

135 We only document the parameters that differ from 

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

137 

138 :param repo: 

139 Our parent repository. 

140 

141 :param binsha: 

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

143 See the `url` parameter. 

144 

145 :param parent_commit: 

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

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

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

149 

150 :param url: 

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

152 

153 :param branch_path: 

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

155 repository. 

156 """ 

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

158 self.size = 0 

159 self._parent_commit = parent_commit 

160 if url is not None: 

161 self._url = url 

162 if branch_path is not None: 

163 self._branch_path = branch_path 

164 if name is not None: 

165 self._name = name 

166 

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

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

169 reader: SectionConstraint = self.config_reader() 

170 # Default submodule values. 

171 try: 

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

173 except cp.NoSectionError as e: 

174 if self.repo.working_tree_dir is not None: 

175 raise ValueError( 

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

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

178 ) from e 

179 

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

181 # GitPython extension values - optional. 

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

183 elif attr == "_name": 

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

185 else: 

186 super()._set_cache_(attr) 

187 # END handle attribute name 

188 

189 @classmethod 

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

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

192 try: 

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

194 except InvalidGitRepositoryError: 

195 return IterableList("") 

196 # END handle intermediate items 

197 

198 @classmethod 

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

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

201 

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

203 """Compare with another submodule.""" 

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

205 # Otherwise this type wouldn't be hashable. 

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

207 return self._name == other._name 

208 

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

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

211 return not (self == other) 

212 

213 def __hash__(self) -> int: 

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

215 return hash(self._name) 

216 

217 def __str__(self) -> str: 

218 return self._name 

219 

220 def __repr__(self) -> str: 

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

222 type(self).__name__, 

223 self._name, 

224 self.path, 

225 self.url, 

226 self.branch_path, 

227 ) 

228 

229 @classmethod 

230 def _config_parser( 

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

232 ) -> SubmoduleConfigParser: 

233 """ 

234 :return: 

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

236 

237 :raise IOError: 

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

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

240 delayed until the first access of the config parser. 

241 """ 

242 parent_matches_head = True 

243 if parent_commit is not None: 

244 try: 

245 parent_matches_head = repo.head.commit == parent_commit 

246 except ValueError: 

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

248 # to a valid ref. 

249 pass 

250 # END handle parent_commit 

251 fp_module: Union[str, BytesIO] 

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

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

254 else: 

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

256 try: 

257 fp_module = cls._sio_modules(parent_commit) 

258 except KeyError as e: 

259 raise IOError( 

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

261 ) from e 

262 # END handle exceptions 

263 # END handle non-bare working tree 

264 

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

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

267 # END handle writes of historical submodules 

268 

269 return SubmoduleConfigParser(fp_module, read_only=read_only) 

270 

271 def _clear_cache(self) -> None: 

272 """Clear the possibly changed values.""" 

273 for name in self._cache_attrs: 

274 try: 

275 delattr(self, name) 

276 except AttributeError: 

277 pass 

278 # END try attr deletion 

279 # END for each name to delete 

280 

281 @classmethod 

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

283 """ 

284 :return: 

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

286 respective blob's data 

287 """ 

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

289 sio.name = cls.k_modules_file 

290 return sio 

291 

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

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

294 try: 

295 pc = self.parent_commit 

296 except ValueError: 

297 pc = None 

298 # END handle empty parent repository 

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

300 parser.set_submodule(self) 

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

302 

303 @classmethod 

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

305 if cls._need_gitfile_submodules(parent_repo.git): 

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

307 if parent_repo.working_tree_dir: 

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

309 raise NotADirectoryError() 

310 

311 @classmethod 

312 def _clone_repo( 

313 cls, 

314 repo: "Repo", 

315 url: str, 

316 path: PathLike, 

317 name: str, 

318 allow_unsafe_options: bool = False, 

319 allow_unsafe_protocols: bool = False, 

320 **kwargs: Any, 

321 ) -> "Repo": 

322 """ 

323 :return: 

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

325 

326 :param repo: 

327 Our parent repository. 

328 

329 :param url: 

330 URL to clone from. 

331 

332 :param path: 

333 Repository-relative path to the submodule checkout location. 

334 

335 :param name: 

336 Canonical name of the submodule. 

337 

338 :param allow_unsafe_protocols: 

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

340 

341 :param allow_unsafe_options: 

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

343 

344 :param kwargs: 

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

346 """ 

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

348 module_checkout_path = module_abspath 

349 if cls._need_gitfile_submodules(repo.git): 

350 kwargs["separate_git_dir"] = module_abspath 

351 module_abspath_dir = osp.dirname(module_abspath) 

352 if not osp.isdir(module_abspath_dir): 

353 os.makedirs(module_abspath_dir) 

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

355 

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

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

358 repo_remote_url = repo.remote(remote_name).url 

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

360 

361 clone = git.Repo.clone_from( 

362 url, 

363 module_checkout_path, 

364 allow_unsafe_options=allow_unsafe_options, 

365 allow_unsafe_protocols=allow_unsafe_protocols, 

366 **kwargs, 

367 ) 

368 if cls._need_gitfile_submodules(repo.git): 

369 cls._write_git_file_and_module_config(module_checkout_path, module_abspath) 

370 

371 return clone 

372 

373 @classmethod 

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

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

376 

377 :raise ValueError: 

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

379 """ 

380 path = to_native_path_linux(path) 

381 if path.endswith("/"): 

382 path = path[:-1] 

383 # END handle trailing slash 

384 

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

386 working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir) 

387 if not path.startswith(working_tree_linux): 

388 raise ValueError( 

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

390 % (working_tree_linux, path) 

391 ) 

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

393 if not path: 

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

395 # END verify converted relative path makes sense 

396 # END convert to a relative path 

397 

398 return path 

399 

400 @classmethod 

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

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

403 git module repository. 

404 

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

406 relative to the `working_tree_dir`. 

407 

408 :note: 

409 This will overwrite existing files! 

410 

411 :note: 

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

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

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

415 one. 

416 

417 :param working_tree_dir: 

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

419 

420 :param module_abspath: 

421 Absolute path to the bare repository. 

422 """ 

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

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

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

426 os.remove(git_file) 

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

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

429 

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

431 writer.set_value( 

432 "core", 

433 "worktree", 

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

435 ) 

436 

437 # { Edit Interface 

438 

439 @classmethod 

440 def add( 

441 cls, 

442 repo: "Repo", 

443 name: str, 

444 path: PathLike, 

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

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

447 no_checkout: bool = False, 

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

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

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

451 allow_unsafe_options: bool = False, 

452 allow_unsafe_protocols: bool = False, 

453 ) -> "Submodule": 

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

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

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

457 provided, the existing submodule will be returned. 

458 

459 :param repo: 

460 Repository instance which should receive the submodule. 

461 

462 :param name: 

463 The name/identifier for the submodule. 

464 

465 :param path: 

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

467 located. 

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

469 

470 :param url: 

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

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

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

474 existing repository a submodule of another one. 

475 

476 :param branch: 

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

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

479 locally as a tracking branch. 

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

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

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

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

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

485 

486 :param no_checkout: 

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

488 will be performed. 

489 

490 :param depth: 

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

492 commits. 

493 

494 :param env: 

495 Optional dictionary containing the desired environment variables. 

496 

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

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

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

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

501 value. 

502 

503 :param clone_multi_options: 

504 A list of clone options. Please see 

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

506 

507 :param allow_unsafe_protocols: 

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

509 

510 :param allow_unsafe_options: 

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

512 

513 :return: 

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

515 

516 :note: 

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

518 repository update fails. 

519 """ 

520 if repo.bare: 

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

522 # END handle bare repos 

523 

524 path = cls._to_relative_path(repo, path) 

525 

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

527 if url is not None: 

528 url = to_native_path_linux(url) 

529 # END ensure URL correctness 

530 

531 # INSTANTIATE INTERMEDIATE SM 

532 sm = cls( 

533 repo, 

534 cls.NULL_BIN_SHA, 

535 cls.k_default_mode, 

536 path, 

537 name, 

538 url="invalid-temporary", 

539 ) 

540 if sm.exists(): 

541 # Reretrieve submodule from tree. 

542 try: 

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

544 sm._name = name 

545 return sm 

546 except KeyError: 

547 # Could only be in index. 

548 index = repo.index 

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

550 sm.binsha = entry.binsha 

551 return sm 

552 # END handle exceptions 

553 # END handle existing 

554 

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

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

557 has_module = sm.module_exists() 

558 branch_is_default = branch is None 

559 if has_module and url is not None: 

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

561 raise ValueError( 

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

563 ) 

564 # END check url 

565 # END verify urls match 

566 

567 mrepo: Union[Repo, None] = None 

568 

569 if url is None: 

570 if not has_module: 

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

572 # END check url 

573 mrepo = sm.module() 

574 # assert isinstance(mrepo, git.Repo) 

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

576 if not urls: 

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

578 # END verify we have url 

579 url = urls[0] 

580 else: 

581 # Clone new repo. 

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

583 if not branch_is_default: 

584 kwargs["b"] = br.name 

585 # END setup checkout-branch 

586 

587 if depth: 

588 if isinstance(depth, int): 

589 kwargs["depth"] = depth 

590 else: 

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

592 if clone_multi_options: 

593 kwargs["multi_options"] = clone_multi_options 

594 

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

596 mrepo = cls._clone_repo( 

597 repo, 

598 url, 

599 path, 

600 name, 

601 env=env, 

602 allow_unsafe_options=allow_unsafe_options, 

603 allow_unsafe_protocols=allow_unsafe_protocols, 

604 **kwargs, 

605 ) 

606 # END verify url 

607 

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

609 url = Git.polish_url(url) 

610 

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

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

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

614 # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one 

615 writer: Union[GitConfigParser, SectionConstraint] 

616 

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

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

619 

620 # Update configuration and index. 

621 index = sm.repo.index 

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

623 writer.set_value("url", url) 

624 writer.set_value("path", path) 

625 

626 sm._url = url 

627 if not branch_is_default: 

628 # Store full path. 

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

630 sm._branch_path = br.path 

631 

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

633 if mrepo: 

634 sm.binsha = mrepo.head.commit.binsha 

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

636 

637 return sm 

638 

639 def update( 

640 self, 

641 recursive: bool = False, 

642 init: bool = True, 

643 to_latest_revision: bool = False, 

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

645 dry_run: bool = False, 

646 force: bool = False, 

647 keep_going: bool = False, 

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

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

650 allow_unsafe_options: bool = False, 

651 allow_unsafe_protocols: bool = False, 

652 ) -> "Submodule": 

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

654 with the binsha of this instance. 

655 

656 :param recursive: 

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

658 

659 :param init: 

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

661 

662 :param to_latest_revision: 

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

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

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

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

667 for this submodule and the branch existed remotely. 

668 

669 :param progress: 

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

671 shown. 

672 

673 :param dry_run: 

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

675 All performed operations are read-only. 

676 

677 :param force: 

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

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

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

681 This will essentially 'forget' commits. 

682 

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

684 respective remote branches will simply not be moved. 

685 

686 :param keep_going: 

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

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

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

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

691 when updating submodules. 

692 

693 :param env: 

694 Optional dictionary containing the desired environment variables. 

695 

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

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

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

699 

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

701 its value. 

702 

703 :param clone_multi_options: 

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

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

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

707 

708 :param allow_unsafe_protocols: 

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

710 

711 :param allow_unsafe_options: 

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

713 

714 :note: 

715 Does nothing in bare repositories. 

716 

717 :note: 

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

719 

720 :return: 

721 self 

722 """ 

723 if self.repo.bare: 

724 return self 

725 # END pass in bare mode 

726 

727 if progress is None: 

728 progress = UpdateProgress() 

729 # END handle progress 

730 prefix = "" 

731 if dry_run: 

732 prefix = "DRY-RUN: " 

733 # END handle prefix 

734 

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

736 if dry_run: 

737 mrepo = None 

738 # END init mrepo 

739 

740 try: 

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

742 ####################################### 

743 try: 

744 mrepo = self.module() 

745 rmts = mrepo.remotes 

746 len_rmts = len(rmts) 

747 for i, remote in enumerate(rmts): 

748 op = FETCH 

749 if i == 0: 

750 op |= BEGIN 

751 # END handle start 

752 

753 progress.update( 

754 op, 

755 i, 

756 len_rmts, 

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

758 ) 

759 # =============================== 

760 if not dry_run: 

761 remote.fetch(progress=progress) 

762 # END handle dry-run 

763 # =============================== 

764 if i == len_rmts - 1: 

765 op |= END 

766 # END handle end 

767 progress.update( 

768 op, 

769 i, 

770 len_rmts, 

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

772 ) 

773 # END fetch new data 

774 except InvalidGitRepositoryError: 

775 mrepo = None 

776 if not init: 

777 return self 

778 # END early abort if init is not allowed 

779 

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

781 checkout_module_abspath = self.abspath 

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

783 try: 

784 os.rmdir(checkout_module_abspath) 

785 except OSError as e: 

786 raise OSError( 

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

788 ) from e 

789 # END handle OSError 

790 # END handle directory removal 

791 

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

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

794 progress.update( 

795 BEGIN | CLONE, 

796 0, 

797 1, 

798 prefix 

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

800 ) 

801 if not dry_run: 

802 mrepo = self._clone_repo( 

803 self.repo, 

804 self.url, 

805 self.path, 

806 self.name, 

807 n=True, 

808 env=env, 

809 multi_options=clone_multi_options, 

810 allow_unsafe_options=allow_unsafe_options, 

811 allow_unsafe_protocols=allow_unsafe_protocols, 

812 ) 

813 # END handle dry-run 

814 progress.update( 

815 END | CLONE, 

816 0, 

817 1, 

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

819 ) 

820 

821 if not dry_run: 

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

823 try: 

824 mrepo = cast("Repo", mrepo) 

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

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

827 local_branch = mkhead(mrepo, self.branch_path) 

828 

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

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

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

832 # END initial checkout + branch creation 

833 

834 # Make sure HEAD is not detached. 

835 mrepo.head.set_reference( 

836 local_branch, 

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

838 ) 

839 mrepo.head.reference.set_tracking_branch(remote_branch) 

840 except (IndexError, InvalidGitRepositoryError): 

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

842 # END handle tracking branch 

843 

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

845 # default implementation will be offended and not update the 

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

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

848 # redundant! 

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

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

851 # END handle dry_run 

852 # END handle initialization 

853 

854 # DETERMINE SHAS TO CHECK OUT 

855 ############################# 

856 binsha = self.binsha 

857 hexsha = self.hexsha 

858 if mrepo is not None: 

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

860 # existed. 

861 is_detached = mrepo.head.is_detached 

862 # END handle dry_run 

863 

864 if mrepo is not None and to_latest_revision: 

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

866 if not is_detached: 

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

868 if rref is not None: 

869 rcommit = rref.commit 

870 binsha = rcommit.binsha 

871 hexsha = rcommit.hexsha 

872 else: 

873 _logger.error( 

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

875 msg_base, 

876 mrepo.head.reference, 

877 ) 

878 # END handle remote ref 

879 else: 

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

881 # END handle detached head 

882 # END handle to_latest_revision option 

883 

884 # Update the working tree. 

885 # Handles dry_run. 

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

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

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

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

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

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

892 # changes the user had done. 

893 may_reset = True 

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

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

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

897 if force: 

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

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

900 _logger.debug(msg) 

901 else: 

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

903 msg %= ( 

904 is_detached and "checkout" or "reset", 

905 mrepo.head, 

906 mrepo, 

907 ) 

908 _logger.info(msg) 

909 may_reset = False 

910 # END handle force 

911 # END handle if we are in the future 

912 

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

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

915 # END handle force and dirty state 

916 # END handle empty repo 

917 

918 # END verify future/past 

919 progress.update( 

920 BEGIN | UPDWKTREE, 

921 0, 

922 1, 

923 prefix 

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

925 ) 

926 

927 if not dry_run and may_reset: 

928 if is_detached: 

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

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

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

932 # for future options regarding rebase and merge. 

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

934 else: 

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

936 # END handle checkout 

937 # If we may reset/checkout. 

938 progress.update( 

939 END | UPDWKTREE, 

940 0, 

941 1, 

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

943 ) 

944 # END update to new commit only if needed 

945 except Exception as err: 

946 if not keep_going: 

947 raise 

948 _logger.error(str(err)) 

949 # END handle keep_going 

950 

951 # HANDLE RECURSION 

952 ################## 

953 if recursive: 

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

955 if mrepo is not None: 

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

957 submodule.update( 

958 recursive, 

959 init, 

960 to_latest_revision, 

961 progress=progress, 

962 dry_run=dry_run, 

963 force=force, 

964 keep_going=keep_going, 

965 ) 

966 # END handle recursive update 

967 # END handle dry run 

968 # END for each submodule 

969 

970 return self 

971 

972 @unbare_repo 

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

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

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

976 adjusting our index entry accordingly. 

977 

978 :param module_path: 

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

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

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

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

983 

984 :param configuration: 

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

986 to the given path. 

987 

988 :param module: 

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

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

991 parent repository in an inconsistent state. 

992 

993 :return: 

994 self 

995 

996 :raise ValueError: 

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

998 

999 :note: 

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

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

1002 """ 

1003 if module + configuration < 1: 

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

1005 # END handle input 

1006 

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

1008 

1009 # VERIFY DESTINATION 

1010 if module_checkout_path == self.path: 

1011 return self 

1012 # END handle no change 

1013 

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

1015 if osp.isfile(module_checkout_abspath): 

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

1017 # END handle target files 

1018 

1019 index = self.repo.index 

1020 tekey = index.entry_key(module_checkout_path, 0) 

1021 # if the target item already exists, fail 

1022 if configuration and tekey in index.entries: 

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

1024 # END handle index key already there 

1025 

1026 # Remove existing destination. 

1027 if module: 

1028 if osp.exists(module_checkout_abspath): 

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

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

1031 # END handle non-emptiness 

1032 

1033 if osp.islink(module_checkout_abspath): 

1034 os.remove(module_checkout_abspath) 

1035 else: 

1036 os.rmdir(module_checkout_abspath) 

1037 # END handle link 

1038 else: 

1039 # Recreate parent directories. 

1040 # NOTE: renames() does that now. 

1041 pass 

1042 # END handle existence 

1043 # END handle module 

1044 

1045 # Move the module into place if possible. 

1046 cur_path = self.abspath 

1047 renamed_module = False 

1048 if module and osp.exists(cur_path): 

1049 os.renames(cur_path, module_checkout_abspath) 

1050 renamed_module = True 

1051 

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

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

1054 self._write_git_file_and_module_config(module_checkout_abspath, module_abspath) 

1055 # END handle git file rewrite 

1056 # END move physical module 

1057 

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

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

1060 previous_sm_path = self.path 

1061 try: 

1062 if configuration: 

1063 try: 

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

1065 entry = index.entries[ekey] 

1066 del index.entries[ekey] 

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

1068 index.entries[tekey] = nentry 

1069 except KeyError as e: 

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

1071 # END handle submodule doesn't exist 

1072 

1073 # Update configuration. 

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

1075 writer.set_value("path", module_checkout_path) 

1076 self.path = module_checkout_path 

1077 # END handle configuration flag 

1078 except Exception: 

1079 if renamed_module: 

1080 os.renames(module_checkout_abspath, cur_path) 

1081 # END undo module renaming 

1082 raise 

1083 # END handle undo rename 

1084 

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

1086 # directory. 

1087 if previous_sm_path == self.name: 

1088 self.rename(module_checkout_path) 

1089 

1090 return self 

1091 

1092 @unbare_repo 

1093 def remove( 

1094 self, 

1095 module: bool = True, 

1096 force: bool = False, 

1097 configuration: bool = True, 

1098 dry_run: bool = False, 

1099 ) -> "Submodule": 

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

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

1102 

1103 :param module: 

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

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

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

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

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

1109 have been altered. 

1110 

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

1112 to touching the direct submodule. 

1113 

1114 :param force: 

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

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

1117 

1118 :param configuration: 

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

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

1121 you to safely delete the repository of your submodule. 

1122 

1123 :param dry_run: 

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

1125 usually throw. 

1126 

1127 :return: 

1128 self 

1129 

1130 :note: 

1131 Doesn't work in bare repositories. 

1132 

1133 :note: 

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

1135 leave an inconsistent state. 

1136 

1137 :raise git.exc.InvalidGitRepositoryError: 

1138 Thrown if the repository cannot be deleted. 

1139 

1140 :raise OSError: 

1141 If directories or files could not be removed. 

1142 """ 

1143 if not (module or configuration): 

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

1145 # END handle parameters 

1146 

1147 # Recursively remove children of this submodule. 

1148 nc = 0 

1149 for csm in self.children(): 

1150 nc += 1 

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

1152 del csm 

1153 

1154 if configuration and not dry_run and nc > 0: 

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

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

1157 # expected. 

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

1159 # END handle recursion 

1160 

1161 # DELETE REPOSITORY WORKING TREE 

1162 ################################ 

1163 if module and self.module_exists(): 

1164 mod = self.module() 

1165 git_dir = mod.git_dir 

1166 if force: 

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

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

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

1170 # submodules first. 

1171 mp = self.abspath 

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

1173 if osp.islink(mp): 

1174 method = os.remove 

1175 elif osp.isdir(mp): 

1176 method = rmtree 

1177 elif osp.exists(mp): 

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

1179 # END handle brutal deletion 

1180 if not dry_run: 

1181 assert method 

1182 method(mp) 

1183 # END apply deletion method 

1184 else: 

1185 # Verify we may delete our module. 

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

1187 raise InvalidGitRepositoryError( 

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

1189 % mod.working_tree_dir 

1190 ) 

1191 # END check for dirt 

1192 

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

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

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

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

1197 for remote in mod.remotes: 

1198 num_branches_with_new_commits = 0 

1199 rrefs = remote.refs 

1200 for rref in rrefs: 

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

1202 # END for each remote ref 

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

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

1205 raise InvalidGitRepositoryError( 

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

1207 ) 

1208 # END handle new commits 

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

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

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

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

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

1214 if len(rrefs): 

1215 del rref # skipcq: PYL-W0631 

1216 # END handle remotes 

1217 del rrefs 

1218 del remote 

1219 # END for each remote 

1220 

1221 # Finally delete our own submodule. 

1222 if not dry_run: 

1223 self._clear_cache() 

1224 wtd = mod.working_tree_dir 

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

1226 gc.collect() 

1227 rmtree(str(wtd)) 

1228 # END delete tree if possible 

1229 # END handle force 

1230 

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

1232 self._clear_cache() 

1233 rmtree(git_dir) 

1234 # END handle separate bare repository 

1235 # END handle module deletion 

1236 

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

1238 if not dry_run: 

1239 self._clear_cache() 

1240 

1241 # DELETE CONFIGURATION 

1242 ###################### 

1243 if configuration and not dry_run: 

1244 # First the index-entry. 

1245 parent_index = self.repo.index 

1246 try: 

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

1248 except KeyError: 

1249 pass 

1250 # END delete entry 

1251 parent_index.write() 

1252 

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

1254 # information anymore. 

1255 

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

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

1258 

1259 with self.config_writer() as sc_writer: 

1260 sc_writer.remove_section() 

1261 # END delete configuration 

1262 

1263 return self 

1264 

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

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

1267 contain the ``.gitmodules`` blob. 

1268 

1269 :param commit: 

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

1271 to the most recent commit. 

1272 

1273 :param check: 

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

1275 validity of the submodule. 

1276 

1277 :raise ValueError: 

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

1279 

1280 :raise ValueError: 

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

1282 

1283 :return: 

1284 self 

1285 """ 

1286 if commit is None: 

1287 self._parent_commit = None 

1288 return self 

1289 # END handle None 

1290 pcommit = self.repo.commit(commit) 

1291 pctree = pcommit.tree 

1292 if self.k_modules_file not in pctree: 

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

1294 # END handle exceptions 

1295 

1296 prev_pc = self._parent_commit 

1297 self._parent_commit = pcommit 

1298 

1299 if check: 

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

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

1302 self._parent_commit = prev_pc 

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

1304 # END handle submodule did not exist 

1305 # END handle checking mode 

1306 

1307 # Update our sha, it could have changed. 

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

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

1310 try: 

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

1312 except KeyError: 

1313 self.binsha = self.NULL_BIN_SHA 

1314 

1315 self._clear_cache() 

1316 return self 

1317 

1318 @unbare_repo 

1319 def config_writer( 

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

1321 ) -> SectionConstraint["SubmoduleConfigParser"]: 

1322 """ 

1323 :return: 

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

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

1326 

1327 :param index: 

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

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

1330 repository. 

1331 

1332 :param write: 

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

1334 

1335 :note: 

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

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

1338 yourself once the whole operation is complete. 

1339 

1340 :raise ValueError: 

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

1342 current head commit. 

1343 

1344 :raise IOError: 

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

1346 """ 

1347 writer = self._config_parser_constrained(read_only=False) 

1348 if index is not None: 

1349 writer.config._index = index 

1350 writer.config._auto_write = write 

1351 return writer 

1352 

1353 @unbare_repo 

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

1355 """Rename this submodule. 

1356 

1357 :note: 

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

1359 

1360 * ``$parent_git_dir / config`` 

1361 * ``$working_tree_dir / .gitmodules`` 

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

1363 

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

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

1366 

1367 :return: 

1368 This :class:`Submodule` instance 

1369 """ 

1370 if self.name == new_name: 

1371 return self 

1372 

1373 # .git/config 

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

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

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

1377 # entries. 

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

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

1380 

1381 # .gitmodules 

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

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

1384 

1385 self._name = new_name 

1386 

1387 # .git/modules 

1388 mod = self.module() 

1389 if mod.has_separate_working_tree(): 

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

1391 source_dir = mod.git_dir 

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

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

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

1395 os.renames(source_dir, tmp_dir) 

1396 source_dir = tmp_dir 

1397 # END handle self-containment 

1398 os.renames(source_dir, destination_module_abspath) 

1399 if mod.working_tree_dir: 

1400 self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath) 

1401 # END move separate git repository 

1402 

1403 return self 

1404 

1405 # } END edit interface 

1406 

1407 # { Query Interface 

1408 

1409 @unbare_repo 

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

1411 """ 

1412 :return: 

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

1414 submodule path 

1415 

1416 :raise git.exc.InvalidGitRepositoryError: 

1417 If a repository was not available. 

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

1419 """ 

1420 module_checkout_abspath = self.abspath 

1421 try: 

1422 repo = git.Repo(module_checkout_abspath) 

1423 if repo != self.repo: 

1424 return repo 

1425 # END handle repo uninitialized 

1426 except (InvalidGitRepositoryError, NoSuchPathError) as e: 

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

1428 else: 

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

1430 # END handle exceptions 

1431 

1432 def module_exists(self) -> bool: 

1433 """ 

1434 :return: 

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

1436 See the :meth:`module` method. 

1437 """ 

1438 try: 

1439 self.module() 

1440 return True 

1441 except Exception: 

1442 return False 

1443 # END handle exception 

1444 

1445 def exists(self) -> bool: 

1446 """ 

1447 :return: 

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

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

1450 though its module doesn't exist on disk. 

1451 """ 

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

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

1454 loc = locals() 

1455 for attr in self._cache_attrs: 

1456 try: 

1457 if hasattr(self, attr): 

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

1459 # END if we have the attribute cache 

1460 except (cp.NoSectionError, ValueError): 

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

1462 # happen on PY2. 

1463 pass 

1464 # END for each attr 

1465 self._clear_cache() 

1466 

1467 try: 

1468 try: 

1469 self.path # noqa: B018 

1470 return True 

1471 except Exception: 

1472 return False 

1473 # END handle exceptions 

1474 finally: 

1475 for attr in self._cache_attrs: 

1476 if attr in loc: 

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

1478 # END if we have a cache 

1479 # END reapply each attribute 

1480 # END handle object state consistency 

1481 

1482 @property 

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

1484 """ 

1485 :return: 

1486 The branch instance that we are to checkout 

1487 

1488 :raise git.exc.InvalidGitRepositoryError: 

1489 If our module is not yet checked out. 

1490 """ 

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

1492 

1493 @property 

1494 def branch_path(self) -> PathLike: 

1495 """ 

1496 :return: 

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

1498 the remote and track 

1499 """ 

1500 return self._branch_path 

1501 

1502 @property 

1503 def branch_name(self) -> str: 

1504 """ 

1505 :return: 

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

1507 """ 

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

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

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

1511 

1512 @property 

1513 def url(self) -> str: 

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

1515 return self._url 

1516 

1517 @property 

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

1519 """ 

1520 :return: 

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

1522 ``.gitmodules`` file 

1523 

1524 :note: 

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

1526 """ 

1527 if self._parent_commit is None: 

1528 return self.repo.commit() 

1529 return self._parent_commit 

1530 

1531 @property 

1532 def name(self) -> str: 

1533 """ 

1534 :return: 

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

1536 ``.gitmodules`` file. 

1537 

1538 :note: 

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

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

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

1542 """ 

1543 return self._name 

1544 

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

1546 """ 

1547 :return: 

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

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

1550 

1551 :note: 

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

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

1554 

1555 :note: 

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

1557 

1558 :raise IOError: 

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

1560 """ 

1561 return self._config_parser_constrained(read_only=True) 

1562 

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

1564 """ 

1565 :return: 

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

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

1568 checked out. 

1569 """ 

1570 return self._get_intermediate_items(self) 

1571 

1572 # } END query interface 

1573 

1574 # { Iterable Interface 

1575 

1576 @classmethod 

1577 def iter_items( 

1578 cls, 

1579 repo: "Repo", 

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

1581 *args: Any, 

1582 **kwargs: Any, 

1583 ) -> Iterator["Submodule"]: 

1584 """ 

1585 :return: 

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

1587 repository 

1588 """ 

1589 try: 

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

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

1592 except (IOError, BadName): 

1593 return 

1594 # END handle empty iterator 

1595 

1596 for sms in parser.sections(): 

1597 n = sm_name(sms) 

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

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

1600 b = cls.k_head_default 

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

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

1603 # END handle optional information 

1604 

1605 # Get the binsha. 

1606 index = repo.index 

1607 try: 

1608 rt = pc.tree # Root tree 

1609 sm = rt[p] 

1610 except KeyError: 

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

1612 try: 

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

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

1615 except KeyError: 

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

1617 # .gitmodules file. 

1618 continue 

1619 # END handle keyerror 

1620 # END handle critical error 

1621 

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

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

1624 continue 

1625 

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

1627 sm._name = n 

1628 if pc != repo.commit(): 

1629 sm._parent_commit = pc 

1630 # END set only if not most recent! 

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

1632 sm._url = u 

1633 

1634 yield sm 

1635 # END for each section 

1636 

1637 # } END iterable interface