467 lines
20 KiB
Python
467 lines
20 KiB
Python
# This module is part of GitPython and is released under the
|
|
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
|
|
|
|
__all__ = ["RootModule", "RootUpdateProgress"]
|
|
|
|
import logging
|
|
|
|
import git
|
|
from git.exc import InvalidGitRepositoryError
|
|
|
|
from .base import Submodule, UpdateProgress
|
|
from .util import find_first_remote_branch
|
|
|
|
# typing -------------------------------------------------------------------
|
|
|
|
from typing import TYPE_CHECKING, Union
|
|
|
|
from git.types import Commit_ish
|
|
|
|
if TYPE_CHECKING:
|
|
from git.repo import Repo
|
|
from git.util import IterableList
|
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RootUpdateProgress(UpdateProgress):
|
|
"""Utility class which adds more opcodes to
|
|
:class:`~git.objects.submodule.base.UpdateProgress`."""
|
|
|
|
REMOVE, PATHCHANGE, BRANCHCHANGE, URLCHANGE = [
|
|
1 << x for x in range(UpdateProgress._num_op_codes, UpdateProgress._num_op_codes + 4)
|
|
]
|
|
_num_op_codes = UpdateProgress._num_op_codes + 4
|
|
|
|
__slots__ = ()
|
|
|
|
|
|
BEGIN = RootUpdateProgress.BEGIN
|
|
END = RootUpdateProgress.END
|
|
REMOVE = RootUpdateProgress.REMOVE
|
|
BRANCHCHANGE = RootUpdateProgress.BRANCHCHANGE
|
|
URLCHANGE = RootUpdateProgress.URLCHANGE
|
|
PATHCHANGE = RootUpdateProgress.PATHCHANGE
|
|
|
|
|
|
class RootModule(Submodule):
|
|
"""A (virtual) root of all submodules in the given repository.
|
|
|
|
This can be used to more easily traverse all submodules of the
|
|
superproject (master repository).
|
|
"""
|
|
|
|
__slots__ = ()
|
|
|
|
k_root_name = "__ROOT__"
|
|
|
|
def __init__(self, repo: "Repo") -> None:
|
|
# repo, binsha, mode=None, path=None, name = None, parent_commit=None, url=None, ref=None)
|
|
super().__init__(
|
|
repo,
|
|
binsha=self.NULL_BIN_SHA,
|
|
mode=self.k_default_mode,
|
|
path="",
|
|
name=self.k_root_name,
|
|
parent_commit=repo.head.commit,
|
|
url="",
|
|
branch_path=git.Head.to_full_path(self.k_head_default),
|
|
)
|
|
|
|
def _clear_cache(self) -> None:
|
|
"""May not do anything."""
|
|
pass
|
|
|
|
# { Interface
|
|
|
|
def update( # type: ignore[override]
|
|
self,
|
|
previous_commit: Union[Commit_ish, str, None] = None,
|
|
recursive: bool = True,
|
|
force_remove: bool = False,
|
|
init: bool = True,
|
|
to_latest_revision: bool = False,
|
|
progress: Union[None, "RootUpdateProgress"] = None,
|
|
dry_run: bool = False,
|
|
force_reset: bool = False,
|
|
keep_going: bool = False,
|
|
) -> "RootModule":
|
|
"""Update the submodules of this repository to the current HEAD commit.
|
|
|
|
This method behaves smartly by determining changes of the path of a submodule's
|
|
repository, next to changes to the to-be-checked-out commit or the branch to be
|
|
checked out. This works if the submodule's ID does not change.
|
|
|
|
Additionally it will detect addition and removal of submodules, which will be
|
|
handled gracefully.
|
|
|
|
:param previous_commit:
|
|
If set to a commit-ish, the commit we should use as the previous commit the
|
|
HEAD pointed to before it was set to the commit it points to now.
|
|
If ``None``, it defaults to ``HEAD@{1}`` otherwise.
|
|
|
|
:param recursive:
|
|
If ``True``, the children of submodules will be updated as well using the
|
|
same technique.
|
|
|
|
:param force_remove:
|
|
If submodules have been deleted, they will be forcibly removed. Otherwise
|
|
the update may fail if a submodule's repository cannot be deleted as changes
|
|
have been made to it.
|
|
(See :meth:`Submodule.update <git.objects.submodule.base.Submodule.update>`
|
|
for more information.)
|
|
|
|
:param init:
|
|
If we encounter a new module which would need to be initialized, then do it.
|
|
|
|
:param to_latest_revision:
|
|
If ``True``, instead of checking out the revision pointed to by this
|
|
submodule's sha, the checked out tracking branch will be merged with the
|
|
latest remote branch fetched from the repository's origin.
|
|
|
|
Unless `force_reset` is specified, a local tracking branch will never be
|
|
reset into its past, therefore the remote branch must be in the future for
|
|
this to have an effect.
|
|
|
|
:param force_reset:
|
|
If ``True``, submodules may checkout or reset their branch even if the
|
|
repository has pending changes that would be overwritten, or if the local
|
|
tracking branch is in the future of the remote tracking branch and would be
|
|
reset into its past.
|
|
|
|
:param progress:
|
|
:class:`RootUpdateProgress` instance, or ``None`` if no progress should be
|
|
sent.
|
|
|
|
:param dry_run:
|
|
If ``True``, operations will not actually be performed. Progress messages
|
|
will change accordingly to indicate the WOULD DO state of the operation.
|
|
|
|
:param keep_going:
|
|
If ``True``, we will ignore but log all errors, and keep going recursively.
|
|
Unless `dry_run` is set as well, `keep_going` could cause
|
|
subsequent/inherited errors you wouldn't see otherwise.
|
|
In conjunction with `dry_run`, this can be useful to anticipate all errors
|
|
when updating submodules.
|
|
|
|
:return:
|
|
self
|
|
"""
|
|
if self.repo.bare:
|
|
raise InvalidGitRepositoryError("Cannot update submodules in bare repositories")
|
|
# END handle bare
|
|
|
|
if progress is None:
|
|
progress = RootUpdateProgress()
|
|
# END ensure progress is set
|
|
|
|
prefix = ""
|
|
if dry_run:
|
|
prefix = "DRY-RUN: "
|
|
|
|
repo = self.repo
|
|
|
|
try:
|
|
# SETUP BASE COMMIT
|
|
###################
|
|
cur_commit = repo.head.commit
|
|
if previous_commit is None:
|
|
try:
|
|
previous_commit = repo.commit(repo.head.log_entry(-1).oldhexsha)
|
|
if previous_commit.binsha == previous_commit.NULL_BIN_SHA:
|
|
raise IndexError
|
|
# END handle initial commit
|
|
except IndexError:
|
|
# In new repositories, there is no previous commit.
|
|
previous_commit = cur_commit
|
|
# END exception handling
|
|
else:
|
|
previous_commit = repo.commit(previous_commit) # Obtain commit object.
|
|
# END handle previous commit
|
|
|
|
psms: "IterableList[Submodule]" = self.list_items(repo, parent_commit=previous_commit)
|
|
sms: "IterableList[Submodule]" = self.list_items(repo)
|
|
spsms = set(psms)
|
|
ssms = set(sms)
|
|
|
|
# HANDLE REMOVALS
|
|
###################
|
|
rrsm = spsms - ssms
|
|
len_rrsm = len(rrsm)
|
|
|
|
for i, rsm in enumerate(rrsm):
|
|
op = REMOVE
|
|
if i == 0:
|
|
op |= BEGIN
|
|
# END handle begin
|
|
|
|
# Fake it into thinking its at the current commit to allow deletion
|
|
# of previous module. Trigger the cache to be updated before that.
|
|
progress.update(
|
|
op,
|
|
i,
|
|
len_rrsm,
|
|
prefix + "Removing submodule %r at %s" % (rsm.name, rsm.abspath),
|
|
)
|
|
rsm._parent_commit = repo.head.commit
|
|
rsm.remove(
|
|
configuration=False,
|
|
module=True,
|
|
force=force_remove,
|
|
dry_run=dry_run,
|
|
)
|
|
|
|
if i == len_rrsm - 1:
|
|
op |= END
|
|
# END handle end
|
|
progress.update(op, i, len_rrsm, prefix + "Done removing submodule %r" % rsm.name)
|
|
# END for each removed submodule
|
|
|
|
# HANDLE PATH RENAMES
|
|
#####################
|
|
# URL changes + branch changes.
|
|
csms = spsms & ssms
|
|
len_csms = len(csms)
|
|
for i, csm in enumerate(csms):
|
|
psm: "Submodule" = psms[csm.name]
|
|
sm: "Submodule" = sms[csm.name]
|
|
|
|
# PATH CHANGES
|
|
##############
|
|
if sm.path != psm.path and psm.module_exists():
|
|
progress.update(
|
|
BEGIN | PATHCHANGE,
|
|
i,
|
|
len_csms,
|
|
prefix + "Moving repository of submodule %r from %s to %s" % (sm.name, psm.abspath, sm.abspath),
|
|
)
|
|
# Move the module to the new path.
|
|
if not dry_run:
|
|
psm.move(sm.path, module=True, configuration=False)
|
|
# END handle dry_run
|
|
progress.update(
|
|
END | PATHCHANGE,
|
|
i,
|
|
len_csms,
|
|
prefix + "Done moving repository of submodule %r" % sm.name,
|
|
)
|
|
# END handle path changes
|
|
|
|
if sm.module_exists():
|
|
# HANDLE URL CHANGE
|
|
###################
|
|
if sm.url != psm.url:
|
|
# Add the new remote, remove the old one.
|
|
# This way, if the url just changes, the commits will not have
|
|
# to be re-retrieved.
|
|
nn = "__new_origin__"
|
|
smm = sm.module()
|
|
rmts = smm.remotes
|
|
|
|
# Don't do anything if we already have the url we search in
|
|
# place.
|
|
if len([r for r in rmts if r.url == sm.url]) == 0:
|
|
progress.update(
|
|
BEGIN | URLCHANGE,
|
|
i,
|
|
len_csms,
|
|
prefix + "Changing url of submodule %r from %s to %s" % (sm.name, psm.url, sm.url),
|
|
)
|
|
|
|
if not dry_run:
|
|
assert nn not in [r.name for r in rmts]
|
|
smr = smm.create_remote(nn, sm.url)
|
|
smr.fetch(progress=progress)
|
|
|
|
# If we have a tracking branch, it should be available
|
|
# in the new remote as well.
|
|
if len([r for r in smr.refs if r.remote_head == sm.branch_name]) == 0:
|
|
raise ValueError(
|
|
"Submodule branch named %r was not available in new submodule remote at %r"
|
|
% (sm.branch_name, sm.url)
|
|
)
|
|
# END head is not detached
|
|
|
|
# Now delete the changed one.
|
|
rmt_for_deletion = None
|
|
for remote in rmts:
|
|
if remote.url == psm.url:
|
|
rmt_for_deletion = remote
|
|
break
|
|
# END if urls match
|
|
# END for each remote
|
|
|
|
# If we didn't find a matching remote, but have exactly
|
|
# one, we can safely use this one.
|
|
if rmt_for_deletion is None:
|
|
if len(rmts) == 1:
|
|
rmt_for_deletion = rmts[0]
|
|
else:
|
|
# If we have not found any remote with the
|
|
# original URL we may not have a name. This is a
|
|
# special case, and its okay to fail here.
|
|
# Alternatively we could just generate a unique
|
|
# name and leave all existing ones in place.
|
|
raise InvalidGitRepositoryError(
|
|
"Couldn't find original remote-repo at url %r" % psm.url
|
|
)
|
|
# END handle one single remote
|
|
# END handle check we found a remote
|
|
|
|
orig_name = rmt_for_deletion.name
|
|
smm.delete_remote(rmt_for_deletion)
|
|
# NOTE: Currently we leave tags from the deleted remotes
|
|
# as well as separate tracking branches in the possibly
|
|
# totally changed repository (someone could have changed
|
|
# the url to another project). At some point, one might
|
|
# want to clean it up, but the danger is high to remove
|
|
# stuff the user has added explicitly.
|
|
|
|
# Rename the new remote back to what it was.
|
|
smr.rename(orig_name)
|
|
|
|
# Early on, we verified that the our current tracking
|
|
# branch exists in the remote. Now we have to ensure
|
|
# that the sha we point to is still contained in the new
|
|
# remote tracking branch.
|
|
smsha = sm.binsha
|
|
found = False
|
|
rref = smr.refs[self.branch_name]
|
|
for c in rref.commit.traverse():
|
|
if c.binsha == smsha:
|
|
found = True
|
|
break
|
|
# END traverse all commits in search for sha
|
|
# END for each commit
|
|
|
|
if not found:
|
|
# Adjust our internal binsha to use the one of the
|
|
# remote this way, it will be checked out in the
|
|
# next step. This will change the submodule relative
|
|
# to us, so the user will be able to commit the
|
|
# change easily.
|
|
_logger.warning(
|
|
"Current sha %s was not contained in the tracking\
|
|
branch at the new remote, setting it the the remote's tracking branch",
|
|
sm.hexsha,
|
|
)
|
|
sm.binsha = rref.commit.binsha
|
|
# END reset binsha
|
|
|
|
# NOTE: All checkout is performed by the base
|
|
# implementation of update.
|
|
# END handle dry_run
|
|
progress.update(
|
|
END | URLCHANGE,
|
|
i,
|
|
len_csms,
|
|
prefix + "Done adjusting url of submodule %r" % (sm.name),
|
|
)
|
|
# END skip remote handling if new url already exists in module
|
|
# END handle url
|
|
|
|
# HANDLE PATH CHANGES
|
|
#####################
|
|
if sm.branch_path != psm.branch_path:
|
|
# Finally, create a new tracking branch which tracks the new
|
|
# remote branch.
|
|
progress.update(
|
|
BEGIN | BRANCHCHANGE,
|
|
i,
|
|
len_csms,
|
|
prefix
|
|
+ "Changing branch of submodule %r from %s to %s"
|
|
% (sm.name, psm.branch_path, sm.branch_path),
|
|
)
|
|
if not dry_run:
|
|
smm = sm.module()
|
|
smmr = smm.remotes
|
|
# As the branch might not exist yet, we will have to fetch
|
|
# all remotes to be sure...
|
|
for remote in smmr:
|
|
remote.fetch(progress=progress)
|
|
# END for each remote
|
|
|
|
try:
|
|
tbr = git.Head.create(
|
|
smm,
|
|
sm.branch_name,
|
|
logmsg="branch: Created from HEAD",
|
|
)
|
|
except OSError:
|
|
# ...or reuse the existing one.
|
|
tbr = git.Head(smm, sm.branch_path)
|
|
# END ensure tracking branch exists
|
|
|
|
tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name))
|
|
# NOTE: All head-resetting is done in the base
|
|
# implementation of update but we will have to checkout the
|
|
# new branch here. As it still points to the currently
|
|
# checked out commit, we don't do any harm.
|
|
# As we don't want to update working-tree or index, changing
|
|
# the ref is all there is to do.
|
|
smm.head.reference = tbr
|
|
# END handle dry_run
|
|
|
|
progress.update(
|
|
END | BRANCHCHANGE,
|
|
i,
|
|
len_csms,
|
|
prefix + "Done changing branch of submodule %r" % sm.name,
|
|
)
|
|
# END handle branch
|
|
# END handle
|
|
# END for each common submodule
|
|
except Exception as err:
|
|
if not keep_going:
|
|
raise
|
|
_logger.error(str(err))
|
|
# END handle keep_going
|
|
|
|
# FINALLY UPDATE ALL ACTUAL SUBMODULES
|
|
######################################
|
|
for sm in sms:
|
|
# Update the submodule using the default method.
|
|
sm.update(
|
|
recursive=False,
|
|
init=init,
|
|
to_latest_revision=to_latest_revision,
|
|
progress=progress,
|
|
dry_run=dry_run,
|
|
force=force_reset,
|
|
keep_going=keep_going,
|
|
)
|
|
|
|
# Update recursively depth first - question is which inconsistent state will
|
|
# be better in case it fails somewhere. Defective branch or defective depth.
|
|
# The RootSubmodule type will never process itself, which was done in the
|
|
# previous expression.
|
|
if recursive:
|
|
# The module would exist by now if we are not in dry_run mode.
|
|
if sm.module_exists():
|
|
type(self)(sm.module()).update(
|
|
recursive=True,
|
|
force_remove=force_remove,
|
|
init=init,
|
|
to_latest_revision=to_latest_revision,
|
|
progress=progress,
|
|
dry_run=dry_run,
|
|
force_reset=force_reset,
|
|
keep_going=keep_going,
|
|
)
|
|
# END handle dry_run
|
|
# END handle recursive
|
|
# END for each submodule to update
|
|
|
|
return self
|
|
|
|
def module(self) -> "Repo":
|
|
""":return: The actual repository containing the submodules"""
|
|
return self.repo
|
|
|
|
# } END interface
|
|
|
|
|
|
# } END classes
|