def cmd_find_unmanaged_repos(config): """ Auxilliary function to find SCM folders within workspace that have not been tracked. This allows quicker diagnosis of the general state in a workspace, where folders can be part of the build even when they are not mentioned in the .rosinstall file. Nested SCMs are not investigated. """ class UnmanagedInfoRetriever(): def __init__(self, path, localname, scm_type): self.path = path self.localname = localname self.scm_type = scm_type self.element = AVCSConfigElement( scm_type, os.path.join(self.path, self.localname), localname, '') def do_work(self): vcsc = get_vcs_client(self.scm_type, os.path.join(self.path, self.localname)) return { 'scm': '--' + self. scm_type, # prefix '--' to allow copy&paste to set command 'localname': self.localname, 'path': self.path, 'uri': vcsc.get_url(), 'properties': self.element.get_properties() } path = config.get_base_path() # call SCM info in separate threads elements = config.get_config_elements() managed_paths = [os.path.join(path, e.get_local_name()) for e in elements] unmanaged_paths = [] scm_clients = { SvnClient: 'svn', GitClient: 'git', BzrClient: 'bzr', HgClient: 'hg' } for root, dirs, files in os.walk(path, followlinks=True): if root in managed_paths: # remove it from the walk if it's managed del dirs[:] else: for client, key in scm_clients.items(): # check if it's a vcs dir if client.static_detect_presence(root): # add it to the unmanaged list unmanaged_paths.append((os.path.relpath(root, path), key)) # don't walk any other directories in this root del dirs[:] work = DistributedWork(capacity=len(unmanaged_paths), num_threads=-1) for localname, scm_type in sorted(unmanaged_paths, key=lambda up: up[0], reverse=True): work.add_thread(UnmanagedInfoRetriever(path, localname, scm_type)) outputs = work.run() return outputs
def cmd_diff(config, localnames=None): """ calls SCM diff for all SCM entries in config, relative to path :returns: List of dict {element: ConfigElement, diff: diffstring} :raises MultiProjectException: on plenty of errors """ class DiffRetriever(): def __init__(self, element, path): self.element = element self.path = path def do_work(self): return {'diff': self.element.get_diff(self.path)} path = config.get_base_path() elements = config.get_config_elements() work = DistributedWork(len(elements)) elements = select_elements(config, localnames) for element in elements: if element.is_vcs_element(): work.add_thread(DiffRetriever(element, path)) outputs = work.run() return outputs
def cmd_foreach(config, command, localnames=None, scm_types=None, shell=False): """Run command in all SCM entries in config, relative to path""" class ForeachRetriever(object): def __init__(self, element, command, shell): self.element = element self.command = command self.shell = shell def do_work(self): command = self.command if not self.shell: command = shlex.split(command) _, stdout, stderr = run_shell_command(command, cwd=self.element.path, show_stdout=False, shell=self.shell) return {'stdout': stdout, 'stderr': stderr} elements = select_elements(config, localnames) work = DistributedWork(capacity=len(elements)) for element in elements: if ((scm_types is not None) and (element.get_vcs_type_name() not in scm_types)): continue work.add_thread(ForeachRetriever(element, command, shell)) outputs = work.run() return outputs
def cmd_diff(config, localnames=None): """ calls SCM diff for all SCM entries in config, relative to path :returns: List of dict {element: ConfigElement, diff: diffstring} :raises MultiProjectException: on plenty of errors """ class DiffRetriever(): def __init__(self, element, path): self.element = element self.path = path def do_work(self): return {'diff': self.element.get_diff(self.path)} path = config.get_base_path() elements = config.get_config_elements() work = DistributedWork(capacity=len(elements), num_threads=-1) elements = select_elements(config, localnames) for element in elements: if element.is_vcs_element(): work.add_thread(DiffRetriever(element, path)) outputs = work.run() return outputs
def cmd_find_unmanaged_repos(config): """ Auxilliary function to find SCM folders within workspace that have not been tracked. This allows quicker diagnosis of the general state in a workspace, where folders can be part of the build even when they are not mentioned in the .rosinstall file. Nested SCMs are not investigated. """ class UnmanagedInfoRetriever(): def __init__(self, path, localname, scm_type): self.path = path self.localname = localname self.scm_type = scm_type self.element = AVCSConfigElement(scm_type, os.path.join(self.path, self.localname), localname, '') def do_work(self): vcsc = get_vcs_client(self.scm_type, os.path.join(self.path, self.localname)) return {'scm': '--' + self.scm_type, # prefix '--' to allow copy&paste to set command 'localname': self.localname, 'path': self.path, 'uri': vcsc.get_url(), 'properties': self.element.get_properties()} path = config.get_base_path() # call SCM info in separate threads elements = config.get_config_elements() managed_paths = [os.path.join(path, e.get_local_name()) for e in elements] unmanaged_paths = [] scm_clients = {SvnClient: 'svn', GitClient: 'git', BzrClient:'bzr', HgClient:'hg'} for root, dirs, files in os.walk(path): if root in managed_paths: # remove it from the walk if it's managed del dirs[:] else: for client, key in scm_clients.items(): # check if it's a vcs dir if client.static_detect_presence(root): # add it to the unmanaged list unmanaged_paths.append((os.path.relpath(root, path), key)) # don't walk any other directories in this root del dirs[:] work = DistributedWork(len(unmanaged_paths)) for localname, scm_type in sorted(unmanaged_paths, key=lambda up: up[0], reverse=True): work.add_thread(UnmanagedInfoRetriever(path, localname, scm_type)) outputs = work.run() return outputs
def cmd_status(config, localnames=None, untracked=False): """ calls SCM status for all SCM entries in config, relative to path :returns: List of dict {element: ConfigElement, diff: diffstring} :param untracked: also show files not added to the SCM :raises MultiProjectException: on plenty of errors """ class StatusRetriever(): def __init__(self, element, path, untracked): self.element = element self.path = path self.untracked = untracked def do_work(self): path_spec = self.element.get_path_spec() scmtype = path_spec.get_scmtype() status = self.element.get_status(self.path, self.untracked) # align other scm output to svn columns = -1 if scmtype == "git": columns = 3 elif scmtype == "hg": columns = 2 elif scmtype == "bzr": columns = 4 if columns > -1 and status is not None: status_aligned = '' for line in status.splitlines(): status_aligned = "%s%s%s\n" % (status_aligned, line[:columns].ljust(8), line[columns:]) status = status_aligned return {'status': status} path = config.get_base_path() # call SCM info in separate threads elements = config.get_config_elements() work = DistributedWork(len(elements)) elements = select_elements(config, localnames) for element in elements: if element.is_vcs_element(): work.add_thread(StatusRetriever(element, path, untracked)) outputs = work.run() return outputs
def cmd_status(config, localnames=None, untracked=False): """ calls SCM status for all SCM entries in config, relative to path :returns: List of dict {element: ConfigElement, diff: diffstring} :param untracked: also show files not added to the SCM :raises MultiProjectException: on plenty of errors """ class StatusRetriever(): def __init__(self, element, path, untracked): self.element = element self.path = path self.untracked = untracked def do_work(self): path_spec = self.element.get_path_spec() scmtype = path_spec.get_scmtype() status = self.element.get_status(self.path, self.untracked) # align other scm output to svn columns = -1 if scmtype == "git": columns = 3 elif scmtype == "hg": columns = 2 elif scmtype == "bzr": columns = 4 if columns > -1 and status is not None: status_aligned = '' for line in status.splitlines(): status_aligned = "%s%s%s\n" % (status_aligned, line[:columns].ljust(8), line[columns:]) status = status_aligned return {'status': status} path = config.get_base_path() # call SCM info in separate threads elements = config.get_config_elements() work = DistributedWork(capacity=len(elements), num_threads=-1) elements = select_elements(config, localnames) for element in elements: if element.is_vcs_element(): work.add_thread(StatusRetriever(element, path, untracked)) outputs = work.run() return outputs
def test_distributed_work(self): work = DistributedWork(3) thing1 = FooThing(FooThing(FooThing(None)), result={'done': True}) thing2 = FooThing(FooThing(FooThing(None)), result={'done': True}) thing3 = FooThing(FooThing(FooThing(None)), result={'done': True}) self.assertEqual(3, len(work.outputs)) work.add_thread(thing1) self.assertEqual(1, len(work.threads)) work.add_thread(thing2) self.assertEqual(2, len(work.threads)) work.add_thread(thing3) self.assertEqual(3, len(work.threads)) self.assertEqual(thing1.done, False) self.assertEqual(thing2.done, False) self.assertEqual(thing3.done, False) output = work.run() self.assertEqual(False, 'error' in output[0], output) self.assertEqual(False, 'error' in output[1], output) self.assertEqual(False, 'error' in output[2], output)
def cmd_info(config, localnames=None, untracked=False, fetch=False): """This function compares what should be (config_file) with what is (directories) and returns a list of dictionary giving each local path and all the state information about it available. """ class InfoRetriever(): """ Auxilliary class to perform IO-bound operations in individual threads """ def __init__(self, element, path, untracked, fetch): self.element = element self.path = path self.fetch = fetch self.untracked = untracked def do_work(self): localname = "" scm = None uri = "" curr_uri = None exists = False version = "" # what is given in config file curr_version_label = "" # e.g. branchname remote_revision = "" # UID on remote default_remote_label = None # git default branch display_version = '' modified = "" currevision = "" # revision number of version specversion = "" # actual revision number localname = self.element.get_local_name() path = self.element.get_path() or localname if localname is None or localname == "": raise MultiProjectException( "Missing local-name in element: %s" % self.element) if (os.path.exists(normabspath(path, self.path))): exists = True if self.element.is_vcs_element(): if not exists: path_spec = self.element.get_path_spec() version = path_spec.get_version() else: path_spec = self.element.get_versioned_path_spec( fetch=fetch) version = path_spec.get_version() remote_revision = path_spec.get_remote_revision() curr_version_label = path_spec.get_curr_version() if (curr_version_label is not None and version != curr_version_label): display_version = curr_version_label else: display_version = version curr_uri = path_spec.get_curr_uri() status = self.element.get_status(self.path, self.untracked) if (status is not None and status.strip() != ''): modified = True specversion = path_spec.get_revision() if (version is not None and version.strip() != '' and (specversion is None or specversion.strip() == '')): specversion = '"%s"' % version if (self.fetch and specversion == None and path_spec.get_scmtype() == 'git'): default_remote_label = self.element.get_default_remote_label( ) currevision = path_spec.get_current_revision() scm = path_spec.get_scmtype() uri = path_spec.get_uri() return { 'scm': scm, 'exists': exists, 'localname': localname, 'path': path, 'uri': uri, 'curr_uri': curr_uri, 'version': version, 'remote_revision': remote_revision, 'default_remote_label': default_remote_label, 'curr_version_label': curr_version_label, 'specversion': specversion, 'actualversion': currevision, 'modified': modified, 'properties': self.element.get_properties() } path = config.get_base_path() # call SCM info in separate threads elements = config.get_config_elements() elements = select_elements(config, localnames) work = DistributedWork(capacity=len(elements), num_threads=-1) for element in elements: if element.get_properties( ) is None or not 'setup-file' in element.get_properties(): work.add_thread(InfoRetriever(element, path, untracked, fetch)) outputs = work.run() return outputs
def cmd_install_or_update(config, backup_path=None, mode='abort', robust=False, localnames=None, num_threads=1, timeout=None, verbose=False, shallow=False): """ performs many things, generally attempting to make the local filesystem look like what the config specifies, pulling from remote sources the most recent changes. The command may have stdin user interaction (TODO abstract) :param backup_path: if and where to backup trees before deleting them :param robust: proceed to next element even when one element fails :returns: True on Success :raises MultiProjectException: on plenty of errors """ success = True if not os.path.exists(config.get_base_path()): os.mkdir(config.get_base_path()) # Prepare install operation check filesystem and ask user preparation_reports = [] elements = select_elements(config, localnames) for tree_el in elements: abs_backup_path = None if backup_path is not None: abs_backup_path = os.path.join(config.get_base_path(), backup_path) try: preparation_report = tree_el.prepare_install( backup_path=abs_backup_path, arg_mode=mode, robust=robust) if preparation_report is not None: if preparation_report.abort: raise MultiProjectException( "Aborting install because of %s" % preparation_report.error) if not preparation_report.skip: preparation_reports.append(preparation_report) else: if preparation_report.error is not None: print("Skipping install of %s because: %s" % ( preparation_report.config_element.get_local_name(), preparation_report.error)) except MultiProjectException as exc: fail_str = ("Failed to install tree '%s'\n %s" % (tree_el.get_path(), exc)) if robust: success = False print("Continuing despite %s" % fail_str) else: raise MultiProjectException(fail_str) class Installer(): def __init__(self, report): self.element = report.config_element self.report = report def do_work(self): self.element.install(checkout=self.report.checkout, backup=self.report.backup, backup_path=self.report.backup_path, inplace=self.report.inplace, timeout=self.report.timeout, verbose=self.report.verbose, shallow=self.report.shallow) return {} work = DistributedWork(capacity=len(preparation_reports), num_threads=num_threads, silent=False) for report in preparation_reports: report.verbose = verbose report.timeout = timeout report.shallow = shallow thread = Installer(report) work.add_thread(thread) try: work.run() except MultiProjectException as exc: print("Exception caught during install: %s" % exc) success = False if not robust: raise return success
def cmd_info(config, localnames=None, untracked=False): """This function compares what should be (config_file) with what is (directories) and returns a list of dictionary giving each local path and all the state information about it available. """ class InfoRetriever(): """ Auxilliary class to perform IO-bound operations in individual threads """ def __init__(self, element, path, untracked): self.element = element self.path = path self.untracked = untracked def do_work(self): localname = "" scm = None uri = "" curr_uri = None exists = False version = "" # what is given in config file modified = "" actualversion = "" # revision number of version specversion = "" # actual revision number localname = self.element.get_local_name() path = self.element.get_path() or localname if localname is None or localname == "": raise MultiProjectException( "Missing local-name in element: %s" % self.element) if (os.path.exists(normabspath(path, self.path))): exists = True if self.element.is_vcs_element(): if not exists: path_spec = self.element.get_path_spec() version = path_spec.get_version() else: path_spec = self.element.get_versioned_path_spec() version = path_spec.get_version() curr_uri = path_spec.get_curr_uri() status = self.element.get_status(self.path, self.untracked) if (status is not None and status.strip() != ''): modified = True specversion = path_spec.get_revision() if (version is not None and version.strip() != '' and (specversion is None or specversion.strip() == '')): specversion = '"%s"' % version actualversion = path_spec.get_current_revision() scm = path_spec.get_scmtype() uri = path_spec.get_uri() return {'scm': scm, 'exists': exists, 'localname': localname, 'path': path, 'uri': uri, 'curr_uri': curr_uri, 'version': version, 'specversion': specversion, 'actualversion': actualversion, 'modified': modified, 'properties': self.element.get_properties()} path = config.get_base_path() # call SCM info in separate threads elements = config.get_config_elements() elements = select_elements(config, localnames) work = DistributedWork(len(elements)) for element in elements: if element.get_properties() is None or not 'setup-file' in element.get_properties(): work.add_thread(InfoRetriever(element, path, untracked)) outputs = work.run() return outputs
def cmd_install_or_update( config, backup_path=None, mode='abort', robust=False, localnames=None, num_threads=1, timeout=None, verbose=False): """ performs many things, generally attempting to make the local filesystem look like what the config specifies, pulling from remote sources the most recent changes. The command may have stdin user interaction (TODO abstract) :param backup_path: if and where to backup trees before deleting them :param robust: proceed to next element even when one element fails :returns: True on Success :raises MultiProjectException: on plenty of errors """ success = True if not os.path.exists(config.get_base_path()): os.mkdir(config.get_base_path()) # Prepare install operation check filesystem and ask user preparation_reports = [] elements = select_elements(config, localnames) for tree_el in elements: abs_backup_path = None if backup_path is not None: abs_backup_path = os.path.join(config.get_base_path(), backup_path) try: preparation_report = tree_el.prepare_install( backup_path=abs_backup_path, arg_mode=mode, robust=robust) if preparation_report is not None: if preparation_report.abort: raise MultiProjectException( "Aborting install because of %s" % preparation_report.error) if not preparation_report.skip: preparation_reports.append(preparation_report) else: if preparation_report.error is not None: print("Skipping install of %s because: %s" % (preparation_report.config_element.get_local_name(), preparation_report.error)) except MultiProjectException as exc: fail_str = ("Failed to install tree '%s'\n %s" % (tree_el.get_path(), exc)) if robust: success = False print("Continuing despite %s" % fail_str) else: raise MultiProjectException(fail_str) class Installer(): def __init__(self, report): self.element = report.config_element self.report = report def do_work(self): self.element.install(checkout=self.report.checkout, backup=self.report.backup, backup_path=self.report.backup_path, inplace=self.report.inplace, timeout=self.report.timeout, verbose=self.report.verbose) return {} work = DistributedWork(len(preparation_reports), num_threads, silent=False) for report in preparation_reports: report.verbose = verbose report.timeout = timeout thread = Installer(report) work.add_thread(thread) try: work.run() except MultiProjectException as exc: print ("Exception caught during install: %s" % exc) success = False if not robust: raise return success