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
268 # Don't do anything if we already have the url we search in
269 # place.
270 if len([r for r in rmts if r.url == sm.url]) == 0:
271 progress.update(
272 BEGIN | URLCHANGE,
273 i,
274 len_csms,
275 prefix + "Changing url of submodule %r from %s to %s" % (sm.name, psm.url, sm.url),
276 )
277
278 if not dry_run:
279 previous_remote = None
280 for remote in rmts:
281 if remote.url == psm.url:
282 previous_remote = remote
283 break
284 # END if urls match
285 # END for each remote
286
287 # If we didn't find a matching remote, but have exactly
288 # one, we can safely use this one.
289 if previous_remote is None:
290 if len(rmts) == 1:
291 previous_remote = rmts[0]
292 else:
293 # If we have not found any remote with the
294 # original URL we may not have a name. This is a
295 # special case, and its okay to fail here.
296 # Alternatively we could just generate a unique
297 # name and leave all existing ones in place.
298 raise InvalidGitRepositoryError(
299 "Couldn't find original remote-repo at url %r" % psm.url
300 )
301 # END handle one single remote
302 # END handle check we found a remote
303
304 if no_fetch:
305 # A new remote would have no cached refs. Preserve
306 # the existing refs and tracking configuration for
307 # offline updates instead of replacing the remote.
308 previous_remote.set_url(git.Git.polish_url(sm.url, expand_vars=False))
309 else:
310 assert nn not in [r.name for r in rmts]
311 smr = smm.create_remote(nn, sm.url)
312 smr.fetch(progress=progress)
313
314 # If we have a tracking branch, it should be available
315 # in the new remote as well.
316 if len([r for r in smr.refs if r.remote_head == sm.branch_name]) == 0:
317 raise ValueError(
318 "Submodule branch named %r was not available in new submodule remote at %r"
319 % (sm.branch_name, sm.url)
320 )
321 # END head is not detached
322
323 orig_name = previous_remote.name
324 smm.delete_remote(previous_remote)
325 # NOTE: Currently we leave tags from the deleted remotes
326 # as well as separate tracking branches in the possibly
327 # totally changed repository (someone could have changed
328 # the url to another project). At some point, one might
329 # want to clean it up, but the danger is high to remove
330 # stuff the user has added explicitly.
331
332 # Rename the new remote back to what it was.
333 smr.rename(orig_name)
334
335 # Early on, we verified that the our current tracking
336 # branch exists in the remote. Now we have to ensure
337 # that the sha we point to is still contained in the new
338 # remote tracking branch.
339 smsha = sm.binsha
340 found = False
341 rref = smr.refs[self.branch_name]
342 for c in rref.commit.traverse():
343 if c.binsha == smsha:
344 found = True
345 break
346 # END traverse all commits in search for sha
347 # END for each commit
348
349 if not found:
350 # Adjust our internal binsha to use the one of the
351 # remote this way, it will be checked out in the
352 # next step. This will change the submodule relative
353 # to us, so the user will be able to commit the
354 # change easily.
355 _logger.warning(
356 "Current sha %s was not contained in the tracking\
357 branch at the new remote, setting it the the remote's tracking branch",
358 sm.hexsha,
359 )
360 sm.binsha = rref.commit.binsha
361 # END reset binsha
362
363 # NOTE: All checkout is performed by the base
364 # implementation of update.
365 # END handle dry_run
366 progress.update(
367 END | URLCHANGE,
368 i,
369 len_csms,
370 prefix + "Done adjusting url of submodule %r" % (sm.name),
371 )
372 # END skip remote handling if new url already exists in module
373 # END handle url
374
375 # HANDLE PATH CHANGES
376 #####################
377 if sm.branch_path != psm.branch_path:
378 # Finally, create a new tracking branch which tracks the new
379 # remote branch.
380 progress.update(
381 BEGIN | BRANCHCHANGE,
382 i,
383 len_csms,
384 prefix
385 + "Changing branch of submodule %r from %s to %s"
386 % (sm.name, psm.branch_path, sm.branch_path),
387 )
388 if not dry_run:
389 smm = sm.module()
390 smmr = smm.remotes
391 # As the branch might not exist yet, fetch all remotes
392 # unless restricted to locally cached refs.
393 if not no_fetch:
394 for remote in smmr:
395 remote.fetch(progress=progress)
396 # END for each remote
397
398 try:
399 tbr = git.Head.create(
400 smm,
401 sm.branch_name,
402 logmsg="branch: Created from HEAD",
403 )
404 except OSError:
405 # ...or reuse the existing one.
406 tbr = git.Head(smm, sm.branch_path)
407 # END ensure tracking branch exists
408
409 tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name))
410 # NOTE: All head-resetting is done in the base
411 # implementation of update but we will have to checkout the
412 # new branch here. As it still points to the currently
413 # checked out commit, we don't do any harm.
414 # As we don't want to update working-tree or index, changing
415 # the ref is all there is to do.
416 smm.head.reference = tbr
417 # END handle dry_run
418
419 progress.update(
420 END | BRANCHCHANGE,
421 i,
422 len_csms,
423 prefix + "Done changing branch of submodule %r" % sm.name,
424 )
425 # END handle branch
426 # END handle
427 # END for each common submodule
428 except Exception as err:
429 if not keep_going:
430 raise
431 _logger.error(str(err))
432 # END handle keep_going
433
434 # FINALLY UPDATE ALL ACTUAL SUBMODULES
435 ######################################
436 for sm in sms:
437 # Update the submodule using the default method.
438 sm.update(
439 recursive=False,
440 init=init,
441 to_latest_revision=to_latest_revision,
442 progress=progress,
443 dry_run=dry_run,
444 force=force_reset,
445 keep_going=keep_going,
446 no_fetch=no_fetch,
447 )
448
449 # Update recursively depth first - question is which inconsistent state will
450 # be better in case it fails somewhere. Defective branch or defective depth.
451 # The RootSubmodule type will never process itself, which was done in the
452 # previous expression.
453 if recursive:
454 # The module would exist by now if we are not in dry_run mode.
455 if sm.module_exists():
456 type(self)(sm.module()).update(
457 recursive=True,
458 force_remove=force_remove,
459 init=init,
460 to_latest_revision=to_latest_revision,
461 progress=progress,
462 dry_run=dry_run,
463 force_reset=force_reset,
464 keep_going=keep_going,
465 no_fetch=no_fetch,
466 )
467 # END handle dry_run
468 # END handle recursive
469 # END for each submodule to update
470
471 return self
472
473 def module(self) -> "Repo":
474 """:return: The actual repository containing the submodules"""
475 return self.repo
476
477 # } END interface
478
479
480# } END classes