00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033 from __future__ import print_function
00034 import sys
00035 import os
00036 import textwrap
00037 import shutil
00038 from optparse import OptionParser, IndentedHelpFormatter
00039 from common import samefile, select_element, select_elements, MultiProjectException
00040 from config_yaml import PathSpec
00041
00042 import multiproject_cmd
00043
00044
00045
00046 __MULTIPRO_CMD_DICT__={
00047 "help" : "provide help for commands",
00048 "init" : "set up a directory as workspace",
00049 "info" : "Overview of some entries",
00050 "merge" : "merges your workspace with another config set",
00051 "set" : "add or changes one entry from your workspace config",
00052 "update" : "update or check out some of your config elements",
00053 "remove" : "remove an entry from your workspace config, without deleting files",
00054 "snapshot" : "write a file specifying repositories to have the version they currently have",
00055 "diff" : "print a diff over some SCM controlled entries",
00056 "status" : "print the change status of files in some SCM controlled entries",
00057 }
00058
00059 class IndentedHelpFormatterWithNL(IndentedHelpFormatter):
00060 def format_description(self, description):
00061 if not description: return ""
00062 desc_width = self.width - self.current_indent
00063 indent = " "*self.current_indent
00064
00065 bits = description.split('\n')
00066 formatted_bits = [
00067 textwrap.fill(bit,
00068 desc_width,
00069 initial_indent=indent,
00070 subsequent_indent=indent)
00071 for bit in bits]
00072 result = "\n".join(formatted_bits) + "\n"
00073 return result
00074
00075
00076 def _get_mode_from_options(parser, options):
00077 mode = 'prompt'
00078 if options.delete_changed:
00079 mode = 'delete'
00080 if options.abort_changed:
00081 if mode == 'delete':
00082 parser.error("delete-changed-uris is mutually exclusive with abort-changed-uris")
00083 mode = 'abort'
00084 if options.backup_changed != '':
00085 if mode == 'delete':
00086 parser.error("delete-changed-uris is mutually exclusive with backup-changed-uris")
00087 if mode == 'abort':
00088 parser.error("abort-changed-uris is mutually exclusive with backup-changed-uris")
00089 mode = 'backup'
00090 return mode
00091
00092
00093
00094 class MultiprojectCLI:
00095
00096 def __init__(self, config_filename = None):
00097 self.config_filename = config_filename
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109 def cmd_diff(self, target_path, argv, config = None):
00110 parser = OptionParser(usage="usage: rosws diff [localname]* ",
00111 description=__MULTIPRO_CMD_DICT__["diff"],
00112 epilog="See: http://www.ros.org/wiki/rosinstall for details\n")
00113
00114 parser.add_option("-t", "--target-workspace", dest="workspace", default=None,
00115 help="which workspace to use",
00116 action="store")
00117 (options, args) = parser.parse_args(argv)
00118
00119 if config == None:
00120 config = multiproject_cmd.get_config(target_path, [], config_filename = self.config_filename)
00121 elif config.get_base_path() != target_path:
00122 raise MultiProjectException("Config path does not match %s %s "%(config.get_base_path(), target_path))
00123
00124
00125 if len(args) > 0:
00126 difflist = multiproject_cmd.cmd_diff(config, localnames = args)
00127 else:
00128 difflist = multiproject_cmd.cmd_diff(config)
00129 alldiff = ""
00130 for entrydiff in difflist:
00131 if entrydiff['diff'] != None:
00132 alldiff += entrydiff['diff']
00133 print(alldiff)
00134
00135 return False
00136
00137
00138 def cmd_status(self, target_path, argv, config = None):
00139 parser = OptionParser(usage="usage: rosws status [localname]* ",
00140 description=__MULTIPRO_CMD_DICT__["status"] + ". The status columns meanings are as the respective SCM defines them.",
00141 epilog="""See: http://www.ros.org/wiki/rosinstall for details""")
00142 parser.add_option("--untracked", dest="untracked", default=False,
00143 help="Also shows untracked files",
00144 action="store_true")
00145
00146 parser.add_option("-t", "--target-workspace", dest="workspace", default=None,
00147 help="which workspace to use",
00148 action="store")
00149 (options, args) = parser.parse_args(argv)
00150
00151 if config == None:
00152 config = multiproject_cmd.get_config(target_path, [], config_filename = self.config_filename)
00153 elif config.get_base_path() != target_path:
00154 raise MultiProjectException("Config path does not match %s %s "%(config.get_base_path(), target_path))
00155
00156 if len(args) > 0:
00157 statuslist = multiproject_cmd.cmd_status(config,
00158 localnames = args,
00159 untracked = options.untracked)
00160 else:
00161 statuslist = multiproject_cmd.cmd_status(config, untracked = options.untracked)
00162 allstatus=""
00163 for entrystatus in statuslist:
00164 if entrystatus['status'] != None:
00165 allstatus += entrystatus['status']
00166 print(allstatus)
00167 return 0
00168
00169
00170 def cmd_set(self, target_path, argv, config = None):
00171 """
00172 command for modifying/adding a single entry
00173 :param target_path: where to look for config
00174 :param config: config to use instead of parsing file anew
00175 """
00176 parser = OptionParser(usage="usage: rosws set [localname] [SCM-URI]? [--(detached|svn|hg|git|bzr)] [--version=VERSION]]",
00177 formatter = IndentedHelpFormatterWithNL(),
00178 description=__MULTIPRO_CMD_DICT__["set"] + """
00179 The command will infer whether you want to add or modify an entry. If
00180 you modify, it will only change the details you provide, keeping
00181 those you did not provide. if you only provide a uri, will use the
00182 basename of it as localname unless such an element already exists.
00183
00184 The command only changes the configuration, to checkout or update
00185 the element, run rosws update afterwards.
00186
00187 Examples:
00188 $ rosws set robot_model --hg https://kforge.ros.org/robotmodel/robot_model
00189 $ rosws set robot_model --version robot_model-1.7.1
00190 $ rosws set robot_model --detached
00191 """,
00192 epilog="See: http://www.ros.org/wiki/rosinstall for details\n")
00193 parser.add_option("--detached", dest="detach", default=False,
00194 help="make an entry unmanaged (default for new element)",
00195 action="store_true")
00196 parser.add_option("-v","--version-new", dest="version", default=None,
00197 help="point SCM to this version",
00198 action="store")
00199 parser.add_option("--git", dest="git", default=False,
00200 help="make an entry a git entry",
00201 action="store_true")
00202 parser.add_option("--svn", dest="svn", default=False,
00203 help="make an entry a subversion entry",
00204 action="store_true")
00205 parser.add_option("--hg", dest="hg", default=False,
00206 help="make an entry a mercurial entry",
00207 action="store_true")
00208 parser.add_option("--bzr", dest="bzr", default=False,
00209 help="make an entry a bazaar entry",
00210 action="store_true")
00211 parser.add_option("-y", "--confirm", dest="confirm", default='',
00212 help="Do not ask for confirmation",
00213 action="store_true")
00214
00215 parser.add_option("-t", "--target-workspace", dest="workspace", default=None,
00216 help="which workspace to use",
00217 action="store")
00218 (options, args) = parser.parse_args(argv)
00219
00220 if len(args) > 2:
00221 print("Error: Too many arguments.")
00222 print(parser.usage)
00223 return -1
00224
00225 if config == None:
00226 config = multiproject_cmd.get_config(target_path, [], config_filename = self.config_filename)
00227 elif config.get_base_path() != target_path:
00228 raise MultiProjectException("Config path does not match %s %s "%(config.get_base_path(), target_path))
00229
00230 scmtype = None
00231 count_scms = 0
00232 if options.git:
00233 scmtype = 'git'
00234 count_scms +=1
00235 if options.svn:
00236 scmtype = 'svn'
00237 count_scms +=1
00238 if options.hg:
00239 scmtype = 'hg'
00240 count_scms +=1
00241 if options.bzr:
00242 scmtype = 'bzr'
00243 count_scms +=1
00244 if options.detach:
00245 count_scms +=1
00246 if count_scms > 1:
00247 parser.error("You cannot provide more than one scm provider option")
00248
00249 if len(args) == 0:
00250 parser.error("Must provide a localname")
00251
00252 element = select_element(config.get_config_elements(), args[0])
00253
00254 uri = None
00255 if len(args) == 2:
00256 uri = args[1]
00257 version = None
00258 if options.version is not None:
00259 version = options.version.strip("'\"")
00260
00261 if element is None:
00262
00263 localname = os.path.normpath(args[0])
00264 rel_path = os.path.relpath(os.path.realpath(localname),
00265 os.path.realpath(config.get_base_path()))
00266 if os.path.isabs(localname):
00267
00268 if not rel_path.startswith('..'):
00269 localname = rel_path
00270 else:
00271
00272 if not samefile(os.getcwd(), config.get_base_path()):
00273 if os.path.isdir(localname):
00274 parser.error("Cannot decide which one you want to add:\n%s\n%s"%(os.path.abspath(localname),
00275 os.path.join(config.get_base_path(), localname)))
00276 if not rel_path.startswith('..'):
00277 localname = rel_path
00278
00279 spec = PathSpec(local_name = localname,
00280 uri = uri,
00281 version = version,
00282 scmtype = scmtype)
00283 print(" Add element: \n %s"%spec)
00284 else:
00285
00286 old_spec = element.get_path_spec()
00287 if options.detach:
00288 spec = PathSpec(local_name = element.get_local_name())
00289 else:
00290
00291 if version is None:
00292 version = old_spec.get_version()
00293 spec = PathSpec(local_name = element.get_local_name(),
00294 uri = uri or old_spec.get_uri(),
00295 version = version,
00296 scmtype = scmtype or old_spec.get_scmtype(),
00297 path = old_spec.get_path())
00298 if spec.get_legacy_yaml() == old_spec.get_legacy_yaml():
00299 if not options.detach:
00300 parser.error("No change provided, did you mean --detach ?")
00301 parser.error("No change provided.")
00302 print(" Change element from: \n %s\n to\n %s"%(old_spec, spec))
00303
00304 config.add_path_spec(spec, merge_strategy = 'MergeReplace')
00305 if not options.confirm:
00306 abort = None
00307 prompt = "Continue(y/n): "
00308 while abort == None:
00309 mode_input = raw_input(prompt)
00310 if mode_input == 'y':
00311 abort = False
00312 elif mode_input == 'n':
00313 abort = True
00314 if abort:
00315 print("No changes made.")
00316 return 0
00317 print("Overwriting %s"%os.path.join(config.get_base_path(), self.config_filename))
00318 shutil.move(os.path.join(config.get_base_path(), self.config_filename), "%s.bak"%os.path.join(config.get_base_path(), self.config_filename))
00319 multiproject_cmd.cmd_persist_config(config, self.config_filename)
00320
00321 if (spec.get_scmtype() is not None):
00322 print("Config changed, remember to run 'rosws update %s' to update the folder from %s"%(spec.get_local_name(), spec.get_scmtype()))
00323
00324
00325
00326
00327
00328 return 0
00329
00330
00331 def cmd_update(self, target_path, argv, config = None):
00332 parser = OptionParser(usage="usage: rosws update [localname]*",
00333 formatter = IndentedHelpFormatterWithNL(),
00334 description=__MULTIPRO_CMD_DICT__["update"] + """
00335
00336 This command calls the SCM provider to pull changes from remote to
00337 your local filesystem. In case the url has changed, the command will
00338 ask whether to delete or backup the folder.
00339
00340 Examples:
00341 $ rosws update -t ~/fuerte
00342 $ rosws update robot_model geometry
00343 """,
00344 epilog="See: http://www.ros.org/wiki/rosinstall for details\n")
00345 parser.add_option("--delete-changed-uris", dest="delete_changed", default=False,
00346 help="Delete the local copy of a directory before changing uri.",
00347 action="store_true")
00348 parser.add_option("--abort-changed-uris", dest="abort_changed", default=False,
00349 help="Abort if changed uri detected",
00350 action="store_true")
00351 parser.add_option("--continue-on-error", dest="robust", default=False,
00352 help="Continue despite checkout errors",
00353 action="store_true")
00354 parser.add_option("--backup-changed-uris", dest="backup_changed", default='',
00355 help="backup the local copy of a directory before changing uri to this directory.",
00356 action="store")
00357 parser.add_option("-j", "--parallel", dest="jobs", default=1,
00358 help="How many parallel threads to use for installing",
00359 action="store")
00360 parser.add_option("-v", "--verbose", dest="verbose", default=False,
00361 help="Whether to print out more information",
00362 action="store_true")
00363
00364 parser.add_option("-t", "--target-workspace", dest="workspace", default=None,
00365 help="which workspace to use",
00366 action="store")
00367 (options, args) = parser.parse_args(argv)
00368
00369 if config == None:
00370 config = multiproject_cmd.get_config(target_path, [], config_filename = self.config_filename)
00371 elif config.get_base_path() != target_path:
00372 raise MultiProjectException("Config path does not match %s %s "%(config.get_base_path(), target_path))
00373 success = True
00374 mode = _get_mode_from_options(parser, options)
00375 if args == []:
00376
00377 args = None
00378 if success:
00379 install_success = multiproject_cmd.cmd_install_or_update(
00380 config,
00381 localnames = args,
00382 backup_path = options.backup_changed,
00383 mode = mode,
00384 robust = options.robust,
00385 num_threads = int(options.jobs),
00386 verbose = options.verbose)
00387 if install_success or options.robust:
00388 return 0
00389 return 1
00390
00391
00392 def cmd_remove(self, target_path, argv, config = None):
00393 parser = OptionParser(usage="usage: rosws remove [localname]*",
00394 formatter = IndentedHelpFormatterWithNL(),
00395 description=__MULTIPRO_CMD_DICT__["remove"] + """
00396 The command removes entries from your configuration file, it does not affect your filesystem.
00397 """,
00398 epilog="See: http://www.ros.org/wiki/rosinstall for details\n")
00399 (options, args) = parser.parse_args(argv)
00400 if len(args) < 1:
00401 print("Error: Too few arguments.")
00402 print(parser.usage)
00403 return -1
00404
00405 if config == None:
00406 config = multiproject_cmd.get_config(target_path, [], config_filename = self.config_filename)
00407 elif config.get_base_path() != target_path:
00408 raise MultiProjectException("Config path does not match %s %s "%(config.get_base_path(), target_path))
00409 success = True
00410 elements = select_elements(config, args)
00411 for element in elements:
00412 if not config.remove_element(element.get_local_name()):
00413 success = False
00414 print("Bug: No such element %s in config, aborting without changes"%(element.get_local_name()))
00415 break
00416 if success:
00417 print("Overwriting %s"%os.path.join(config.get_base_path(), self.config_filename))
00418 shutil.move(os.path.join(config.get_base_path(), self.config_filename), "%s.bak"%os.path.join(config.get_base_path(), self.config_filename))
00419 multiproject_cmd.cmd_persist_config(config, self.config_filename)
00420 print("Removed entries %s"%args)
00421
00422 return 0
00423
00424
00425
00426
00427
00428
00429
00430
00431
00432
00433
00434 def _get_element_diff(self, new_path_spec, config_old, extra_verbose = False):
00435 """
00436 :returns: a string telling what changed for element compared to old config
00437 """
00438 if new_path_spec is None or config_old is None:
00439 return ''
00440 output = ' %s'%new_path_spec.get_local_name()
00441 if extra_verbose:
00442 old_element = None
00443 if config_old is not None:
00444 old_element = select_element(config_old.get_config_elements(), new_path_spec.get_local_name())
00445
00446 if old_element is None:
00447 if new_path_spec.get_scmtype() is not None:
00448 output += " \t%s %s %s"%(new_path_spec.get_scmtype(), new_path_spec.get_uri(), new_path_spec.get_version() or '')
00449 else:
00450 old_path_spec = old_element.get_path_spec()
00451 for attr in new_path_spec.__dict__:
00452 if (attr in old_path_spec.__dict__
00453 and old_path_spec.__dict__[attr] != new_path_spec.__dict__[attr]
00454 and attr[1:] not in ['local_name', 'path']):
00455 old_val = old_path_spec.__dict__[attr]
00456 new_val = new_path_spec.__dict__[attr]
00457
00458 commonprefix = new_val[:[x[0]==x[1] for x in zip(str(new_val), str(old_val))].index(0)]
00459 if len(commonprefix) > 11:
00460 new_val = "...%s"%new_val[len(commonprefix)-7:]
00461 output += " \t%s: %s -> %s;"%(attr[1:], old_val, new_val)
00462 else:
00463 if (attr not in old_path_spec.__dict__
00464 and new_path_spec.__dict__[attr] is not None
00465 and new_path_spec.__dict__[attr] != ""
00466 and new_path_spec.__dict__[attr] != []
00467 and attr[1:] not in ['local_name', 'path']):
00468 output += " %s = %s"%(attr[1:], new_path_spec.__dict__[attr])
00469 return output
00470
00471
00472 def prompt_merge(self,
00473 target_path,
00474 additional_uris,
00475 additional_specs,
00476 path_change_message = None,
00477 merge_strategy = 'KillAppend',
00478 confirmed = False,
00479 config = None):
00480 """
00481 Prompts the user for the resolution of a merge
00482
00483 :param target_path: Location of the config workspace
00484 :param additional_uris: what needs merging in
00485 :returns: tupel (Config or None if no change, bool path_changed)
00486 """
00487 if config == None:
00488 config = multiproject_cmd.get_config(target_path, additional_uris = [], config_filename = self.config_filename)
00489 elif config.get_base_path() != target_path:
00490 raise MultiProjectException("Config path does not match %s %s "%(config.get_base_path(), target_path))
00491 local_names_old = [x.get_local_name() for x in config.get_config_elements()]
00492
00493 extra_verbose = confirmed
00494 abort = None
00495 last_merge_strategy = None
00496 while abort == None:
00497
00498 if (last_merge_strategy is None
00499 or last_merge_strategy != merge_strategy):
00500 newconfig = multiproject_cmd.get_config(target_path, additional_uris = [], config_filename = self.config_filename)
00501 config_actions = multiproject_cmd.add_uris(newconfig,
00502 additional_uris = additional_uris,
00503 merge_strategy = merge_strategy)
00504 for path_spec in additional_specs:
00505 action = newconfig.add_path_spec(path_spec, merge_strategy)
00506 config_actions[path_spec.get_local_name()] = (action, path_spec)
00507 last_merge_strategy = merge_strategy
00508
00509 local_names_new = [x.get_local_name() for x in newconfig.get_config_elements()]
00510
00511 path_changed = False
00512 ask_user = False
00513 output = ""
00514 new_elements = []
00515 changed_elements = []
00516 discard_elements = []
00517 for localname, (action, new_path_spec) in config_actions.items():
00518 index = -1
00519 if localname in local_names_old:
00520 index = local_names_old.index(localname)
00521 if action == 'KillAppend':
00522 ask_user = True
00523 if (index > -1 and local_names_old[:index+1] == local_names_new[:index+1]):
00524 action = 'MergeReplace'
00525 else:
00526 changed_elements.append(self._get_element_diff(new_path_spec, config, extra_verbose))
00527 path_changed = True
00528
00529 if action == 'Append':
00530 path_changed = True
00531 new_elements.append(self._get_element_diff(new_path_spec, config, extra_verbose))
00532 elif action == 'MergeReplace':
00533 changed_elements.append(self._get_element_diff(new_path_spec, config, extra_verbose))
00534 ask_user = True
00535 elif action == 'MergeKeep':
00536 discard_elements.append(self._get_element_diff(new_path_spec, config, extra_verbose))
00537 ask_user = True
00538 if len(changed_elements) > 0:
00539 output += "\n Change details of element (Use --merge-keep or --merge-replace to change):\n"
00540 if extra_verbose:
00541 output += " %s\n"%"\n".join(changed_elements)
00542 else:
00543 output += " %s\n"%", ".join(changed_elements)
00544 if len(new_elements) > 0:
00545 output += "\n Add new elements:\n"
00546 if extra_verbose:
00547 output += " %s\n"%"\n".join(new_elements)
00548 else:
00549 output += " %s\n"%", ".join(new_elements)
00550
00551
00552 if local_names_old != local_names_new[:len(local_names_old)]:
00553 old_order = ' '.join(reversed(local_names_old))
00554 new_order = ' '.join(reversed(local_names_new))
00555 output += "\n %s (Use --merge-keep or --merge-replace to prevent) from\n %s\n to\n %s\n\n"%(path_change_message or "Element Order change", old_order, new_order)
00556 ask_user = True
00557
00558 if output == "":
00559 return (None, False)
00560 if confirmed or not ask_user:
00561 print(" Performing actions: ")
00562 print(output)
00563 return (newconfig, path_changed)
00564 else:
00565 print(output)
00566 showhelp = True
00567 while(showhelp):
00568 showhelp = False
00569 prompt = "Continue: (y)es, (n)o, (v)erbosity, (a)dvance options: "
00570 mode_input = raw_input(prompt)
00571 if mode_input == 'y':
00572 return (newconfig, path_changed)
00573 elif mode_input == 'n':
00574 abort = True
00575 elif mode_input == 'a':
00576 strategies = {'MergeKeep': "(k)eep",
00577 'MergeReplace': "(s)witch in",
00578 'KillAppend': "(a)ppending"}
00579 unselected = [strategies[x] for x in strategies if x != merge_strategy]
00580 print( """New entries will just be appended to the config and
00581 appear at the beginning of your ROS_PACKAGE_PATH. The merge strategy
00582 decides how to deal with entries having a duplicate localname or path.
00583
00584 "(k)eep" means the existing entry will stay as it is, the new one will
00585 be discarded. Useful for getting additional elements from other
00586 workspaces without affecting your setup.
00587
00588 "(s)witch in" means that the new entry will replace the old in the
00589 same position. Useful for upgrading/downgrading.
00590
00591 "switch (a)ppend" means that the existing entry will be removed, and
00592 the new entry appended to the end of the list. This maintains order
00593 of elements in the order they were given.
00594
00595 Switch append is the default.
00596 """)
00597 prompt = """Change Strategy %s: """%", ".join(unselected)
00598 mode_input = raw_input(prompt)
00599 if mode_input == 's':
00600 merge_strategy = 'MergeReplace'
00601 elif mode_input == 'k':
00602 merge_strategy = 'MergeKeep'
00603 elif mode_input == 'a':
00604 merge_strategy = 'KillAppend'
00605
00606 elif mode_input == 'v':
00607 extra_verbose = not extra_verbose
00608 if abort:
00609 print("No changes made.")
00610 return (None, False)
00611 print('==========================================')
00612