1# Software License Agreement (BSD License) 2# 3# Copyright (c) 2010, Willow Garage, Inc. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions 8# are met: 9# 10# * Redistributions of source code must retain the above copyright 11# notice, this list of conditions and the following disclaimer. 12# * Redistributions in binary form must reproduce the above 13# copyright notice, this list of conditions and the following 14# disclaimer in the documentation and/or other materials provided 15# with the distribution. 16# * Neither the name of Willow Garage, Inc. nor the names of its 17# contributors may be used to endorse or promote products derived 18# from this software without specific prior written permission. 19# 20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31# POSSIBILITY OF SUCH DAMAGE. 32 33"Support for any command line interface (CLI) for wstool" 34 35try: 36 from collections import OrderedDict 37except: 38 from ordereddict import OrderedDict 39import os 40import re 41from optparse import OptionParser 42from wstool.common import samefile, MultiProjectException, select_elements 43 44ONLY_OPTION_VALID_ATTRS = ['path', 'localname', 'version', 45 'revision', 'cur_revision', 'uri', 'cur_uri', 'scmtype'] 46 47 48def get_workspace(argv, shell_path, config_filename=None, varname=None): 49 """ 50 If target option -t is given return value of that one. Else, if varname 51 is given and exists, considers that one, plus, 52 if config_filename is given, searches for a file named in config_filename 53 in 'shell_path' and ancestors. 54 In that case, if two solutions are found, asks the user. 55 56 :param shell_path: where to look for relevant config_filename 57 :param config_filename: optional, filename for files defining workspaces 58 :param varname: optional, env var to be used as workspace folder 59 :returns: abspath if a .rosinstall was found, error and exist else. 60 """ 61 parser = OptionParser() 62 parser.add_option( 63 "-t", "--target-workspace", 64 dest="workspace", default=None, 65 help="which workspace to use", 66 action="store") 67 # suppress errors based on any other options this parser is agnostic about 68 argv2 = [x for x in argv if ((not x.startswith('-')) or 69 x.startswith('--target-workspace=') or 70 x.startswith('-t') or 71 x == '--target-workspace')] 72 (options, _) = parser.parse_args(argv2) 73 if options.workspace is not None: 74 if (config_filename is not None and 75 not os.path.isfile(os.path.join(options.workspace, config_filename))): 76 77 raise MultiProjectException("%s has no workspace configuration file '%s'" % (os.path.abspath(options.workspace), config_filename)) 78 return os.path.abspath(options.workspace) 79 80 varname_path = None 81 if varname is not None and varname in os.environ: 82 # workspace could be relative, maybe confusing, 83 # but that's the users fault 84 varname_path = os.environ[varname] 85 if varname_path.strip() == '' or not os.path.isdir(varname_path): 86 varname_path = None 87 88 # use current dir 89 current_path = None 90 if config_filename is not None: 91 while shell_path is not None and not shell_path == os.path.dirname(shell_path): 92 if os.path.exists(os.path.join(shell_path, config_filename)): 93 current_path = shell_path 94 break 95 shell_path = os.path.dirname(shell_path) 96 97 if current_path is not None and varname_path is not None and not samefile(current_path, varname_path): 98 raise MultiProjectException("Ambiguous workspace: %s=%s, %s" % (varname, varname_path, os.path.abspath(config_filename))) 99 100 if current_path is None and varname_path is None: 101 raise MultiProjectException("Command requires a target workspace.") 102 103 if current_path is not None: 104 return current_path 105 else: 106 return varname_path 107 108 109def _uris_match(basepath, uri1, uri2): 110 """ 111 True if uri2 is None or not None and same folder or equal string 112 as uri1. Relative folders resolved using basepath 113 """ 114 if uri1 is None: 115 uri1 = '' 116 if uri2 is None: 117 return True 118 if ((uri1 == uri2) or 119 (basepath is not None and 120 os.path.isdir(os.path.join(basepath, uri1)) and 121 os.path.realpath(os.path.join(basepath, uri2)) == os.path.realpath(os.path.join(basepath, uri1)))): 122 return True 123 return False 124 125 126def _get_svn_version_from_uri(uri): 127 """ 128 in case of SVN, we can use the final part of 129 standard uri as spec version, if it follows canonical SVN layout 130 131 :param uri: uri to extract version from 132 :returns changed_uri: str, version extracted uri 133 :returns version: str, extracted version 134 :returns: (None, None), for empty uri or when there is no regex match for version info 135 """ 136 if uri is None: 137 return None, None 138 match = re.match('(.*/)((tags|branches|trunk)(/.*)*)', uri) 139 if (match is not None and 140 len(match.groups()) > 1 and 141 uri == ''.join(match.groups()[0:2])): 142 changed_uri = match.groups()[0] 143 version = match.groups()[1] 144 return changed_uri, version 145 return None, None 146 147 148def _get_status_flags(basepath, elt_dict): 149 """ 150 returns a string where each char conveys status information about 151 a config element entry 152 153 :param basepath: path in which element lies 154 :param elt_dict: a dict representing one elt_dict in a table 155 :returns: str 156 """ 157 if 'exists' in elt_dict and elt_dict['exists'] is False: 158 return 'x' 159 mflag = '' 160 if 'modified' in elt_dict and elt_dict['modified'] is True: 161 mflag = 'M' 162 if (('curr_uri' in elt_dict and 163 not _uris_match(basepath, elt_dict['uri'], elt_dict['curr_uri'])) or 164 ('specversion' in elt_dict and 165 elt_dict['specversion'] is not None and 166 elt_dict['actualversion'] is not None and 167 elt_dict['specversion'] != elt_dict['actualversion'])): 168 mflag += 'V' 169 if (('remote_revision' in elt_dict and 170 elt_dict['remote_revision'] != '' and 171 elt_dict['remote_revision'] is not None and 172 'actualversion' in elt_dict and 173 elt_dict['actualversion'] is not None and 174 elt_dict['remote_revision'] != elt_dict['actualversion']) or 175 (('version' not in elt_dict or 176 elt_dict['version'] is None) and 177 'default_remote_label' in elt_dict and 178 elt_dict['default_remote_label'] is not None and 179 ('curr_version' not in elt_dict or 180 elt_dict['curr_version'] != elt_dict['default_remote_label']))): 181 mflag += 'C' 182 return mflag 183 184 185def get_info_table_elements(basepath, entries, unmanaged=False): 186 """returns a list of dict with refined information from entries""" 187 188 outputs = [] 189 for line in entries: 190 if not 'curr_uri' in line: 191 line['curr_uri'] = None 192 if not 'specversion' in line: 193 line['specversion'] = None 194 if not 'actualversion' in line: 195 line['actualversion'] = None 196 if not 'curr_version' in line: 197 line['curr_version'] = None 198 if not 'version' in line: 199 line['version'] = None 200 if not 'remote_revision' in line: 201 line['remote_revision'] = None 202 if not 'curr_version_label' in line: 203 line['curr_version_label'] = None 204 output_dict = {'scm': line['scm'], 205 'uri': line['uri'], 206 'curr_uri': None, 207 'version': line['version'], 208 'localname': line['localname']} 209 210 if line is None: 211 print("Bug Warning, an element is missing") 212 continue 213 214 if line['scm'] == 'git': 215 if (line['specversion'] is not None and len(line['specversion']) > 12): 216 line['specversion'] = line['specversion'][0:12] 217 if (line['actualversion'] is not None and len(line['actualversion']) > 12): 218 line['actualversion'] = line['actualversion'][0:12] 219 if (line['remote_revision'] is not None and len(line['remote_revision']) > 12): 220 line['remote_revision'] = line['remote_revision'][0:12] 221 222 if line['scm'] is not None: 223 224 if line['scm'] == 'svn': 225 (line['uri'], 226 line['version']) = _get_svn_version_from_uri(uri=line['uri']) 227 if line['curr_uri'] is not None: 228 (line['curr_uri'], 229 line['curr_version_label']) = _get_svn_version_from_uri( 230 uri=line['curr_uri']) 231 232 if line['scm'] in ['git', 'svn', 'hg']: 233 line['curr_version'] = line['curr_version_label'] 234 235 if line['curr_version'] is not None: 236 output_dict['version'] = line['curr_version'] 237 if output_dict['version'] is not None: 238 if line['version'] != output_dict['version']: 239 if line['version']: 240 output_dict['version'] += " (%s)" % line['version'] 241 else: 242 if line['default_remote_label']: 243 if output_dict['version'] == line['default_remote_label']: 244 output_dict['version'] += " (=)" 245 else: 246 output_dict['version'] += " (%s)" % line['default_remote_label'] 247 else: 248 output_dict['version'] += " (-)" 249 250 if (line['specversion'] is not None and 251 line['specversion'] != '' and 252 line['actualversion'] != line['specversion']): 253 output_dict['matching'] = "%s (%s)" % (line['actualversion'], line['specversion']) 254 else: 255 output_dict['matching'] = line['actualversion'] 256 257 common_prefixes = ["https://", "http://"] 258 if line['uri'] is not None and unmanaged is False: 259 for pre in common_prefixes: 260 if line['uri'].startswith(pre): 261 line['uri'] = line['uri'][len(pre):] 262 break 263 output_dict['uri'] = line['uri'] 264 265 if line['curr_uri'] is not None: 266 for pre in common_prefixes: 267 if line['curr_uri'].startswith(pre): 268 line['curr_uri'] = line['curr_uri'][len(pre):] 269 break 270 271 if (not _uris_match(basepath, line['uri'], line['curr_uri'])): 272 output_dict['uri'] = "%s (%s)" % (line[ 273 'curr_uri'], line['uri']) 274 275 else: 276 output_dict['matching'] = " " 277 output_dict['status'] = _get_status_flags(basepath, line) 278 279 outputs.append(output_dict) 280 281 return outputs 282 283 284def get_info_table(basepath, entries, data_only=False, reverse=False, 285 unmanaged=False, selected_headers=None): 286 """ 287 return a refined textual representation of the entries. Provides 288 column headers and processes data. 289 """ 290 headers = OrderedDict([ 291 ('localname', "Localname"), 292 ('status', "S"), 293 ('scm', "SCM"), 294 ('version', "Version (Spec)"), 295 ('matching', "UID (Spec)"), 296 ('uri', "URI (Spec) [http(s)://...]"), 297 ]) 298 # table design 299 if unmanaged: 300 selected_headers = ['localname', 'scm', 'uri'] 301 elif selected_headers is None: 302 selected_headers = headers.keys() 303 # validate selected_headers 304 invalid_headers = [h for h in selected_headers if h not in headers.keys()] 305 if invalid_headers: 306 raise ValueError('Invalid headers are passed: %s' % invalid_headers) 307 308 outputs = get_info_table_elements( 309 basepath=basepath, 310 entries=entries, 311 unmanaged=unmanaged) 312 313 # adjust column width 314 column_length = {} 315 for header in list(headers.keys()): 316 column_length[header] = len(headers[header]) 317 for entry in outputs: 318 if entry[header] is not None: 319 column_length[header] = max(column_length[header], 320 len(entry[header])) 321 322 resultlines = [] 323 if not data_only and len(outputs) > 0: 324 header_line = ' ' 325 for i, header in enumerate(selected_headers): 326 output = headers[header] 327 if i < len(selected_headers) - 1: 328 output = output.ljust(column_length[header]) + " " 329 header_line += output 330 resultlines.append(header_line) 331 header_line = ' ' 332 for i, header in enumerate(selected_headers): 333 output = '-' * len(headers[header]) 334 if i < len(selected_headers) - 1: 335 output = output.ljust(column_length[header]) + " " 336 header_line += output 337 resultlines.append(header_line) 338 339 if reverse: 340 outputs = reversed(outputs) 341 for entry in outputs: 342 if entry is None: 343 print("Bug Warning, an element is missing") 344 continue 345 data_line = ' ' 346 for i, header in enumerate(selected_headers): 347 output = entry[header] 348 if output is None: 349 output = '' 350 if i < len(selected_headers) - 1: 351 output = output.ljust(column_length[header]) + " " 352 data_line += output 353 resultlines.append(data_line) 354 return "\n".join(resultlines) 355 356 357def get_info_list(basepath, line, data_only=False): 358 """ 359 Info for a single config entry 360 """ 361 362 assert line is not None, "Bug Warning, an element is missing" 363 364 headers = { 365 'uri': "URI:", 366 'curr_uri': "Current URI:", 367 'scm': "SCM:", 368 'localname': "Localname:", 369 'path': "Path", 370 'version': "Spec-Version:", 371 'curr_version_label': "Current-Version:", 372 'status': "Status:", 373 'specversion': "Spec-Revision:", 374 'actualversion': "Current-Revision:", 375 'properties': "Other Properties:"} 376 377 # table design 378 selected_headers = ['localname', 'path', 'status', 379 'scm', 'uri', 'curr_uri', 380 'version', 'curr_version_label', 'specversion', 381 'actualversion', 'properties'] 382 383 line['status'] = _get_status_flags(basepath, line) 384 385 header_length = 0 386 for header in list(headers.keys()): 387 header_length = max(header_length, len(headers[header])) 388 result = '' 389 for header in selected_headers: 390 if not data_only: 391 title = "%s " % (headers[header].ljust(header_length)) 392 else: 393 title = '' 394 if header in line: 395 output = line[header] 396 if output is None: 397 output = '' 398 result += "%s%s\n" % (title, output) 399 return result 400 401 402def get_info_table_raw_csv(config, parser, properties, localnames): 403 """ 404 returns raw data without decorations in comma-separated value format. 405 allows to select properties. 406 Given a config, collects all elements, and prints a line of each, 407 with selected properties in the output 408 409 :param parser: OptionParser used to throw option errors 410 :param properties: list of property ids to display 411 :param localnames: which config elements to show 412 :return: list of str, each a csv line 413 """ 414 lookup_required = False 415 for attr in properties: 416 if not attr in ONLY_OPTION_VALID_ATTRS: 417 parser.error("Invalid --only option '%s', valids are %s" % 418 (attr, ONLY_OPTION_VALID_ATTRS)) 419 if attr in ['cur_revision', 'cur_uri', 'revision']: 420 lookup_required = True 421 elements = select_elements(config, localnames) 422 result=[] 423 for element in elements: 424 if lookup_required and element.is_vcs_element(): 425 spec = element.get_versioned_path_spec() 426 else: 427 spec = element.get_path_spec() 428 output = [] 429 for attr in properties: 430 if 'localname' == attr: 431 output.append(spec.get_local_name() or '') 432 if 'path' == attr: 433 output.append(spec.get_path() or '') 434 if 'scmtype' == attr: 435 output.append(spec.get_scmtype() or '') 436 if 'uri' == attr: 437 output.append(spec.get_uri() or '') 438 if 'version' == attr: 439 output.append(spec.get_version() or '') 440 if 'revision' == attr: 441 output.append(spec.get_revision() or '') 442 if 'cur_uri' == attr: 443 output.append(spec.get_curr_uri() or '') 444 if 'cur_revision' == attr: 445 output.append(spec.get_current_revision() or '') 446 result.append(','.join(output)) 447 return result 448