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