clm5.0/manage_externals/manic/sourcetree.py
2025-01-12 20:48:10 +08:00

354 lines
14 KiB
Python

"""
FIXME(bja, 2017-11) External and SourceTree have a circular dependancy!
"""
import errno
import logging
import os
from .externals_description import ExternalsDescription
from .externals_description import read_externals_description_file
from .externals_description import create_externals_description
from .repository_factory import create_repository
from .repository_git import GitRepository
from .externals_status import ExternalStatus
from .utils import fatal_error, printlog
from .global_constants import EMPTY_STR, LOCAL_PATH_INDICATOR
from .global_constants import VERBOSITY_VERBOSE
class _External(object):
"""
_External represents an external object inside a SourceTree
"""
# pylint: disable=R0902
def __init__(self, root_dir, name, ext_description, svn_ignore_ancestry):
"""Parse an external description file into a dictionary of externals.
Input:
root_dir : string - the root directory path where
'local_path' is relative to.
name : string - name of the ext_description object. may or may not
correspond to something in the path.
ext_description : dict - source ExternalsDescription object
svn_ignore_ancestry : bool - use --ignore-externals with svn switch
"""
self._name = name
self._repo = None
self._externals = EMPTY_STR
self._externals_sourcetree = None
self._stat = ExternalStatus()
self._sparse = None
# Parse the sub-elements
# _path : local path relative to the containing source tree
self._local_path = ext_description[ExternalsDescription.PATH]
# _repo_dir : full repository directory
repo_dir = os.path.join(root_dir, self._local_path)
self._repo_dir_path = os.path.abspath(repo_dir)
# _base_dir : base directory *containing* the repository
self._base_dir_path = os.path.dirname(self._repo_dir_path)
# repo_dir_name : base_dir_path + repo_dir_name = rep_dir_path
self._repo_dir_name = os.path.basename(self._repo_dir_path)
assert(os.path.join(self._base_dir_path, self._repo_dir_name)
== self._repo_dir_path)
self._required = ext_description[ExternalsDescription.REQUIRED]
self._externals = ext_description[ExternalsDescription.EXTERNALS]
# Treat a .gitmodules file as a backup externals config
if not self._externals:
if GitRepository.has_submodules(self._repo_dir_path):
self._externals = ExternalsDescription.GIT_SUBMODULES_FILENAME
repo = create_repository(
name, ext_description[ExternalsDescription.REPO],
svn_ignore_ancestry=svn_ignore_ancestry)
if repo:
self._repo = repo
if self._externals and (self._externals.lower() != 'none'):
self._create_externals_sourcetree()
def get_name(self):
"""
Return the external object's name
"""
return self._name
def get_local_path(self):
"""
Return the external object's path
"""
return self._local_path
def status(self):
"""
If the repo destination directory exists, ensure it is correct (from
correct URL, correct branch or tag), and possibly update the external.
If the repo destination directory does not exist, checkout the correce
branch or tag.
If load_all is True, also load all of the the externals sub-externals.
"""
self._stat.path = self.get_local_path()
if not self._required:
self._stat.source_type = ExternalStatus.OPTIONAL
elif self._local_path == LOCAL_PATH_INDICATOR:
# LOCAL_PATH_INDICATOR, '.' paths, are standalone
# component directories that are not managed by
# checkout_externals.
self._stat.source_type = ExternalStatus.STANDALONE
else:
# managed by checkout_externals
self._stat.source_type = ExternalStatus.MANAGED
ext_stats = {}
if not os.path.exists(self._repo_dir_path):
self._stat.sync_state = ExternalStatus.EMPTY
msg = ('status check: repository directory for "{0}" does not '
'exist.'.format(self._name))
logging.info(msg)
self._stat.current_version = 'not checked out'
# NOTE(bja, 2018-01) directory doesn't exist, so we cannot
# use repo to determine the expected version. We just take
# a best-guess based on the assumption that only tag or
# branch should be set, but not both.
if not self._repo:
self._stat.expected_version = 'unknown'
else:
self._stat.expected_version = self._repo.tag() + self._repo.branch()
else:
if self._repo:
self._repo.status(self._stat, self._repo_dir_path)
if self._externals and self._externals_sourcetree:
# we expect externals and they exist
cwd = os.getcwd()
# SourceTree expects to be called from the correct
# root directory.
os.chdir(self._repo_dir_path)
ext_stats = self._externals_sourcetree.status(self._local_path)
os.chdir(cwd)
all_stats = {}
# don't add the root component because we don't manage it
# and can't provide useful info about it.
if self._local_path != LOCAL_PATH_INDICATOR:
# store the stats under tha local_path, not comp name so
# it will be sorted correctly
all_stats[self._stat.path] = self._stat
if ext_stats:
all_stats.update(ext_stats)
return all_stats
def checkout(self, verbosity, load_all):
"""
If the repo destination directory exists, ensure it is correct (from
correct URL, correct branch or tag), and possibly update the external.
If the repo destination directory does not exist, checkout the correct
branch or tag.
If load_all is True, also load all of the the externals sub-externals.
"""
if load_all:
pass
# Make sure we are in correct location
if not os.path.exists(self._repo_dir_path):
# repository directory doesn't exist. Need to check it
# out, and for that we need the base_dir_path to exist
try:
os.makedirs(self._base_dir_path)
except OSError as error:
if error.errno != errno.EEXIST:
msg = 'Could not create directory "{0}"'.format(
self._base_dir_path)
fatal_error(msg)
if self._stat.source_type != ExternalStatus.STANDALONE:
if verbosity >= VERBOSITY_VERBOSE:
# NOTE(bja, 2018-01) probably do not want to pass
# verbosity in this case, because if (verbosity ==
# VERBOSITY_DUMP), then the previous status output would
# also be dumped, adding noise to the output.
self._stat.log_status_message(VERBOSITY_VERBOSE)
if self._repo:
if self._stat.sync_state == ExternalStatus.STATUS_OK:
# If we're already in sync, avoid showing verbose output
# from the checkout command, unless the verbosity level
# is 2 or more.
checkout_verbosity = verbosity - 1
else:
checkout_verbosity = verbosity
self._repo.checkout(self._base_dir_path, self._repo_dir_name,
checkout_verbosity, self.clone_recursive())
def checkout_externals(self, verbosity, load_all):
"""Checkout the sub-externals for this object
"""
if self.load_externals():
if self._externals_sourcetree:
# NOTE(bja, 2018-02): the subtree externals objects
# were created during initial status check. Updating
# the external may have changed which sub-externals
# are needed. We need to delete those objects and
# re-read the potentially modified externals
# description file.
self._externals_sourcetree = None
self._create_externals_sourcetree()
self._externals_sourcetree.checkout(verbosity, load_all)
def load_externals(self):
'Return True iff an externals file should be loaded'
load_ex = False
if os.path.exists(self._repo_dir_path):
if self._externals:
if self._externals.lower() != 'none':
load_ex = os.path.exists(os.path.join(self._repo_dir_path,
self._externals))
return load_ex
def clone_recursive(self):
'Return True iff any .gitmodules files should be processed'
# Try recursive unless there is an externals entry
recursive = not self._externals
return recursive
def _create_externals_sourcetree(self):
"""
"""
if not os.path.exists(self._repo_dir_path):
# NOTE(bja, 2017-10) repository has not been checked out
# yet, can't process the externals file. Assume we are
# checking status before code is checkoud out and this
# will be handled correctly later.
return
cwd = os.getcwd()
os.chdir(self._repo_dir_path)
if self._externals.lower() == 'none':
msg = ('Internal: Attempt to create source tree for '
'externals = none in {}'.format(self._repo_dir_path))
fatal_error(msg)
if not os.path.exists(self._externals):
if GitRepository.has_submodules():
self._externals = ExternalsDescription.GIT_SUBMODULES_FILENAME
if not os.path.exists(self._externals):
# NOTE(bja, 2017-10) this check is redundent with the one
# in read_externals_description_file!
msg = ('External externals description file "{0}" '
'does not exist! In directory: {1}'.format(
self._externals, self._repo_dir_path))
fatal_error(msg)
externals_root = self._repo_dir_path
model_data = read_externals_description_file(externals_root,
self._externals)
externals = create_externals_description(model_data,
parent_repo=self._repo)
self._externals_sourcetree = SourceTree(externals_root, externals)
os.chdir(cwd)
class SourceTree(object):
"""
SourceTree represents a group of managed externals
"""
def __init__(self, root_dir, model, svn_ignore_ancestry=False):
"""
Build a SourceTree object from a model description
"""
self._root_dir = os.path.abspath(root_dir)
self._all_components = {}
self._required_compnames = []
for comp in model:
src = _External(self._root_dir, comp, model[comp], svn_ignore_ancestry)
self._all_components[comp] = src
if model[comp][ExternalsDescription.REQUIRED]:
self._required_compnames.append(comp)
def status(self, relative_path_base=LOCAL_PATH_INDICATOR):
"""Report the status components
FIXME(bja, 2017-10) what do we do about situations where the
user checked out the optional components, but didn't add
optional for running status? What do we do where the user
didn't add optional to the checkout but did add it to the
status. -- For now, we run status on all components, and try
to do the right thing based on the results....
"""
load_comps = self._all_components.keys()
summary = {}
for comp in load_comps:
printlog('{0}, '.format(comp), end='')
stat = self._all_components[comp].status()
stat_final = {}
for name in stat.keys():
# check if we need to append the relative_path_base to
# the path so it will be sorted in the correct order.
if stat[name].path.startswith(relative_path_base):
# use as is, without any changes to path
stat_final[name] = stat[name]
else:
# append relative_path_base to path and store under key = updated path
modified_path = os.path.join(relative_path_base,
stat[name].path)
stat_final[modified_path] = stat[name]
stat_final[modified_path].path = modified_path
summary.update(stat_final)
return summary
def checkout(self, verbosity, load_all, load_comp=None):
"""
Checkout or update indicated components into the the configured
subdirs.
If load_all is True, recursively checkout all externals.
If load_all is False, load_comp is an optional set of components to load.
If load_all is True and load_comp is None, only load the required externals.
"""
if verbosity >= VERBOSITY_VERBOSE:
printlog('Checking out externals: ')
else:
printlog('Checking out externals: ', end='')
if load_all:
load_comps = self._all_components.keys()
elif load_comp is not None:
load_comps = [load_comp]
else:
load_comps = self._required_compnames
# checkout the primary externals
for comp in load_comps:
if verbosity < VERBOSITY_VERBOSE:
printlog('{0}, '.format(comp), end='')
else:
# verbose output handled by the _External object, just
# output a newline
printlog(EMPTY_STR)
self._all_components[comp].checkout(verbosity, load_all)
printlog('')
# now give each external an opportunitity to checkout it's externals.
for comp in load_comps:
self._all_components[comp].checkout_externals(verbosity, load_all)