1"""SCM Client definitions.""" 2 3from __future__ import print_function, unicode_literals 4 5import logging 6import os 7import re 8import sys 9 10import pkg_resources 11import six 12 13from rbtools.clients.errors import SCMError 14from rbtools.utils.process import execute 15 16 17# The clients are lazy loaded via load_scmclients() 18SCMCLIENTS = None 19 20 21class PatchAuthor(object): 22 """The author of a patch or commit. 23 24 This wraps the full name and e-mail address of a commit or patch's 25 author primarily for use in :py:meth:`SCMClient.apply_patch`. 26 27 Attributes: 28 fullname (unicode): 29 The full name of the author. 30 31 email (unicode): 32 The e-mail address of the author. 33 """ 34 35 def __init__(self, full_name, email): 36 """Initialize the author information. 37 38 Args: 39 full_name (unicode): 40 The full name of the author. 41 42 email (unicode): 43 The e-mail address of the author. 44 """ 45 self.fullname = full_name 46 self.email = email 47 48 49class PatchResult(object): 50 """The result of a patch operation. 51 52 This stores state on whether the patch could be applied (fully or 53 partially), whether there are conflicts that can be resolved (as in 54 conflict markers, not reject files), which files conflicted, and the 55 patch output. 56 """ 57 58 def __init__(self, applied, has_conflicts=False, 59 conflicting_files=[], patch_output=None): 60 """Initialize the object. 61 62 Args: 63 applied (bool): 64 Whether the patch was applied. 65 66 has_conflicts (bool, optional): 67 Whether the applied patch included conflicts. 68 69 conflicting_files (list of unicode, optional): 70 A list of the filenames containing conflicts. 71 72 patch_output (unicode, optional): 73 The output of the patch command. 74 """ 75 self.applied = applied 76 self.has_conflicts = has_conflicts 77 self.conflicting_files = conflicting_files 78 self.patch_output = patch_output 79 80 81class SCMClient(object): 82 """A base representation of an SCM tool. 83 84 These are used for fetching repository information and generating diffs. 85 """ 86 87 name = None 88 89 #: Whether or not the SCM client can generate a commit history. 90 supports_commit_history = False 91 supports_diff_extra_args = False 92 supports_diff_exclude_patterns = False 93 supports_no_renames = False 94 supports_patch_revert = False 95 96 can_amend_commit = False 97 can_merge = False 98 can_push_upstream = False 99 can_delete_branch = False 100 can_branch = False 101 can_bookmark = False 102 103 #: Whether commits can be squashed during merge. 104 can_squash_merges = False 105 106 def __init__(self, config=None, options=None): 107 """Initialize the client. 108 109 Args: 110 config (dict, optional): 111 The loaded user config. 112 113 options (argparse.Namespace, optional): 114 The parsed command line arguments. 115 """ 116 self.config = config or {} 117 self.options = options 118 self.capabilities = None 119 120 def get_repository_info(self): 121 """Return repository information for the current working tree. 122 123 This is expected to be overridden by subclasses. 124 125 Returns: 126 rbtools.clients.RepositoryInfo: 127 The repository info structure. 128 """ 129 return None 130 131 def check_options(self): 132 """Verify the command line options. 133 134 This is expected to be overridden by subclasses, if they need to do 135 specific validation of the command line. 136 137 Raises: 138 rbtools.clients.errors.OptionsCheckError: 139 The supplied command line options were incorrect. In 140 particular, if a file has history scheduled with the commit, 141 the user needs to explicitly choose what behavior they want. 142 """ 143 pass 144 145 def get_changenum(self, revisions): 146 """Return the change number for the given revisions. 147 148 This is only used when the client is supposed to send a change number 149 to the server (such as with Perforce). 150 151 Args: 152 revisions (dict): 153 A revisions dictionary as returned by ``parse_revision_spec``. 154 155 Returns: 156 unicode: 157 The change number to send to the Review Board server. 158 """ 159 return None 160 161 def scan_for_server(self, repository_info): 162 """Find the server path. 163 164 This will search for the server name in the .reviewboardrc config 165 files. These are loaded with the current directory first, and searching 166 through each parent directory, and finally $HOME/.reviewboardrc last. 167 168 Args: 169 repository_info (rbtools.clients.RepositoryInfo): 170 The repository information structure. 171 172 Returns: 173 unicode: 174 The Review Board server URL, if available. 175 """ 176 return self._get_server_from_config(self.config, repository_info) 177 178 def parse_revision_spec(self, revisions=[]): 179 """Parse the given revision spec. 180 181 The 'revisions' argument is a list of revisions as specified by the 182 user. Items in the list do not necessarily represent a single revision, 183 since the user can use SCM-native syntaxes such as "r1..r2" or "r1:r2". 184 SCMTool-specific overrides of this method are expected to deal with 185 such syntaxes. 186 187 Args: 188 revisions (list of unicode, optional): 189 A list of revisions as specified by the user. Items in the list 190 do not necessarily represent a single revision, since the user 191 can use SCM-native syntaxes such as ``r1..r2`` or ``r1:r2``. 192 SCMTool-specific overrides of this method are expected to deal 193 with such syntaxes. 194 195 Raises: 196 rbtools.clients.errors.InvalidRevisionSpecError: 197 The given revisions could not be parsed. 198 199 rbtools.clients.errors.TooManyRevisionsError: 200 The specified revisions list contained too many revisions. 201 202 Returns: 203 dict: 204 A dictionary with the following keys: 205 206 ``base`` (:py:class:`unicode`): 207 A revision to use as the base of the resulting diff. 208 209 ``tip`` (:py:class:`unicode`): 210 A revision to use as the tip of the resulting diff. 211 212 ``parent_base`` (:py:class:`unicode`, optional): 213 The revision to use as the base of a parent diff. 214 215 ``commit_id`` (:py:class:`unicode`, optional): 216 The ID of the single commit being posted, if not using a 217 range. 218 219 Additional keys may be included by subclasses for their own 220 internal use. 221 222 These will be used to generate the diffs to upload to Review Board 223 (or print). The diff for review will include the changes in (base, 224 tip], and the parent diff (if necessary) will include (parent, 225 base]. 226 227 If a single revision is passed in, this will return the parent of 228 that revision for "base" and the passed-in revision for "tip". 229 230 If zero revisions are passed in, this will return revisions 231 relevant for the "current change". The exact definition of what 232 "current" means is specific to each SCMTool backend, and documented 233 in the implementation classes. 234 """ 235 return { 236 'base': None, 237 'tip': None, 238 } 239 240 def diff(self, revisions, include_files=[], exclude_patterns=[], 241 no_renames=False, extra_args=[]): 242 """Perform a diff using the given revisions. 243 244 This is expected to be overridden by subclasses. 245 246 Args: 247 revisions (dict): 248 A dictionary of revisions, as returned by 249 :py:meth:`parse_revision_spec`. 250 251 include_files (list of unicode, optional): 252 A list of files to whitelist during the diff generation. 253 254 exclude_patterns (list of unicode, optional): 255 A list of shell-style glob patterns to blacklist during diff 256 generation. 257 258 extra_args (list, unused): 259 Additional arguments to be passed to the diff generation. 260 261 Returns: 262 dict: 263 A dictionary containing the following keys: 264 265 ``diff`` (:py:class:`bytes`): 266 The contents of the diff to upload. 267 268 ``parent_diff`` (:py:class:`bytes`, optional): 269 The contents of the parent diff, if available. 270 271 ``commit_id`` (:py:class:`unicode`, optional): 272 The commit ID to include when posting, if available. 273 274 ``base_commit_id` (:py:class:`unicode`, optional): 275 The ID of the commit that the change is based on, if available. 276 This is necessary for some hosting services that don't provide 277 individual file access. 278 """ 279 return { 280 'diff': None, 281 'parent_diff': None, 282 'commit_id': None, 283 'base_commit_id': None, 284 } 285 286 def get_commit_history(self, revisions): 287 """Return the commit history between the given revisions. 288 289 Derived classes must override this method if they support posting with 290 history. 291 292 Args: 293 revisions (dict): 294 The parsed revision spec to use to generate the history. 295 296 Returns: 297 list of dict: 298 The history entries. 299 """ 300 raise NotImplementedError 301 302 def _get_server_from_config(self, config, repository_info): 303 """Return the Review Board server URL in the config. 304 305 Args: 306 config (dict): 307 The loaded user config. 308 309 repository_info (rbtools.clients.RepositoryInfo): 310 The repository info structure. 311 312 Returns: 313 unicode: 314 The server URL, if available. 315 """ 316 if 'REVIEWBOARD_URL' in config: 317 return config['REVIEWBOARD_URL'] 318 elif 'TREES' in config: 319 trees = config['TREES'] 320 if not isinstance(trees, dict): 321 raise ValueError('"TREES" in config file is not a dict!') 322 323 # If repository_info is a list, check if any one entry is in trees. 324 path = None 325 326 if isinstance(repository_info.path, list): 327 for path in repository_info.path: 328 if path in trees: 329 break 330 else: 331 path = None 332 elif repository_info.path in trees: 333 path = repository_info.path 334 335 if path and 'REVIEWBOARD_URL' in trees[path]: 336 return trees[path]['REVIEWBOARD_URL'] 337 338 return None 339 340 def _get_p_number(self, base_path, base_dir): 341 """Return the appropriate value for the -p argument to patch. 342 343 This function returns an integer. If the integer is -1, then the -p 344 option should not be provided to patch. Otherwise, the return value is 345 the argument to :command:`patch -p`. 346 347 Args: 348 base_path (unicode): 349 The relative path beetween the repository root and the 350 directory that the diff file was generated in. 351 352 base_dir (unicode): 353 The current relative path between the repository root and the 354 user's working directory. 355 356 Returns: 357 int: 358 The prefix number to pass into the :command:`patch` command. 359 """ 360 if base_path and base_dir.startswith(base_path): 361 return base_path.count('/') + 1 362 else: 363 return -1 364 365 def _strip_p_num_slashes(self, files, p_num): 366 """Strip the smallest prefix containing p_num slashes from filenames. 367 368 To match the behavior of the :command:`patch -pX` option, adjacent 369 slashes are counted as a single slash. 370 371 Args: 372 files (list of unicode): 373 The filenames to process. 374 375 p_num (int): 376 The number of prefixes to strip. 377 378 Returns: 379 list of unicode: 380 The processed list of filenames. 381 """ 382 if p_num > 0: 383 regex = re.compile(r'[^/]*/+') 384 return [regex.sub('', f, p_num) for f in files] 385 else: 386 return files 387 388 def has_pending_changes(self): 389 """Return whether there are changes waiting to be committed. 390 391 Derived classes should override this method if they wish to support 392 checking for pending changes. 393 394 Returns: 395 bool: 396 ``True`` if the working directory has been modified or if changes 397 have been staged in the index. 398 """ 399 raise NotImplementedError 400 401 def apply_patch(self, patch_file, base_path, base_dir, p=None, 402 revert=False): 403 """Apply the patch and return a PatchResult indicating its success. 404 405 Args: 406 patch_file (unicode): 407 The name of the patch file to apply. 408 409 base_path (unicode): 410 The base path that the diff was generated in. 411 412 base_dir (unicode): 413 The path of the current working directory relative to the root 414 of the repository. 415 416 p (unicode, optional): 417 The prefix level of the diff. 418 419 revert (bool, optional): 420 Whether the patch should be reverted rather than applied. 421 422 Returns: 423 rbtools.clients.PatchResult: 424 The result of the patch operation. 425 """ 426 # Figure out the -p argument for patch. We override the calculated 427 # value if it is supplied via a commandline option. 428 p_num = p or self._get_p_number(base_path, base_dir) 429 430 cmd = ['patch'] 431 432 if revert: 433 cmd.append('-R') 434 435 try: 436 p_num = int(p_num) 437 except ValueError: 438 p_num = 0 439 logging.warn('Invalid -p value: %s; assuming zero.', p_num) 440 441 if p_num is not None: 442 if p_num >= 0: 443 cmd.append('-p%d' % p_num) 444 else: 445 logging.warn('Unsupported -p value: %d; assuming zero.', p_num) 446 447 cmd.extend(['-i', six.text_type(patch_file)]) 448 449 # Ignore return code 2 in case the patch file consists of only empty 450 # files, which 'patch' can't handle. Other 'patch' errors also give 451 # return code 2, so we must check the command output. 452 rc, patch_output = execute(cmd, extra_ignore_errors=(2,), 453 return_error_code=True) 454 only_garbage_in_patch = ('patch: **** Only garbage was found in the ' 455 'patch input.\n') 456 457 if (patch_output and patch_output.startswith('patch: **** ') and 458 patch_output != only_garbage_in_patch): 459 raise SCMError('Failed to execute command: %s\n%s' 460 % (cmd, patch_output)) 461 462 # Check the patch for any added/deleted empty files to handle. 463 if self.supports_empty_files(): 464 try: 465 with open(patch_file, 'rb') as f: 466 patch = f.read() 467 except IOError as e: 468 logging.error('Unable to read file %s: %s', patch_file, e) 469 return 470 471 patched_empty_files = self.apply_patch_for_empty_files( 472 patch, p_num, revert=revert) 473 474 # If there are no empty files in a "garbage-only" patch, the patch 475 # is probably malformed. 476 if (patch_output == only_garbage_in_patch and 477 not patched_empty_files): 478 raise SCMError('Failed to execute command: %s\n%s' 479 % (cmd, patch_output)) 480 481 # TODO: Should this take into account apply_patch_for_empty_files ? 482 # The return value of that function is False both when it fails 483 # and when there are no empty files. 484 return PatchResult(applied=(rc == 0), patch_output=patch_output) 485 486 def create_commit(self, message, author, run_editor, 487 files=[], all_files=False): 488 """Create a commit based on the provided message and author. 489 490 Derived classes should override this method if they wish to support 491 committing changes to their repositories. 492 493 Args: 494 message (unicode): 495 The commit message to use. 496 497 author (object): 498 The author of the commit. This is expected to have ``fullname`` 499 and ``email`` attributes. 500 501 run_editor (bool): 502 Whether to run the user's editor on the commmit message before 503 committing. 504 505 files (list of unicode, optional): 506 The list of filenames to commit. 507 508 all_files (bool, optional): 509 Whether to commit all changed files, ignoring the ``files`` 510 argument. 511 512 Raises: 513 NotImplementedError: 514 The client does not support creating commits. 515 516 rbtools.clients.errors.CreateCommitError: 517 The commit message could not be created. It may have been 518 aborted by the user. 519 """ 520 raise NotImplementedError 521 522 def get_commit_message(self, revisions): 523 """Return the commit message from the commits in the given revisions. 524 525 This pulls out the first line from the commit messages of the given 526 revisions. That is then used as the summary. 527 528 Args: 529 revisions (dict): 530 A dictionary as returned by :py:meth:`parse_revision_spec`. 531 532 Returns: 533 dict: 534 A dictionary containing ``summary`` and ``description`` keys, 535 matching the first line of the commit message and the remainder, 536 respectively. 537 """ 538 commit_message = self.get_raw_commit_message(revisions) 539 lines = commit_message.splitlines() 540 541 if not lines: 542 return None 543 544 result = { 545 'summary': lines[0], 546 } 547 548 # Try to pull the body of the commit out of the full commit 549 # description, so that we can skip the summary. 550 if len(lines) >= 3 and lines[0] and not lines[1]: 551 result['description'] = '\n'.join(lines[2:]).strip() 552 else: 553 result['description'] = commit_message 554 555 return result 556 557 def delete_branch(self, branch_name, merged_only=True): 558 """Delete the specified branch. 559 560 Args: 561 branch_name (unicode): 562 The name of the branch to delete. 563 564 merged_only (bool, optional): 565 Whether to limit branch deletion to only those branches which 566 have been merged into the current HEAD. 567 """ 568 raise NotImplementedError 569 570 def merge(self, target, destination, message, author, squash=False, 571 run_editor=False, close_branch=True): 572 """Merge the target branch with destination branch. 573 574 Args: 575 target (unicode): 576 The name of the branch to merge. 577 578 destination (unicode): 579 The name of the branch to merge into. 580 581 message (unicode): 582 The commit message to use. 583 584 author (object): 585 The author of the commit. This is expected to have ``fullname`` 586 and ``email`` attributes. 587 588 squash (bool, optional): 589 Whether to squash the commits or do a plain merge. 590 591 run_editor (bool, optional): 592 Whether to run the user's editor on the commmit message before 593 committing. 594 595 close_branch (bool, optional): 596 Whether to close/delete the merged branch. 597 598 Raises: 599 rbtools.clients.errors.MergeError: 600 An error occurred while merging the branch. 601 """ 602 raise NotImplementedError 603 604 def push_upstream(self, remote_branch): 605 """Push the current branch to upstream. 606 607 Args: 608 remote_branch (unicode): 609 The name of the branch to push to. 610 611 Raises: 612 rbtools.client.errors.PushError: 613 The branch was unable to be pushed. 614 """ 615 raise NotImplementedError 616 617 def get_raw_commit_message(self, revisions): 618 """Extract the commit messages on the commits in the given revisions. 619 620 Derived classes should override this method in order to allow callers 621 to fetch commit messages. This is needed for description guessing. 622 623 If a derived class is unable to fetch the description, ``None`` should 624 be returned. 625 626 Callers that need to differentiate the summary from the description 627 should instead use get_commit_message(). 628 629 Args: 630 revisions (dict): 631 A dictionary containing ``base`` and ``tip`` keys. 632 633 Returns: 634 unicode: 635 The commit messages of all commits between (base, tip]. 636 """ 637 raise NotImplementedError 638 639 def get_current_branch(self): 640 """Return the repository branch name of the current directory. 641 642 Derived classes should override this method if they are able to 643 determine the current branch of the working directory. 644 645 Returns: 646 unicode: 647 A string with the name of the current branch. If the branch is 648 unable to be determined, returns ``None``. 649 """ 650 raise NotImplementedError 651 652 def supports_empty_files(self): 653 """Return whether the server supports added/deleted empty files. 654 655 Returns: 656 bool: 657 ``True`` if the Review Board server supports added or deleted empty 658 files. 659 """ 660 return False 661 662 def apply_patch_for_empty_files(self, patch, p_num, revert=False): 663 """Return whether any empty files in the patch are applied. 664 665 Args: 666 patch (bytes): 667 The contents of the patch. 668 669 p_num (unicode): 670 The prefix level of the diff. 671 672 revert (bool, optional): 673 Whether the patch should be reverted rather than applied. 674 675 Returns: 676 ``True`` if there are empty files in the patch. ``False`` if there 677 were no empty files, or if an error occurred while applying the 678 patch. 679 """ 680 raise NotImplementedError 681 682 def amend_commit_description(self, message, revisions=None): 683 """Update a commit message to the given string. 684 685 Args: 686 message (unicode): 687 The commit message to use when amending the commit. 688 689 revisions (dict, optional): 690 A dictionary of revisions, as returned by 691 :py:meth:`parse_revision_spec`. This provides compatibility 692 with SCMs that allow modifications of multiple changesets at 693 any given time, and will amend the change referenced by the 694 ``tip`` key. 695 696 Raises: 697 rbtools.clients.errors.AmendError: 698 The amend operation failed. 699 """ 700 raise NotImplementedError 701 702 703class RepositoryInfo(object): 704 """A representation of a source code repository.""" 705 706 def __init__(self, path=None, base_path=None, local_path=None, name=None, 707 supports_changesets=False, supports_parent_diffs=False): 708 """Initialize the object. 709 710 Args: 711 path (unicode or list of unicode, optional): 712 The path of the repository, or a list of possible paths 713 (with the primary one appearing first). 714 715 base_path (unicode, optional): 716 The relative path between the current working directory and the 717 repository root. 718 719 local_path (unicode, optional): 720 The local filesystem path for the repository. This can 721 sometimes be the same as ``path``, but may not be (since that 722 can contain a remote repository path). 723 724 name (unicode, optional): 725 The name of the repository, as configured on Review Board. 726 This might be available through some repository metadata. 727 728 supports_changesets (bool, optional): 729 Whether the repository type supports changesets that store 730 their data server-side. 731 732 supports_parent_diffs (bool, optional): 733 Whether the repository type supports posting changes with 734 parent diffs. 735 """ 736 self.path = path 737 self.base_path = base_path 738 self.local_path = local_path 739 self.name = name 740 self.supports_changesets = supports_changesets 741 self.supports_parent_diffs = supports_parent_diffs 742 logging.debug('Repository info: %s', self) 743 744 def __str__(self): 745 """Return a string representation of the repository info. 746 747 Returns: 748 unicode: 749 A loggable representation. 750 """ 751 return 'Path: %s, Base path: %s, Supports changesets: %s' % \ 752 (self.path, self.base_path, self.supports_changesets) 753 754 def set_base_path(self, base_path): 755 """Set the base path of the repository info. 756 757 Args: 758 base_path (unicode): 759 The relative path between the current working directory and the 760 repository root. 761 """ 762 if not base_path.startswith('/'): 763 base_path = '/' + base_path 764 765 logging.debug('changing repository info base_path from %s to %s', 766 self.base_path, base_path) 767 self.base_path = base_path 768 769 def find_server_repository_info(self, server): 770 """Try to find the repository from the list of repositories on the server. 771 772 For Subversion, this could be a repository with a different URL. For 773 all other clients, this is a noop. 774 775 Args: 776 server (rbtools.api.resource.RootResource): 777 The root resource for the Review Board server. 778 779 Returns: 780 RepositoryInfo: 781 The server-side information for this repository. 782 """ 783 return self 784 785 786def load_scmclients(config, options): 787 """Load the available SCM clients. 788 789 Args: 790 config (dict): 791 The loaded user config. 792 793 options (argparse.Namespace): 794 The parsed command line arguments. 795 """ 796 global SCMCLIENTS 797 798 SCMCLIENTS = {} 799 800 for ep in pkg_resources.iter_entry_points(group='rbtools_scm_clients'): 801 try: 802 client = ep.load()(config=config, options=options) 803 client.entrypoint_name = ep.name 804 SCMCLIENTS[ep.name] = client 805 except Exception: 806 logging.exception('Could not load SCM Client "%s"', ep.name) 807 808 809def scan_usable_client(config, options, client_name=None, 810 require_repository_info=True): 811 """Scan for a usable SCMClient. 812 813 Args: 814 config (dict): 815 The loaded user config. 816 817 options (argparse.Namespace): 818 The parsed command line arguments. 819 820 client_name (unicode, optional): 821 A specific client name, which can come from the configuration. This 822 can be used to disambiguate if there are nested repositories, or to 823 speed up detection. 824 825 require_repository_info (bool, optional): 826 Whether information on a repository is required. This is the 827 default. If disabled, this will return ``None`` for the repository 828 information if a matching repository could not be found. 829 830 Returns: 831 tuple: 832 A 2-tuple, containing the repository info structure and the tool 833 instance. 834 """ 835 from rbtools.clients.perforce import PerforceClient 836 837 repository_info = None 838 tool = None 839 840 # TODO: We should only load all of the scm clients if the client_name 841 # isn't provided. 842 if SCMCLIENTS is None: 843 load_scmclients(config, options) 844 845 if client_name: 846 if client_name not in SCMCLIENTS: 847 logging.error('The provided repository type "%s" is invalid.', 848 client_name) 849 sys.exit(1) 850 else: 851 scmclients = { 852 client_name: SCMCLIENTS[client_name] 853 } 854 else: 855 scmclients = SCMCLIENTS 856 857 candidate_repos = [] 858 859 for name, tool in six.iteritems(scmclients): 860 logging.debug('Checking for a %s repository...', tool.name) 861 repository_info = tool.get_repository_info() 862 863 if repository_info: 864 candidate_repos.append((repository_info, tool)) 865 866 if candidate_repos: 867 if len(candidate_repos) == 1: 868 repository_info, tool = candidate_repos[0] 869 else: 870 logging.debug('Finding deepest repository of multiple matching ' 871 'repository types.') 872 873 deepest_repo_len = 0 874 deepest_repo_info = None 875 deepest_repo_tool = None 876 877 for repo, tool in candidate_repos: 878 if (repo.local_path and 879 len(os.path.normpath(repo.local_path)) > deepest_repo_len): 880 deepest_repo_len = len(repo.local_path) 881 deepest_repo_info = repo 882 deepest_repo_tool = tool 883 884 if deepest_repo_info: 885 repository_info = deepest_repo_info 886 tool = deepest_repo_tool 887 888 logging.warn('Multiple matching repositories were found. ' 889 'Using %s repository at %s.', 890 tool.name, repository_info.local_path) 891 logging.warn('Define REPOSITORY_TYPE in .reviewboardrc if ' 892 'you wish to use a different repository.') 893 else: 894 # If finding the deepest repository fails (for example, when 895 # posting against a remote SVN repository there will be no 896 # local path), just default to the first repository found 897 repository_info, tool = candidate_repos[0] 898 899 if repository_info is not None: 900 # Verify that options specific to an SCM Client have not been mis-used. 901 if (getattr(options, 'change_only', False) and 902 not repository_info.supports_changesets): 903 logging.error('The --change-only option is not valid for the ' 904 'current SCM client.\n') 905 sys.exit(1) 906 907 if (getattr(options, 'parent_branch', None) and 908 not repository_info.supports_parent_diffs): 909 logging.error('The --parent option is not valid for the ' 910 'current SCM client.') 911 sys.exit(1) 912 913 if (not isinstance(tool, PerforceClient) and 914 (getattr(options, 'p4_client', None) or 915 getattr(options, 'p4_port', None))): 916 logging.error('The --p4-client and --p4-port options are not ' 917 'valid for the current SCM client.\n') 918 sys.exit(1) 919 elif require_repository_info: 920 if client_name: 921 logging.error('The provided repository type was not detected ' 922 'in the current directory.') 923 elif getattr(options, 'repository_url', None): 924 logging.error('No supported repository could be accessed at ' 925 'the supplied url.') 926 else: 927 logging.error('The current directory does not contain a checkout ' 928 'from a supported source code repository.') 929 930 sys.exit(1) 931 932 return repository_info, tool 933 934 935def print_clients(config, options): 936 """Print the supported detected SCM clients. 937 938 Each SCM client, including those provided by third party packages, 939 will be printed. Additionally, SCM clients which are detected in 940 the current directory will be highlighted. 941 942 Args: 943 config (dict): 944 The loaded user config. 945 946 options (argparse.Namespace): 947 The parsed command line options. 948 """ 949 print('The following repository types are supported by this installation') 950 print('of RBTools. Each "<type>" may be used as a value for the') 951 print('"--repository-type=<type>" command line argument. Repository types') 952 print('which are detected in the current directory are marked with a "*"') 953 print('[*] "<type>": <Name>') 954 955 if SCMCLIENTS is None: 956 load_scmclients(config, options) 957 958 for name, tool in six.iteritems(SCMCLIENTS): 959 repository_info = tool.get_repository_info() 960 961 if repository_info: 962 print(' * "%s": %s' % (name, tool.name)) 963 else: 964 print(' "%s": %s' % (name, tool.name)) 965