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