summaryrefslogtreecommitdiff
path: root/vcstool/commands/import_.py
diff options
context:
space:
mode:
Diffstat (limited to 'vcstool/commands/import_.py')
-rw-r--r--vcstool/commands/import_.py267
1 files changed, 267 insertions, 0 deletions
diff --git a/vcstool/commands/import_.py b/vcstool/commands/import_.py
new file mode 100644
index 0000000..188b721
--- /dev/null
+++ b/vcstool/commands/import_.py
@@ -0,0 +1,267 @@
+from __future__ import print_function
+
+import argparse
+import os
+import sys
+
+from vcstool import __version__ as vcstool_version
+from vcstool.clients import vcstool_clients
+from vcstool.clients.vcs_base import run_command
+from vcstool.clients.vcs_base import which
+from vcstool.executor import ansi
+from vcstool.executor import execute_jobs
+from vcstool.executor import output_repositories
+from vcstool.executor import output_results
+from vcstool.streams import set_streams
+import yaml
+
+try:
+ import urllib.request as request
+except ImportError:
+ import urllib2 as request
+
+from .command import add_common_arguments
+from .command import Command
+
+
+class ImportCommand(Command):
+
+ command = 'import'
+ help = 'Import the list of repositories'
+
+ def __init__(
+ self, args, url, version=None, recursive=False, shallow=False
+ ):
+ super(ImportCommand, self).__init__(args)
+ self.url = url
+ self.version = version
+ self.force = args.force
+ self.retry = args.retry
+ self.skip_existing = args.skip_existing
+ self.recursive = recursive
+ self.shallow = shallow
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(
+ description='Import the list of repositories', prog='vcs import')
+ group = parser.add_argument_group('"import" command parameters')
+ group.add_argument(
+ '--input', type=file_or_url_type, default='-',
+ help='Where to read YAML from', metavar='FILE_OR_URL')
+ group.add_argument(
+ '--force', action='store_true', default=False,
+ help="Delete existing directories if they don't contain the "
+ 'repository being imported')
+ group.add_argument(
+ '--shallow', action='store_true', default=False,
+ help='Create a shallow clone without a history')
+ group.add_argument(
+ '--recursive', action='store_true', default=False,
+ help='Recurse into submodules')
+ group.add_argument(
+ '--retry', type=int, metavar='N', default=2,
+ help='Retry commands requiring network access N times on failure')
+ group.add_argument(
+ '--skip-existing', action='store_true', default=False,
+ help="Don't overwrite existing directories or change custom checkouts "
+ 'in repos using the same URL (but fetch repos with same URL)')
+
+ return parser
+
+
+def file_or_url_type(value):
+ if os.path.exists(value) or '://' not in value:
+ return argparse.FileType('r')(value)
+ # use another user agent to avoid getting a 403 (forbidden) error,
+ # since some websites blacklist or block unrecognized user agents
+ return request.Request(
+ value, headers={'User-Agent': 'vcstool/' + vcstool_version})
+
+
+def get_repositories(yaml_file):
+ try:
+ root = yaml.safe_load(yaml_file)
+ except yaml.YAMLError as e:
+ raise RuntimeError('Input data is not valid yaml format: %s' % e)
+
+ try:
+ repositories = root['repositories']
+ return get_repos_in_vcstool_format(repositories)
+ except KeyError as e:
+ raise RuntimeError('Input data is not valid format: %s' % e)
+ except TypeError as e:
+ # try rosinstall file format
+ try:
+ return get_repos_in_rosinstall_format(root)
+ except Exception:
+ raise RuntimeError('Input data is not valid format: %s' % e)
+
+
+def get_repos_in_vcstool_format(repositories):
+ repos = {}
+ if repositories is None:
+ print(
+ ansi('yellowf') + 'List of repositories is empty' + ansi('reset'),
+ file=sys.stderr)
+ return repos
+ for path in repositories:
+ repo = {}
+ attributes = repositories[path]
+ try:
+ repo['type'] = attributes['type']
+ repo['url'] = attributes['url']
+ if 'version' in attributes:
+ repo['version'] = attributes['version']
+ except KeyError as e:
+ print(
+ ansi('yellowf') + (
+ "Repository '%s' does not provide the necessary "
+ 'information: %s' % (path, e)) + ansi('reset'),
+ file=sys.stderr)
+ continue
+ repos[path] = repo
+ return repos
+
+
+def get_repos_in_rosinstall_format(root):
+ repos = {}
+ for i, item in enumerate(root):
+ if len(item.keys()) != 1:
+ raise RuntimeError('Input data is not valid format')
+ repo = {'type': list(item.keys())[0]}
+ attributes = list(item.values())[0]
+ try:
+ path = attributes['local-name']
+ except KeyError as e:
+ print(
+ ansi('yellowf') + (
+ 'Repository #%d does not provide the necessary '
+ 'information: %s' % (i, e)) + ansi('reset'),
+ file=sys.stderr)
+ continue
+ try:
+ repo['url'] = attributes['uri']
+ if 'version' in attributes:
+ repo['version'] = attributes['version']
+ except KeyError as e:
+ print(
+ ansi('yellowf') + (
+ "Repository '%s' does not provide the necessary "
+ 'information: %s' % (path, e)) + ansi('reset'),
+ file=sys.stderr)
+ continue
+ repos[path] = repo
+ return repos
+
+
+def generate_jobs(repos, args):
+ jobs = []
+ for path, repo in repos.items():
+ path = os.path.join(args.path, path)
+ clients = [c for c in vcstool_clients if c.type == repo['type']]
+ if not clients:
+ from vcstool.clients.none import NoneClient
+ job = {
+ 'client': NoneClient(path),
+ 'command': None,
+ 'cwd': path,
+ 'output':
+ "Repository type '%s' is not supported" % repo['type'],
+ 'returncode': NotImplemented
+ }
+ jobs.append(job)
+ continue
+
+ client = clients[0](path)
+ command = ImportCommand(
+ args, repo['url'],
+ str(repo['version']) if 'version' in repo else None,
+ recursive=args.recursive, shallow=args.shallow)
+ job = {'client': client, 'command': command}
+ jobs.append(job)
+ return jobs
+
+
+def add_dependencies(jobs):
+ paths = [job['client'].path for job in jobs]
+ for job in jobs:
+ job['depends'] = set()
+ path = job['client'].path
+ while True:
+ parent_path = os.path.dirname(path)
+ if parent_path == path:
+ break
+ path = parent_path
+ if path in paths:
+ job['depends'].add(path)
+
+
+def main(args=None, stdout=None, stderr=None):
+ set_streams(stdout=stdout, stderr=stderr)
+
+ parser = get_parser()
+ add_common_arguments(
+ parser, skip_hide_empty=True, skip_nested=True, path_nargs='?',
+ path_help='Base path to clone repositories to')
+ args = parser.parse_args(args)
+ try:
+ input_ = args.input
+ if isinstance(input_, request.Request):
+ input_ = request.urlopen(input_)
+ repos = get_repositories(input_)
+ except (RuntimeError, request.URLError) as e:
+ print(ansi('redf') + str(e) + ansi('reset'), file=sys.stderr)
+ return 1
+ jobs = generate_jobs(repos, args)
+ add_dependencies(jobs)
+
+ if args.repos:
+ output_repositories([job['client'] for job in jobs])
+
+ workers = args.workers
+ # for ssh URLs check if the host is known to prevent ssh asking for
+ # confirmation when using more than one worker
+ if workers > 1:
+ ssh_keygen = None
+ checked_hosts = set()
+ for job in list(jobs):
+ if job['command'] is None:
+ continue
+ url = job['command'].url
+ # only check the host from a ssh URL
+ if not url.startswith('git@') or ':' not in url:
+ continue
+ host = url[4:].split(':', 1)[0]
+
+ # only check each host name once
+ if host in checked_hosts:
+ continue
+ checked_hosts.add(host)
+
+ # get ssh-keygen path once
+ if ssh_keygen is None:
+ ssh_keygen = which('ssh-keygen') or False
+ if not ssh_keygen:
+ continue
+
+ result = run_command([ssh_keygen, '-F', host], '')
+ if result['returncode']:
+ print(
+ 'At least one hostname (%s) is unknown, switching to a '
+ 'single worker to allow interactively answering the ssh '
+ 'question to confirm the fingerprint' % host)
+ workers = 1
+ break
+
+ results = execute_jobs(
+ jobs, show_progress=True, number_of_workers=workers,
+ debug_jobs=args.debug)
+ output_results(results)
+
+ any_error = any(r['returncode'] for r in results)
+ return 1 if any_error else 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())