root.py 19 KB

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