root.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  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. __all__ = ["RootModule", "RootUpdateProgress"]
  4. import logging
  5. import git
  6. from git.exc import InvalidGitRepositoryError
  7. from .base import Submodule, UpdateProgress
  8. from .util import find_first_remote_branch
  9. # typing -------------------------------------------------------------------
  10. from typing import TYPE_CHECKING, Union
  11. from git.types import Commit_ish
  12. if TYPE_CHECKING:
  13. from git.repo import Repo
  14. from git.util import IterableList
  15. # ----------------------------------------------------------------------------
  16. _logger = logging.getLogger(__name__)
  17. class RootUpdateProgress(UpdateProgress):
  18. """Utility class which adds more opcodes to
  19. :class:`~git.objects.submodule.base.UpdateProgress`."""
  20. REMOVE, PATHCHANGE, BRANCHCHANGE, URLCHANGE = [
  21. 1 << x for x in range(UpdateProgress._num_op_codes, UpdateProgress._num_op_codes + 4)
  22. ]
  23. _num_op_codes = UpdateProgress._num_op_codes + 4
  24. __slots__ = ()
  25. BEGIN = RootUpdateProgress.BEGIN
  26. END = RootUpdateProgress.END
  27. REMOVE = RootUpdateProgress.REMOVE
  28. BRANCHCHANGE = RootUpdateProgress.BRANCHCHANGE
  29. URLCHANGE = RootUpdateProgress.URLCHANGE
  30. PATHCHANGE = RootUpdateProgress.PATHCHANGE
  31. class RootModule(Submodule):
  32. """A (virtual) root of all submodules in the given repository.
  33. This can be used to more easily traverse all submodules of the
  34. superproject (master repository).
  35. """
  36. __slots__ = ()
  37. k_root_name = "__ROOT__"
  38. def __init__(self, repo: "Repo") -> None:
  39. # repo, binsha, mode=None, path=None, name = None, parent_commit=None, url=None, ref=None)
  40. super().__init__(
  41. repo,
  42. binsha=self.NULL_BIN_SHA,
  43. mode=self.k_default_mode,
  44. path="",
  45. name=self.k_root_name,
  46. parent_commit=repo.head.commit,
  47. url="",
  48. branch_path=git.Head.to_full_path(self.k_head_default),
  49. )
  50. def _clear_cache(self) -> None:
  51. """May not do anything."""
  52. pass
  53. # { Interface
  54. def update( # type: ignore[override]
  55. self,
  56. previous_commit: Union[Commit_ish, str, None] = None,
  57. recursive: bool = True,
  58. force_remove: bool = False,
  59. init: bool = True,
  60. to_latest_revision: bool = False,
  61. progress: Union[None, "RootUpdateProgress"] = None,
  62. dry_run: bool = False,
  63. force_reset: bool = False,
  64. keep_going: bool = False,
  65. ) -> "RootModule":
  66. """Update the submodules of this repository to the current HEAD commit.
  67. This method behaves smartly by determining changes of the path of a submodule's
  68. repository, next to changes to the to-be-checked-out commit or the branch to be
  69. checked out. This works if the submodule's ID does not change.
  70. Additionally it will detect addition and removal of submodules, which will be
  71. handled gracefully.
  72. :param previous_commit:
  73. If set to a commit-ish, the commit we should use as the previous commit the
  74. HEAD pointed to before it was set to the commit it points to now.
  75. If ``None``, it defaults to ``HEAD@{1}`` otherwise.
  76. :param recursive:
  77. If ``True``, the children of submodules will be updated as well using the
  78. same technique.
  79. :param force_remove:
  80. If submodules have been deleted, they will be forcibly removed. Otherwise
  81. the update may fail if a submodule's repository cannot be deleted as changes
  82. have been made to it.
  83. (See :meth:`Submodule.update <git.objects.submodule.base.Submodule.update>`
  84. for more information.)
  85. :param init:
  86. If we encounter a new module which would need to be initialized, then do it.
  87. :param to_latest_revision:
  88. If ``True``, instead of checking out the revision pointed to by this
  89. submodule's sha, the checked out tracking branch will be merged with the
  90. latest remote branch fetched from the repository's origin.
  91. Unless `force_reset` is specified, a local tracking branch will never be
  92. reset into its past, therefore the remote branch must be in the future for
  93. this to have an effect.
  94. :param force_reset:
  95. If ``True``, submodules may checkout or reset their branch even if the
  96. repository has pending changes that would be overwritten, or if the local
  97. tracking branch is in the future of the remote tracking branch and would be
  98. reset into its past.
  99. :param progress:
  100. :class:`RootUpdateProgress` instance, or ``None`` if no progress should be
  101. sent.
  102. :param dry_run:
  103. If ``True``, operations will not actually be performed. Progress messages
  104. will change accordingly to indicate the WOULD DO state of the operation.
  105. :param keep_going:
  106. If ``True``, we will ignore but log all errors, and keep going recursively.
  107. Unless `dry_run` is set as well, `keep_going` could cause
  108. subsequent/inherited errors you wouldn't see otherwise.
  109. In conjunction with `dry_run`, this can be useful to anticipate all errors
  110. when updating submodules.
  111. :return:
  112. self
  113. """
  114. if self.repo.bare:
  115. raise InvalidGitRepositoryError("Cannot update submodules in bare repositories")
  116. # END handle bare
  117. if progress is None:
  118. progress = RootUpdateProgress()
  119. # END ensure progress is set
  120. prefix = ""
  121. if dry_run:
  122. prefix = "DRY-RUN: "
  123. repo = self.repo
  124. try:
  125. # SETUP BASE COMMIT
  126. ###################
  127. cur_commit = repo.head.commit
  128. if previous_commit is None:
  129. try:
  130. previous_commit = repo.commit(repo.head.log_entry(-1).oldhexsha)
  131. if previous_commit.binsha == previous_commit.NULL_BIN_SHA:
  132. raise IndexError
  133. # END handle initial commit
  134. except IndexError:
  135. # In new repositories, there is no previous commit.
  136. previous_commit = cur_commit
  137. # END exception handling
  138. else:
  139. previous_commit = repo.commit(previous_commit) # Obtain commit object.
  140. # END handle previous commit
  141. psms: "IterableList[Submodule]" = self.list_items(repo, parent_commit=previous_commit)
  142. sms: "IterableList[Submodule]" = self.list_items(repo)
  143. spsms = set(psms)
  144. ssms = set(sms)
  145. # HANDLE REMOVALS
  146. ###################
  147. rrsm = spsms - ssms
  148. len_rrsm = len(rrsm)
  149. for i, rsm in enumerate(rrsm):
  150. op = REMOVE
  151. if i == 0:
  152. op |= BEGIN
  153. # END handle begin
  154. # Fake it into thinking its at the current commit to allow deletion
  155. # of previous module. Trigger the cache to be updated before that.
  156. progress.update(
  157. op,
  158. i,
  159. len_rrsm,
  160. prefix + "Removing submodule %r at %s" % (rsm.name, rsm.abspath),
  161. )
  162. rsm._parent_commit = repo.head.commit
  163. rsm.remove(
  164. configuration=False,
  165. module=True,
  166. force=force_remove,
  167. dry_run=dry_run,
  168. )
  169. if i == len_rrsm - 1:
  170. op |= END
  171. # END handle end
  172. progress.update(op, i, len_rrsm, prefix + "Done removing submodule %r" % rsm.name)
  173. # END for each removed submodule
  174. # HANDLE PATH RENAMES
  175. #####################
  176. # URL changes + branch changes.
  177. csms = spsms & ssms
  178. len_csms = len(csms)
  179. for i, csm in enumerate(csms):
  180. psm: "Submodule" = psms[csm.name]
  181. sm: "Submodule" = sms[csm.name]
  182. # PATH CHANGES
  183. ##############
  184. if sm.path != psm.path and psm.module_exists():
  185. progress.update(
  186. BEGIN | PATHCHANGE,
  187. i,
  188. len_csms,
  189. prefix + "Moving repository of submodule %r from %s to %s" % (sm.name, psm.abspath, sm.abspath),
  190. )
  191. # Move the module to the new path.
  192. if not dry_run:
  193. psm.move(sm.path, module=True, configuration=False)
  194. # END handle dry_run
  195. progress.update(
  196. END | PATHCHANGE,
  197. i,
  198. len_csms,
  199. prefix + "Done moving repository of submodule %r" % sm.name,
  200. )
  201. # END handle path changes
  202. if sm.module_exists():
  203. # HANDLE URL CHANGE
  204. ###################
  205. if sm.url != psm.url:
  206. # Add the new remote, remove the old one.
  207. # This way, if the url just changes, the commits will not have
  208. # to be re-retrieved.
  209. nn = "__new_origin__"
  210. smm = sm.module()
  211. rmts = smm.remotes
  212. # Don't do anything if we already have the url we search in
  213. # place.
  214. if len([r for r in rmts if r.url == sm.url]) == 0:
  215. progress.update(
  216. BEGIN | URLCHANGE,
  217. i,
  218. len_csms,
  219. prefix + "Changing url of submodule %r from %s to %s" % (sm.name, psm.url, sm.url),
  220. )
  221. if not dry_run:
  222. assert nn not in [r.name for r in rmts]
  223. smr = smm.create_remote(nn, sm.url)
  224. smr.fetch(progress=progress)
  225. # If we have a tracking branch, it should be available
  226. # in the new remote as well.
  227. if len([r for r in smr.refs if r.remote_head == sm.branch_name]) == 0:
  228. raise ValueError(
  229. "Submodule branch named %r was not available in new submodule remote at %r"
  230. % (sm.branch_name, sm.url)
  231. )
  232. # END head is not detached
  233. # Now delete the changed one.
  234. rmt_for_deletion = None
  235. for remote in rmts:
  236. if remote.url == psm.url:
  237. rmt_for_deletion = remote
  238. break
  239. # END if urls match
  240. # END for each remote
  241. # If we didn't find a matching remote, but have exactly
  242. # one, we can safely use this one.
  243. if rmt_for_deletion is None:
  244. if len(rmts) == 1:
  245. rmt_for_deletion = rmts[0]
  246. else:
  247. # If we have not found any remote with the
  248. # original URL we may not have a name. This is a
  249. # special case, and its okay to fail here.
  250. # Alternatively we could just generate a unique
  251. # name and leave all existing ones in place.
  252. raise InvalidGitRepositoryError(
  253. "Couldn't find original remote-repo at url %r" % psm.url
  254. )
  255. # END handle one single remote
  256. # END handle check we found a remote
  257. orig_name = rmt_for_deletion.name
  258. smm.delete_remote(rmt_for_deletion)
  259. # NOTE: Currently we leave tags from the deleted remotes
  260. # as well as separate tracking branches in the possibly
  261. # totally changed repository (someone could have changed
  262. # the url to another project). At some point, one might
  263. # want to clean it up, but the danger is high to remove
  264. # stuff the user has added explicitly.
  265. # Rename the new remote back to what it was.
  266. smr.rename(orig_name)
  267. # Early on, we verified that the our current tracking
  268. # branch exists in the remote. Now we have to ensure
  269. # that the sha we point to is still contained in the new
  270. # remote tracking branch.
  271. smsha = sm.binsha
  272. found = False
  273. rref = smr.refs[self.branch_name]
  274. for c in rref.commit.traverse():
  275. if c.binsha == smsha:
  276. found = True
  277. break
  278. # END traverse all commits in search for sha
  279. # END for each commit
  280. if not found:
  281. # Adjust our internal binsha to use the one of the
  282. # remote this way, it will be checked out in the
  283. # next step. This will change the submodule relative
  284. # to us, so the user will be able to commit the
  285. # change easily.
  286. _logger.warning(
  287. "Current sha %s was not contained in the tracking\
  288. branch at the new remote, setting it the the remote's tracking branch",
  289. sm.hexsha,
  290. )
  291. sm.binsha = rref.commit.binsha
  292. # END reset binsha
  293. # NOTE: All checkout is performed by the base
  294. # implementation of update.
  295. # END handle dry_run
  296. progress.update(
  297. END | URLCHANGE,
  298. i,
  299. len_csms,
  300. prefix + "Done adjusting url of submodule %r" % (sm.name),
  301. )
  302. # END skip remote handling if new url already exists in module
  303. # END handle url
  304. # HANDLE PATH CHANGES
  305. #####################
  306. if sm.branch_path != psm.branch_path:
  307. # Finally, create a new tracking branch which tracks the new
  308. # remote branch.
  309. progress.update(
  310. BEGIN | BRANCHCHANGE,
  311. i,
  312. len_csms,
  313. prefix
  314. + "Changing branch of submodule %r from %s to %s"
  315. % (sm.name, psm.branch_path, sm.branch_path),
  316. )
  317. if not dry_run:
  318. smm = sm.module()
  319. smmr = smm.remotes
  320. # As the branch might not exist yet, we will have to fetch
  321. # all remotes to be sure...
  322. for remote in smmr:
  323. remote.fetch(progress=progress)
  324. # END for each remote
  325. try:
  326. tbr = git.Head.create(
  327. smm,
  328. sm.branch_name,
  329. logmsg="branch: Created from HEAD",
  330. )
  331. except OSError:
  332. # ...or reuse the existing one.
  333. tbr = git.Head(smm, sm.branch_path)
  334. # END ensure tracking branch exists
  335. tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name))
  336. # NOTE: All head-resetting is done in the base
  337. # implementation of update but we will have to checkout the
  338. # new branch here. As it still points to the currently
  339. # checked out commit, we don't do any harm.
  340. # As we don't want to update working-tree or index, changing
  341. # the ref is all there is to do.
  342. smm.head.reference = tbr
  343. # END handle dry_run
  344. progress.update(
  345. END | BRANCHCHANGE,
  346. i,
  347. len_csms,
  348. prefix + "Done changing branch of submodule %r" % sm.name,
  349. )
  350. # END handle branch
  351. # END handle
  352. # END for each common submodule
  353. except Exception as err:
  354. if not keep_going:
  355. raise
  356. _logger.error(str(err))
  357. # END handle keep_going
  358. # FINALLY UPDATE ALL ACTUAL SUBMODULES
  359. ######################################
  360. for sm in sms:
  361. # Update the submodule using the default method.
  362. sm.update(
  363. recursive=False,
  364. init=init,
  365. to_latest_revision=to_latest_revision,
  366. progress=progress,
  367. dry_run=dry_run,
  368. force=force_reset,
  369. keep_going=keep_going,
  370. )
  371. # Update recursively depth first - question is which inconsistent state will
  372. # be better in case it fails somewhere. Defective branch or defective depth.
  373. # The RootSubmodule type will never process itself, which was done in the
  374. # previous expression.
  375. if recursive:
  376. # The module would exist by now if we are not in dry_run mode.
  377. if sm.module_exists():
  378. type(self)(sm.module()).update(
  379. recursive=True,
  380. force_remove=force_remove,
  381. init=init,
  382. to_latest_revision=to_latest_revision,
  383. progress=progress,
  384. dry_run=dry_run,
  385. force_reset=force_reset,
  386. keep_going=keep_going,
  387. )
  388. # END handle dry_run
  389. # END handle recursive
  390. # END for each submodule to update
  391. return self
  392. def module(self) -> "Repo":
  393. """:return: The actual repository containing the submodules"""
  394. return self.repo
  395. # } END interface
  396. # } END classes